# Web dashboard DarkflameServer ships with a web dashboard for managing accounts, moderating, and watching the server. It replaces the separate NexusDashboard. Master starts it when `enable_dashboard=1` is set in `masterconfig.ini`, and it listens on the `port` in `dashboardconfig.ini` (2006 by default). It also talks to master over UDP on `net_port` and the port after it (2010 and 2011 by default); keep those clear of the other servers' ports. Those UDP ports listen on `bind_ip` from `sharedconfig.ini` like every other server; the web page itself listens on `listen_ip`. This page is for server operators. Everything below the first section is optional. ## Getting started 1. Set `enable_dashboard=1` in `masterconfig.ini` and start the server as usual. 2. Create an operator account from the command line: `./MasterServer -a`. Accounts created this way get GM level 9. 3. Open `http://127.0.0.1:2006` on the server itself and sign in. The dashboard only listens on `127.0.0.1` out of the box (`listen_ip` in `dashboardconfig.ini`). To reach it from other machines, put it behind HTTPS with a reverse proxy (Caddy, nginx, ...) on the same machine, or set `listen_ip=0.0.0.0` if you know what you are doing. Behind a proxy, set in `dashboardconfig.ini`: ```ini secure_cookies=1 behind_proxy=1 dashboard_url=https://dashboard.example.com ``` `dashboard_url` is used for links in emails and webhook alerts. ### Tables Every table remembers how you last sorted it and how many rows it shows per page. This is kept in your browser only (per dashboard user), so it doesn't follow you to another browser; clearing site data resets it. Searches and the page you were on aren't kept. Detail pages (accounts, characters, properties and their 3D view, bug reports, play keys) show breadcrumbs for the way you actually got there in this tab, for example Accounts > an account > a character > a property, so you can step back to where you came from. Opened directly, a page shows its usual parent (Properties > a property). ### Game text and languages Game text on the pages (zone, object, mission and activity names, the game's currency and stat names such as coins, universe score, imagination and reputation, leaderboard headers) comes from the client's `locale/locale.xml` (`client_location`), never from the dashboard itself. The dashboard loads every language in that file at startup. - **Language**: *Game text language* in the user menu (saved on your account), else your browser's language when the locale has it (`de-AT` picks `de_DE`), else `en_US`. A phrase a language lacks falls back to `en_US`, then to its locale key or ID ("Zone 1234"). Dashboard text (buttons, headings, help) stays English. - **For developers**: never write a game name into a template, script or route. Use `routes/GameText.h` in C++ (`GameText::ZoneName(id)`, `Name("Objects", lot)`, `Phrase(key)`, `Expand("%[key]")`; a catalog or help string can hold `%[ZoneTable_1150_DisplayDescription]` and be expanded when served), `game.terms.coins`, `phrase("UI_COINS")` and `zone_name(1150)` in templates, and `GameText.term('coins')` and `GameText.zone(1200)` in scripts. Requests run in the viewer's language; `Workers::Reply` carries it to worker threads, and anything cached with names in it is cached per language. - **Check**: the `GameTextJs` test (`tests/dWebTests/game-text.test.mjs`) fails when a known game name (zone names, item names, U-score, coins) is written into a template, a script or a route's string. A legitimate use goes in `tests/dWebTests/game-text-allowlist.json` with why, or gets a `game-text: ok` comment on its line. Pass a client's `locale.xml` as the third argument to check every zone name in it. ## Files to back up Next to the server binaries, the dashboard creates: - `dashboard_jwt_secret`: signs sign-in sessions. Losing it only signs everyone out. - `dashboard_totp_key`: encrypts two-factor login secrets. **Back this up with your database.** Without it, every account with two-factor login is locked out and has to be reset by staff with `accounts_manage` (GM 8+). You can also set `totp_key` (64 hex characters) in `dashboardconfig.ini` instead. - `dashboard_oauth2_token.json`: only if you connect a mail account with OAuth2. The [Backups](#backups) page copies the database itself (including settings changed on the Settings page), and lists the files a restore also needs: these, your `*.ini` files and `vanity/`. Keep copies of them somewhere safe. ## Access levels and permissions What people can do depends on their account's GM level. Out of the box: | Level | Can | |---|---| | 0 | See their own account, characters, properties, strikes and trade/mail history; the leaderboards and the property showcase; use the API; change their password, email and two-factor login | | 1+ | See the account, character, property and bug report lists; read the activity and command logs | | 2+ | Kick and mute; read and write the moderation history; read player reports | | 3+ | Approve names; handle player reports and bug reports; give strikes; see linked accounts, mailboxes, character history and who is online; rescue, teleport and restrict characters; read the chat log; send mail; economy reports and the world map | | 4+ | Ban and lock accounts; email password reset links | | 5+ | Moderate pet names, guilds, properties and leaderboards; revoke strikes; change a character's missions; change the chat filter's words; send and schedule announcements | | 8+ | Manage accounts (create, change email or password, reset two-factor login) and GM levels; edit characters and replace their XML; give items back; read whispers, team and guild chat; send chat into the game; attach items to mail, to one player or everyone; import models; shut down worlds; schedule restarts and events; run economy checks; play keys, client files and the vanity files; see scheduled tasks, the audit log, server logs, crash dumps, server health and instance load; the developer tools | | 9 | Delete accounts; grant permissions to single accounts and characters; change scheduled tasks, instance limits, settings and permissions (these two are always GM 9 only), webhooks and email settings; backups | Each of these is a named permission, and you can change the lowest GM level allowed for any of them without rebuilding or restarting: - On the **Permissions** page (GM 9): click the lowest level that should have it. Changes apply straight away. The page has tabs for the dashboard permissions, the in-game commands and the config file names, with a search, a category filter and a *Changed only* switch. - In `dashboardconfig.ini`: `permission_=`, for example `permission_accounts_ban=3`. The names are on the Permissions page. - As an environment variable: `PERMISSION_ACCOUNTS_BAN=3`. A level set on the page beats the file and environment; **Reset** on the page goes back to them. Staff permissions go from 1 to 9, so players (GM 0) never get them, and GM 9 can always do everything (`permissions_manage` and `settings` are always GM 9). What players do (their own characters, properties, strikes and trade history, the leaderboards, the property showcase, and API access) can be set anywhere from 0 to 9: raise one to take it away from players, for example `permission_api_access=1` so only staff can use the API. Account basics (password, email, two-factor login, signing out) always work. Changing GM levels is also limited by rank: nobody but GM 9 can give or change a level at or above their own. Sending mail needs `mail_send` (GM 3+), but attaching items needs `mail_items` (GM 8), since it hands out items like `/gmadditem`; mailing items to everyone needs `mail_broadcast_items` as well. Items go only to accounts you may manage; mailing them to your own characters also needs `self_items` (below), and without it mail to everyone leaves them out. Set `min_dashboard_gm_level` to keep lower levels out of the dashboard entirely. Single accounts and characters can also be given or denied a permission or command with a grant (see [Permission grants](#permission-grants)). ### Acting on yourself and on your own rank GM 9 can use every tool on anyone, including their own account and other GM 9 accounts. Two safety rails remain: the last GM 9 account that can still sign in (not banned or locked) can't be demoted, banned, locked or deleted, so make another GM 9 first; and deleting your own account asks you to type your username. Below GM 9, staff can never act on an account with a higher GM level than their own, or raise their own GM level. What else they may do is set on the Permissions page: - `manage_equal_rank` (default GM 9): use tools on other accounts with the same GM level. - `self_tools` (default GM 1): tools that gain nothing, used on your own account and characters: rescue or move your characters, kick yourself, sign yourself out everywhere, email yourself a reset link. - `self_items` (default GM 9): give your own characters something: edit coins, U-score, level and items, restore versions, replace XML, change missions, give lost items back, and mail items to your own characters. Without it, items mailed to everyone skip your own characters. - `self_moderation` (default GM 9): change your own record or account security: ban, mute or lock (and lift them), strikes and warnings, history entries, restrictions on your characters, lowering your own GM level, changing your email, password or two-factor login without the current one, and deleting your own account. Each of these works only together with the tool's own permission. Anything staff do to their own account or characters is marked "(on their own account)" in the audit log and in alerts. The same rules apply to the in-game slash commands that act on another player (see below). ### In-game slash commands The Permissions page also lists every in-game slash command with the GM level it needs. The list comes from the world servers: each one stores the commands it registered in the `slash_commands` table when it starts, so the page is empty until a world has started once. **Commands that do the same thing as a dashboard permission use that permission's level.** Change it on the permission's row and the game follows at once; the command's card says which permission it follows, and the permission's row lists its commands ("In game too"). The pairs: | Dashboard permission | Commands | |---|---| | `accounts_kick` | `/kick` | | `accounts_mute` | `/mute` | | `accounts_ban` | `/ban` | | `server_announce` | `/announce`, `/setanntitle`, `/setannmsg` | | `mail_items` | `/mailitem` | | `moderate_properties` | `/approveproperty` | | `players_view` | `/showall`, `/findplayer`, `/spectate` | | `health_view` | `/uptime`, `/metrics` | | `worlds_manage` | `/shutdown` | | `server_restart` | `/shutdownuniverse` | | `server_live_update` | `/liveupdate` | Commands that only act on your own character (`/gmadditem`, `/givemoney`, `/setcurrency`, `/giveuscore`, `/setlevel`) and `/teleport` aren't paired: they keep their own level. The pairs are declared on the commands (`dashboardPermission` in `SlashCommandHandler.cpp`). The world servers read the permission levels the dashboard stored (its page, `dashboardconfig.ini` and its environment), so set `permission_` for the dashboard only. A `command_level_` value for a paired command (from an older setup, a config file or the environment) still wins: the card marks it "overrides accounts_kick", and **Drop the override** makes the command follow the permission again. For a value in a file or the environment, the page stores `command_level_=permission` over it, which you can also put in the file yourself. Upgrading changes no levels by itself: the first time a world of an existing server starts with this version, each command that now follows a permission but needed a different level before (for example `/mute`, GM 6 in the code while `accounts_mute` is GM 2) keeps that level as an override. The page marks it "kept from before pairing", the audit log records it as `change_command_level` by `[upgrade]`, and **Drop the override** makes the command follow the permission from then on. A new server has nothing to keep, so its commands follow the permissions from the start. Other commands have their own level, `command_level_`, where the name is the first alias in lowercase with anything other than letters and digits turned into `_` (e.g. `command_level_spawn`, `command_level_leave_zone`): - On the page: stored for `worldconfig.ini` and beats the file and environment. Running worlds reload their settings at once; no restart. - In `worldconfig.ini` or `sharedconfig.ini`: `command_level_spawn=6`, picked up when the worlds reload their settings (`/reloadconfig`) or restart. - As an environment variable of the world servers: `COMMAND_LEVEL_SPAWN=6`. Changes need `permissions_manage` and go in the audit log (`change_command_level`, and `change_permission` lists the commands that followed). Some limits come from the code and can't be changed, for paired commands too: - Staff commands (anything above GM 0 in the code) go from 1 to 9, so players never get them. Player commands can be raised to any level, e.g. to stop players using `/pvp`. - `/execute` never goes below GM 8: it runs another command as another player, with that player's GM level. - `/setgmlevel` always stays at GM 0, so staff who lowered their own level can raise it again (it never goes above the account's GM level). - Commands the game client acts on by itself (emotes, team, friend and ignore commands) have fixed levels; the page hides them unless you tick **Commands the client handles**. A value outside those limits is ignored. Commands used at a level above GM 0 are written to the command log. **Commands that act on another player follow the dashboard's rules for its tools.** GM 9 may use them on anyone. Below GM 9, never on an account with a higher GM level (while someone plays at a lower level with `/setgmlevel`, their account's level still counts), on your own level only with `manage_equal_rank`, and on yourself only with the matching permission: - `self_tools`: `/kick`, `/kill` - `self_items`: `/mailitem` - `self_moderation`: `/ban`, `/mute`. `/ban` also refuses the last GM 9 account that can sign in. - Other players only (on yourself they work as before): `/teleport `, `/setlevel `, `/execute as `, and `/tpall`, which leaves players you may not move where they are. A refused command says why in the chat and names the permission. The page shows each command's rule on its card. ### Permission grants A grant gives one account or character a permission or command on top of what its GM level allows; a deny takes one away. What someone may do: allowed = (GM level allows it OR a grant covers it) AND no deny covers it A grant or deny names one of: - a dashboard permission (`permission`); it also covers the in-game commands that follow it - an in-game command (`command`) - every permission of a category (`permission_group`), for example Accounts - every command up to a GM level (`command_group`), 1 to 9 Each has an optional expiry, a note, and who gave it and when. Rules: - A deny beats a grant. Denies never apply to GM 9 accounts. - `settings` and `permissions_manage` stay GM 9 only: they can't be granted and no group covers them. Commands with a fixed level and commands with a floor above GM 1 (`/execute`) can't be granted. - On the dashboard only the account's grants count. In game the account's and the logged-in character's count. - `min_dashboard_gm_level` still keeps lower levels out of the dashboard entirely. - Grants count wherever permissions are checked: pages and API routes, the rank rules (`self_*`, `manage_equal_rank`), API access and API key scopes (a key never does more than its owner may now), and live updates over the WebSocket. **Managing them** needs `grants_manage` (default GM 9). Nobody grants or denies what they don't hold themselves (every permission of a group; a command they may use; command groups only up to their own GM level), and only on accounts the rank rules let them manage (their own with `self_moderation`). Removing a grant follows the same rules. Where: - The **Grants** tab of the Permissions page: every grant in force, the history, and a form that searches accounts or characters by name. With only `grants_manage` the page shows just this tab. - The **Permission grants** card on account and character pages. Players see their own grants there, read-only. The form offers only what you may grant. Changes apply at once: the dashboard reads the account's grants with every request, and online players get them through master (`REFRESH_ACCOUNT` / `REFRESH_CHARACTER`) without relogging. Every change goes in the audit log (`grant_permission`, `deny_permission`, `remove_grant`). Removed grants are kept, so the list is also the history. API: `GET /api/grants/catalog`, `GET /api/grants` (`?account=ID` or `?character=ID`), `POST /api/grants` (`{targetType, target, kind, name, deny, expiresAt, note}`), `POST /api/grants/:id/remove`. Stored in the `permission_grants` table. ## Settings The **Settings** page (GM 9) lists every setting the servers read, grouped by what it's for (Server, Gameplay, Players and access, Email, Dashboard, Data retention) rather than by .ini file; each setting still shows its name and where its value comes from (an .ini file, an environment variable, this page, or the built-in default). That includes settings the shipped .ini files leave out (such as `closed_to_non_devs`), with the default the server uses when nothing sets them. - Inputs match the setting: switches for on/off (saved as `1`/`0`), number boxes with their range and unit, dropdowns for fixed choices, and ID lists that show the zone, item or reward code names. - Settings that only matter when another one is on stay out of the way until it is: the MySQL settings appear when the database is `mysql`, hardcore mode's settings when it's switched on, OAuth2's when email sign-in is `oauth2`. - Edits aren't sent until you press **Save changes** (or Ctrl+S). They're saved together: if one is invalid, nothing is saved and that setting is marked. **Undo** puts one back; **Remove the value set here** goes back to the file's value or the default. - A value set here is used when the files and environment leave the setting out. If a file or the environment sets it, keep **Override the value from the file** ticked so your value wins. - Saving tells every server to reload its settings straight away. Settings marked *restart* are only read when a server starts; the page says which ones need it after saving. Settings marked *unused* are in older .ini files but not read by this version (hidden unless **Show unused** is on). - An environment variable named after a setting in upper case (for example `MAX_CLIENTS`) still overrides the files. Order of priority, highest first: a value set here with override, environment variable, the server's own `.ini`, `sharedconfig.ini`, a value set here, the built-in default. The database connection (`database_type`, `sqlite_*`, `mysql_*`), `jwt_secret` and `totp_key` are shown read-only with a lock: the servers need them before they can read anything from the database, so change them in the .ini file or the environment variable the page names, then restart. The same goes for the programs and folders the servers run or read files from: `backup_mysqldump`, `backup_folder`, `client_location` and `dump_folder` can only be set in the .ini files or the environment, so the `settings` permission can't be turned into access to the machine. Values set for them on the Settings page by earlier versions are ignored. Programs such as `mysqldump` are run directly, without a shell. Secrets (passwords, tokens, keys) from files are never copied into the database; secrets set on the web are stored in the database and can't be read back from the page. Where the rows come from and when they go: every server reports the keys of the files it read when it starts (`ConfigSync::Sync`, table `server_config`). A key taken out of a file is forgotten the next time a server that reads that file starts: its row is deleted when it came from the file (not an environment variable), has no value set on this page, and isn't a permission level. Rows with a value set here stay until that value is removed. The shipped `resources/*.ini` files list every setting in the catalog (`dDashboardServer/routes/SettingsCatalog.cpp`) as `key=default` under a comment with its title and description, or name it in a comment (numbered families such as `event_1`...`event_8` and the icon framing keys); the test `SettingsCatalogTests.ShippedFilesListEveryCatalogSetting` fails when one is missing, and running it with `DLU_WRITE_INI_TEMPLATES=1` adds the missing ones to the files under their section. CMake copies a file that isn't in the build folder yet and appends missing keys (without comments) to one that is; values already in a server's files are never changed by it. ### Setting history The **History** tab of the Settings page (same `settings` permission) lists every change made on the Settings page, and by [scheduled events](#scheduled-events): the value set on the web before and after (and the file's value, which applies when there is none), who changed it and when. A setting changed here has a **History** link on the Settings page for just that setting. **Undo** puts the earlier value back through the same save as the Settings page, so it is checked, audited and recorded as a new change; it is refused if the setting was changed again since (undo the newer change first). Secrets are recorded without their values and can't be undone. Changes to permissions and command levels are in the audit log, not here. ## Signing in and accounts ### Two-factor login Anyone can turn on two-factor login on their account page with an authenticator app (Google Authenticator, Authy, 1Password, Bitwarden, ...). They get 10 single-use recovery codes for a lost phone. To require it for staff, set `require_2fa_gm_level`, for example `3` for moderators and up. Staff at that level who haven't set it up can only reach their own account page until they do. If someone loses their phone and their recovery codes, staff with `accounts_manage` (GM 8+) can use **Reset 2FA** on their account page. ### Forgotten passwords With [email](#email) set up, **Forgot your password?** (`/forgot_password`) sends a reset link. Players with two-factor login can also choose a new password there without email: their username, a current code from their authenticator app, and one of their recovery codes. Both codes are needed, so someone who found the printed recovery codes, or has the phone, can't take the account alone. The recovery code is used up, every dashboard session and API key of the account is signed out, and an emailed reset link that's still open stops working. The game password changes too. It's limited like signing in: 10 tries per address every 15 minutes, and wrong codes count as [failed sign-ins](#failed-sign-ins-locking-and-deleting-accounts). The page gives the same answer for unknown usernames, accounts without two-factor login and wrong codes, so it can't be used to find accounts. Each reset goes in the audit log (`password_reset`), to the `security` webhook event, and by email to the account's address if it has one. Banned and locked accounts can't use it. Turn it off with `password_reset_recovery_codes=0` (Settings, Players and access); with neither email nor this, the page is gone. ### Email Email is used for password resets, address confirmation and [report emails](#saved-views-and-report-emails). Set `smtp_host`, `smtp_from_address` and `dashboard_url`, plus either `smtp_username`/`smtp_password` or OAuth2 (`smtp_auth=oauth2`, needed for Gmail and Microsoft 365). The comments in `dashboardconfig.ini` list every option. Use **Send Test** on your account page to check it (`email_settings`, GM 9). ### Registration Off by default. With `allow_registration=1` (Settings, Players and access) anyone can make an account at `/register`. `registration_requires_play_key` (on) asks for a play key, and `registration_requires_email` (off) for an email address, when email is set up. Staff with `accounts_manage` can also create accounts on the Accounts page. While the auth server uses play keys, a GM 0 account can only log in to the game with one, so give it a key there (create one on the Play keys page first). ### Failed sign-ins, locking and deleting accounts Wrong passwords, wrong two-factor codes, failed password recovery attempts and wrong passwords when downloading a backup all count as failed sign-ins. After 5 of them within 15 minutes from one address, that address can't try that account for 15 minutes; other addresses, including the owner's, still can. As a backstop against guessing from many addresses, 25 failures from anywhere without a successful sign-in in between lock the account for 10 minutes. Unlocking the account on its page ends both. **Lock** on an account page (GM 4+, `accounts_ban`) sets the account's Locked flag: it can't log in to the game or the dashboard until it is unlocked, it is kicked from the game, and its open dashboard pages are closed. Locks are in the moderation history and the audit log. Open dashboard pages (their live-update connections) are checked again every minute and whenever their account changes, so a ban, a lock, a lower GM level or **Sign out everywhere** reaches them straight away instead of when the session runs out. Results of actions that finish later (a kick, an email sent, ...) go only to the account that started the action, and only it can ask for them. **Delete** (GM 9, `accounts_delete`) removes the account's characters with everything tied to them (properties and their models, pets, mail, friends, snapshots, ...) and the rows about the account itself (tokens, recovery codes, moderation history, strikes, login addresses, preferences). It is all or nothing. The audit log, chat log and player reports are kept as the record of what happened. ## Live updates Pages update on their own: world servers tell the dashboard (through master) as soon as they write something it shows, and it pushes that to open browsers. Pages about one thing (a character, an account) update in place when it changes: only the parts the server now shows differently change, and what you're typing stays. When the page can't be patched that way (its layout changed) and you're typing, it offers a refresh instead. Moving between pages doesn't reload the dashboard: the next page is fetched and swapped in, and the menu, the top bar and the live connection stay. A thin bar at the top shows while it loads; if it can't be fetched, the page says so with **Try again** and **Open it normally**. Back, forward, reloading, links opened in a new tab and links with a `#` filter work as before. Pages with a 3D view (World 3D, a property and its 3D view, the UGC server page) and pages reached from them load normally. Leaving a page with unsaved changes (Settings, Vanity) asks first. For page scripts (`static/js/nav.js`): listeners a page adds to `document` or `window`, its `setInterval` timers, its `Live` watchers and its DataTables are removed when another page is swapped in; its scripts run again when it's opened again, and `DOMContentLoaded`/`load` handlers they add run once they have all run. Elements a page appends to `` are removed unless marked `data-nav-keep`. `Nav.go(url)` opens a page, `Nav.refresh()` updates the current one in place; `goTo(url)` and `reloadInPlace()` (`common.js`) do the same, or load normally without `nav.js`. `document` gets `dash:page` after a page is swapped in, `dash:leave` before, and `dash:refreshed` after an in-place update (pages that format server values in the browser, like times, do it again then). A page that must always load on its own adds `data-nav="reload"` to any element; a link with `data-nav="off"` always loads normally. View choices you make on the pages (show staff, filters, the 3D viewer's switches, ...) are saved to your account, so they follow you to other browsers. The menu's groups stay open or closed from page to page (kept in this browser): the group of the page you're on opens and stays open until you close it. On wide screens the ☰ button in the top bar hides the menu, and it stays hidden until you show it again. On narrow screens the menu folds into a **Menu** button, and the moderation queues and online players show as cards with their buttons, so you can approve names or kick someone from a phone. ## Running the server ### Worlds The home page lists every running world with its zone, instance and clone ID, and for staff the property on a clone and the world's address. **Shut down** (GM 8+, `worlds_manage`) closes one instance; its players are disconnected. Accounts without `players_view` (players, by default) see no clone IDs: running property instances are merged into one row per zone with how many properties are open and their players, so nobody can tell who is on which property. The same goes for `/api/status`. The **Server Status** card shows auth, chat and, while master starts it (`enable_ugc_server`), the UGC server. Staff with `health_view` also get a **UGC Server** card: whether it is up and for how long, the models and cars and rockets waiting, made and failed (from the database, the UGC server's work list), its busy workers and the space its files take (from its last traffic report), with a link to the UGC page. `/api/servers/ugc` returns the same. ### Announcements and restarts From the home page: - **Announcement** (GM 5+, `server_announce`): a popup and chat message for everyone online. - **Scheduled restart** (GM 8+, `server_restart`): players are warned in game at 60, 30, 15, 10, 5, 2 and 1 minutes and 30 and 10 seconds before, then the whole server shuts down cleanly. **It does not start itself again**: run it under a process supervisor (systemd with `Restart=always`, a Docker restart policy, ...) so it comes back. Who scheduled it (`GET /api/server/restart`) is also only for `server_restart`; everyone sees when and why. ### Scheduled announcements **Scheduled Announcements** (GM 5+, `announcements_schedule`) repeats an announcement on a schedule: a title and message, every world or only chosen zones, a schedule in the same syntax as [scheduled tasks](#scheduled-tasks) (cron or `@every 2h`, in UTC, at most once a minute), and optional start and end dates. The dialog shows the next sends as you type. **Send now** shows it straight away; the schedule carries on. A send that falls while the dashboard or the master server is down is skipped, not sent late. Creating, changing, sending and deleting are audited. For announcements that belong with something else (said when an event starts, repeated while it is on), add an announcement part to a [scheduled event](#scheduled-events) instead; the sends of both show on its calendar. ### Scheduled events **Scheduled Events** (under Live Ops) switches things on together and off again later. An event has **parts**, any mix of: | Part | Needs | While the event is on | |---|---|---| | **Game feature** | `events_manage` | the feature is in an event setting (see below) | | **Vanity changes** | `vanity_manage` | vanity files are switched on or off, an overlay file is laid over the others and NPCs are taken out (see [Vanity](#vanity)) | | **Live event** | `live_events_manage` | a [live event](#live-events) runs: it starts when the event does and ends when it ends (a live event runs at most 7 days; one cut short that way isn't started again) | | **Announcement** | `announcements_schedule` | a message when it starts, repeated on a schedule while it is on (cron or `@every`, as for [scheduled announcements](#scheduled-announcements)), and another when it ends; each is optional | | **Restart** | `server_restart` | a [scheduled restart](#announcements-and-restarts) when it starts or when it ends, with the warning time and reason you give (one already scheduled is left alone) | Adding, changing or removing a part needs the permission its own page needs; changing an event's name, times or mode, cancelling or deleting it needs the permissions of every part it has. Seeing the page needs any of them; parts you can't change are shown read-only. **When** it is on: - **Once**, from a start to an end (at most a year apart), like the old events calendar; - **By rules**, again and again: every October, full-moon nights, Friday evenings (the rules are below). Its **mode** is *Off*, *Scheduled* (on when its times or rules say) or *Always On*. **Priority** decides which event wins where events that are on change the same vanity NPCs or files: they are laid on from the lowest priority to the highest (by id when equal), so the highest wins. The dashboard checks every 5 seconds. When an event turns on it starts each of its parts, and when it turns off it ends them. Each part remembers whether it was started, so every start and end happens once, even if the dashboard or the servers restart in between; a part that couldn't start (every event setting taken, a live event the client no longer has) says why and is tried again. Changing an event that is on keeps the parts you didn't change running; changed and removed parts are undone (a feature's setting put back, a live event ended, the vanity NPCs respawned) and changed ones started again. Announcements and restarts only act the moment their event starts or ends, so changing one while the event is on doesn't say it again, and deleting an event undoes its parts without its end announcement or restart. **Cancel** (for an event that is on once) switches it off now and ends its parts as if it had ended. Everything is audited, and *Scheduled event started/ended*, *Event waiting for a slot* and the parts' own alerts (*Live event started*, *Restart scheduled*, ...) go to the `server` webhook event. An event that is on once and that the dashboard was down for entirely is marked missed. The page lists the events with what each part last did, when each is on next (in your browser's time) and a month calendar that also shows, dashed, what is scheduled on its own pages: live events, the sends of repeating announcements and a scheduled restart. **Export** copies an event as JSON and **Import** takes one or a list of them. #### Game features and the event settings The game has eight event settings, `event_1` to `event_8`: auth sends them to the client at login, and a world server loads objects that are gated on a feature (`gatingOnFeature` in the zone's scene files) only when the feature is in one of them or already unlocked for the client version (`version_major/current/minor`, the `FeatureGating` table). Pick the feature from the list: every `gatingOnFeature` value in the client's scene files, with the zones that use it, and every `FeatureGating` name (marked when it is already on at your client version, where it changes nothing in the zones). When the part starts, the dashboard puts the feature in the first event setting nothing gives a value (a file, the environment or the Settings page) as a `sharedconfig.ini` value that wins over the files; when it ends, the setting is put back as it was, unless someone changed it by hand meanwhile. A feature that is already in a setting is left there and isn't put in a second one. Both go through the Settings save, so they're in the audit log and the [setting history](#setting-history). If every setting is taken the part waits, and says so. **Event settings now** shows what each setting holds and which event put it there. What takes effect when: new logins get the change straight away. Objects are only loaded when a zone starts, so worlds of the zones that use the feature and are already running keep what they had until they restart; the event lists them with their players, and staff with `worlds_manage` can shut one down from there (a fresh instance starts when someone goes there). #### Rules A schedule by rules is a list of rules, on when **any** of them matches or when **all** of them do: | Rule | JSON | On | |---|---|---| | Every year | `{"type": "yearly", "from": "10-01", "to": "10-31"}` | those days every year, both included; `12-20` to `01-02` runs over the new year | | Once | `{"type": "dates", "from": "2026-12-20", "to": "2027-01-02"}` | between two dates (a date alone is the whole day) or times (`2026-10-31T18:00`, the end not included) | | Days of the week | `{"type": "weekdays", "days": ["fri", "sat"]}` | those days | | Times of day | `{"type": "time_of_day", "from": "18:00", "to": "02:00"}` | every day between those times; may run past midnight | | Moon phase | `{"type": "moon", "phase": "full_moon", "days": 0}` | the day the phase falls on (and `days` either side); or with `"hours": 12` instead, that many hours either side of the exact time. Phases: `new_moon`, `first_quarter`, `full_moon`, `last_quarter` | | Group | `{"type": "group", "match": "all", "rules": [...]}` | rules inside rules (up to 4 deep) | Any rule can have `"not": true`. Moon phases are worked out with the method from Meeus' *Astronomical Algorithms* (to a minute or two), so no dates need entering. Days and times in the rules are in the schedule's time zone (`utcOffset`, in minutes from UTC; a new schedule starts in your browser's). It is a fixed offset: it doesn't follow daylight saving. Mind it for whole-day rules: a full-moon *day* in UTC is 8 PM to 8 PM in New York, so pick your own offset if the day should be yours. The editor builds the rules (or edit the JSON directly), says which time zone they are in, and lists when the schedule will be on next, in your browser's time. #### Examples Halloween every October: `halloween.xml` switched on and `summer.xml` off, the game's Halloween content (a feature; use one your client has), fireworks in Nimbus Station and a greeting when it starts. And a werewolf on full-moon nights that wins over Halloween where they meet: ```json [ {"name": "Halloween", "mode": 1, "priority": 0, "schedule": {"utcOffset": 0, "match": "any", "rules": [{"type": "yearly", "from": "10-01", "to": "10-31"}]}, "parts": [ {"kind": "vanity", "config": {"fileSwitches": {"halloween.xml": true, "summer.xml": false}, "file": "", "removals": []}}, {"kind": "feature", "config": {"feature": "oct2011content"}}, {"kind": "live_event", "config": {"type": "celebration", "title": "Halloween fireworks", "message": "", "zones": [1200], "instance": -1, "config": {"effectId": 4, "effectType": "fireworks", "interval": 30}}}, {"kind": "announcement", "config": {"title": "Halloween", "message": "Spooky season has begun!", "zones": [], "atStart": true, "repeat": "", "endMessage": ""}}]}, {"name": "Full moon", "mode": 1, "priority": 10, "schedule": {"utcOffset": 0, "match": "all", "rules": [{"type": "moon", "phase": "full_moon"}, {"type": "time_of_day", "from": "20:00", "to": "24:00"}]}, "parts": [{"kind": "vanity", "config": {"file": "full-moon.xml", "removals": ["Friendly Farmer"], "fileSwitches": {}}}]} ] ``` An event that is on once has `"schedule": null` and `"startsAt"`/`"endsAt"` (unix seconds) instead. ### Live events **Live Events** (GM 8+, `live_events_manage`) starts something in game now, for 1 minute to 7 days, in the zones you pick (and optionally one instance of them). It is announced in those zones when it starts and when it ends (with a summary and the top players), audited, and sent to the `server` webhook event. Every running world of those zones runs it in each of its instances, worlds that open before it ends join in, and everything it spawned is removed when it ends, is ended early (**End now**), or the world shuts down. The page shows each instance's progress live and the top players. - **Treasure hunt**: hides up to 50 objects (picked from the client's objects) per instance, spread out on ground the zone itself uses (where its NPCs walk and its enemies spawn, snapped to the navmesh when there is one). A player finds one by walking up to it (or smashing it), for coins, an item and an effect you choose. - **Bonus**: multiplies coins from loot, U-score from missions and achievements, and loot drop chances (1 to 10). With no zones it applies everywhere. Bonuses running at once don't stack; the biggest counts. - **Invasion**: waves of enemies (up to 5 kinds from the client's enemies) around the zone's spawn point or a player, with a counter of who smashed how many. At most 60 are alive per instance. - **Celebration**: plays an effect from the client's BehaviorEffect table on every player every few seconds. Players see what is running in their world with `/challenge`. To run one at set times (every Friday evening, with Halloween), add a live event part to a [scheduled event](#scheduled-events); it shows here while it runs. Treasure hunts and invasions announce their end in each world with that world's count. The dashboard announces the end of other kinds of events. ### Community challenges **Challenges** (everyone with `challenges_view`, GM 0; changing them needs `challenges_manage`, GM 8) are goals the whole server works on, such as "smash 10,000 enemies this week": a player statistic or a map event kind (optionally only one object, only some zones, and staff left out unless ticked), a target, a start and an end. World servers count each character's part where the game records it (what the client reports on its own doesn't count). The dashboard announces it in game when it passes each of `challenge_milestones` (25, 50, 75 by default) and when it's complete. When the target is reached, every character that added at least the reward minimum gets the rewards: items by mail (one mail per kind), coins in game (online players at once, others the next time they type `/challenge`). Nobody is rewarded twice. Creating, changing, cancelling, completing and rewarding are audited. In game, `/challenge` shows the running challenges, how far they are and what you added, and gives any coins waiting. With the public status page on, public challenges (running, or finished in the last week) and running live events show there too (`public_status_challenges`, `public_status_events`). The complete and over announcements are sent once the dashboard can reach the worlds: a challenge that ends while the dashboard is restarting is announced when it comes back, up to an hour after it ended. Milestones work the same way. ### Scheduled tasks The **Scheduled Tasks** page (GM 8+ to view, GM 9 to change) lists the jobs the dashboard runs on its own: | Task | Default schedule (UTC) | Does | |---|---|---| | `economy_checks` | `15 0 * * *` | Anomaly checks for the day before | | `economy_compaction` | `30 0 * * *` | Merges old ledger rows into months, deletes old trades and mail | | `log_pruning` | `45 0 * * *` | Deletes old log rows, chat, login addresses, health samples, player positions and task runs | | `message_capture_pruning` | `50 0 * * *` | Deletes saved message inspector captures past `inspector_session_days` or over `inspector_max_mb` | | `character_snapshots` | `0 4 * * *` | Saves every character that changed since its last snapshot | | `database_backup` | `30 3 * * *`, **off** | Copies the database (see [Backups](#backups)) | | `report_emails` | `0 7 * * *` | Emails saved Economy views (weekly ones on Mondays) | | `lift_expired_bans` | `5 * * * *` | Unbans accounts whose temporary ban has ended | | `pet_names_auto_approve` | `0 * * * *` | Approves pending pet names that were approved for another pet before | | `pet_owners` | `*/30 * * * *` | Looks up the owners of pets named before the game saved them | For each task you can change the schedule, switch it off (it then only runs when someone presses **Run now**) and run it straight away. Every run is kept with its log for `log_task_days` days; click a run to read it, or **Log** to follow one that is still running. Schedules are cron expressions in UTC (`minute hour day-of-month month day-of-week`), `@hourly`/`@daily`/`@weekly`/ `@monthly`, or a fixed interval like `@every 10m`. The edit dialog shows the next few run times as you type. If the dashboard was down when a task was due, it runs once when the dashboard starts. The `economy_checks` setting from earlier versions is replaced by the task's on/off switch. ### Backups The **Backups** page (GM 9) makes a copy of the database and lists the copies on the server. SQLite databases are copied with `VACUUM INTO` while the servers keep running; MySQL databases with `mysqldump` (set `backup_mysqldump` if it is called something else, such as `mariadb-dump`). Copies go in `backup_folder` (default `backups` next to the server) and only the newest `backup_keep` (default 7) are kept. Scheduled backups are **off** until you switch the `database_backup` task on (daily at 03:30 UTC; change the time on Scheduled Tasks). Downloading a backup asks for your password and two-factor code again, because it contains every account's password hash and email address; each download is audited and sent to security webhooks. Wrong passwords and codes there count as [failed sign-ins](#failed-sign-ins-locking-and-deleting-accounts). Backups are written with owner-only permissions (0600). Every new backup is checked before older ones are deleted, and one that fails its check is thrown away and the older ones kept: SQLite copies get `PRAGMA integrity_check` and a row count of every table; MySQL dumps must be complete (end with `-- Dump completed`). **Verify** checks a backup again: it only reads the file, runs in the background, and is audited (`verify_backup`, `verify_backup_result`). For SQLite it also reports any two-factor secrets this server's key can't decrypt. A backup is only the database, including settings changed on the Settings page. The page lists what a restore also needs; keep copies of these somewhere safe as well: `dashboard_totp_key` (or `totp_key` in `dashboardconfig.ini`), your `*.ini` files, `vanity/` and `dashboard_oauth2_token.json`. **Restoring SQLite:** stop all servers; copy the backup over the database file (`sqlite_database_path`); **delete `-wal` and `-shm`** if they exist (a leftover `-wal` from the old database corrupts the restored one); start the servers. **Restoring MySQL:** stop all servers, then ```sh mariadb -e "DROP DATABASE dlu; CREATE DATABASE dlu" mariadb dlu < dlu-YYYYMMDD-HHMMSS.sql ``` (or `mysql`). Dumps from MariaDB 11+ begin with a sandbox-mode line that MySQL's client rejects; restore those with `mariadb`, or delete the first line. Start the servers. Restoring an older backup is fine: migrations bring it up to date when the servers start. ### Webhooks and alerts On the **Webhooks** page (GM 9) add Discord, Slack or generic JSON webhooks and choose which events they get: `bug_report`, `pending_name` (names waiting for approval), `moderation` (bans, locks, mutes, kicks and restrictions, from the dashboard or in game), `security` (GM level and two-factor changes, API keys, recovery code use), `economy_flag` and `server` (auth, chat or, while it is enabled, the UGC server going down or coming back, restarts, events). JSON deliveries can be signed: with a secret set, each request has `X-DLU-Signature: sha256=`. ### Server health and instance load **Server Health** (GM 8+, `health_view`) charts players online, running worlds, memory used by all server processes, and whether auth, chat and the UGC server were up (the UGC server only while master starts it; grey where it was off), over the last day, week or month (sampled once a minute, kept for `health_days`, 30). The **Servers** table lists every server master knows (master, auth, chat, the dashboard, the UGC server, each world) with its state, how long it has been up, and on Linux, when it runs on the dashboard's machine, its process ID, memory and CPU (of one core, since the table last refreshed), plus what its last traffic report said: connections, ping, worker threads, and for the UGC server its queue and storage. Server processes of this build that master doesn't list show as "Not listed". `/api/servers` returns the table's data. **Prometheus metrics** are off until `metrics_enabled` is switched on (Settings > Dashboard > Metrics). `/metrics` then serves the Prometheus text format: players online (total, per zone and per instance), running worlds per zone, whether master, auth, chat and the UGC server are up (`darkflame_ugc_enabled`, `darkflame_ugc_up`), memory and process count per kind of server, the UGC server's work list (`darkflame_ugc_items`, labels `kind` model or modular and `state` pending, done or failed), dashboard uptime and WebSocket clients, chat messages by channel (and how many the filter blocked), today's coins, items and map events, moderation queues (names, pet names, properties, bug reports, economy flags, player reports), active strikes, each scheduled task's last run, and how long the database reads behind them took. Labels never contain account or character names or addresses. A scraper sends `Authorization: Bearer` with either an API key of an account with the `metrics_view` permission (GM 8 by default), or the shared `metrics_token` (at least 16 characters). `metrics_allowed_ips` can limit where requests come from (addresses or IPv4 ranges like `10.0.0.0/8`), and the numbers are worked out at most every `metrics_cache_seconds` (10), however often they are fetched. `/api/metrics` returns the same text for API keys (the key needs `metrics_view` in its scope). An example scrape config and Grafana dashboard are in `docs/grafana/`. **Instance Load** (`health_view`) records the players in each world instance at the same time and keeps them as long: the busiest zones (most players in one instance, most instances and most players at once), and for a zone each instance's players over time against its caps. How many players an instance takes is decided by the master server: it sends a player to a running public instance of the zone while it has fewer players than the zone's **soft cap** (joining a friend: the **hard cap**), and otherwise starts a new instance; the hard cap is also the most connections that world server accepts. The caps come from the client's `ZoneTable` (`population_soft_cap`, `population_hard_cap`). With `instances_manage` (GM 9) you can set other caps per zone (up to 500), and up to 5 **spare instances**: the master keeps that many instances of the zone with room running (checked every 10 seconds, one started at a time), so players moving in don't wait for a world to start. A spare that stays empty still shuts itself down after 30 minutes like any empty world, and the master starts another. A spare that stops before it has been up for 5 minutes (a crash, a missing file, a port in use) counts as a failure: the next try waits 10 seconds, and each failure in a row doubles the wait, up to 30 minutes, so a broken zone isn't restarted over and over. One that stays up for 5 minutes resets it; the master's log says how long it waits. Changes reach the master straight away (no restart): new instances take the new caps, and running ones too, but a running instance's hard cap can only go down, not above what its world server was started with. Private instances and character selection aren't affected. ### Traffic diagnostics **Diagnostics** (`health_view`, under Logs & Health in the sidebar) shows the load on every server: packets and bytes in and out per second (all servers together, or one picked from the list or by clicking it), packets per second of each server, HTTP requests per second of the dashboard and the UGC server with their errors, HTTP latency (p50, p95 and p99 of the busiest web server), the busiest packet and game message types, and the HTTP routes with their request counts, errors and latency. **Live** shows the last 5 minutes at one second and updates as reports arrive (the `traffic` WebSocket topic); **1 hour** is at 10 seconds; **24 hours** (5 minutes) and **7 days** (30 minutes) come from the database. The servers table also shows each server's RakNet connections, average ping and resent messages, and the worker threads of the dashboard and the UGC server. While the UGC server is enabled, its card says whether it is up (throttled, paused), how many items wait and failed (a link to the failed ones), its busy workers, CPU, memory and stored files; what it made and how long each took is on the UGC page. How it is counted: - Every server (master, auth, chat, each world, the dashboard's master link, UGC) counts in `TrafficStats` (`dCommon/TrafficStats.h`): `dServer` counts each packet it receives (`Receive`, `ReceiveFromMaster`) and sends (`Send`, `SendToMaster`, `Disconnect`) into one-second buckets, keyed by service and packet ID, and for game messages the game message ID; replica construction and serialization are counted under `RAKNET`. A broadcast counts once per connection it goes to. Counting is on the main loop only and costs about 40 ns a packet. Each packet is also counted by peer: the server's own connections (players on auth and worlds; the worlds on chat and every server on master count as other servers), its master link, or other servers (a world's chat link, counted in `ChatServerLink`). - The web server (`dWeb`) counts each request under its route pattern (`GET /api/players/:id`, so there is one entry per route; unknown paths are `(no route)`), its status class, the bytes of the body (a served file: its size) and a latency histogram. A deferred request counts when its answer goes out, so its latency includes the worker's time. Requests carrying `X-Darkflame-Server` (the dashboard's requests to the UGC server) count as another server's; the dashboard counts the requests it makes to the UGC server and the bytes they answer with. Each client address's requests and bytes are counted too, for the Network page's connection list. - Every 5 seconds a server sends `SERVER_TRAFFIC` (`dNet/master/ServerTraffic.h`, about 600 bytes) to master with its seconds, its 24 busiest message types each way, its routes, RakNet's statistics for its connections (datagrams, resends, ping) and a few gauges (`http_deferred_pending`, `websocket_clients`, `workers_busy`, `workers_queued`, `workers_threads`; the UGC server adds `ugc_made_total`, `ugc_failed_total` and `ugc_evicted_total` since it started, `ugc_stored_bytes` and `ugc_max_storage_bytes`). Master passes them to the dashboard and sends its own there; the dashboard keeps its own. Newer servers add optional sections at the end of the report, each after a marker byte: each second's packets by peer with its HTTP requests from and to other servers (about 20 bytes a second), and the 32 busiest remote ends (RakNet's datagrams, bytes, resends and ping for each connection, with the player's account and character on worlds; requests and bytes for each HTTP client address) with the rest summed, and the main loop's frame timing (see Performance). Reports without them (older servers) still read, and older readers stop before them. - The dashboard keeps the last hour at one second in memory, the busiest message types per minute for an hour and per hour for a day, and writes one row per server and minute to `server_traffic` once a minute (in one batch, on the background thread), kept for `traffic_days` (30; Settings > Data retention, pruned by the Log pruning task). Latency percentiles are worked out from mergeable histograms (three buckets per doubling, from 0.1 ms), so a minute's are right; over 24 hours and 7 days a point shows the worst minute's. ### Network **Network** (`health_view`, next to Diagnostics) draws the traffic live, from the same reports, in six columns: game clients on the left, then auth, the worlds (one box per zone) and any other server that reports, then chat (every world links to it), then master, then the dashboard and the UGC server, and web clients (browsers and API users) on the right. Links leaving a side of a box spread down it in the order of their other ends, so they don't all meet at one point. Servers appear as they report, so a new kind of server shows up without changes. Each link has a lane each way, as thick as its bytes per second, with dashes moving faster with more packets, and coloured by its load against its own peak over the last 5 minutes; hover it for the numbers. Boxes show connections, average ping, resends, busy workers and live dashboard pages. Clicking a box shows its links, its busiest message types each way over 5 minutes, its packets per second over 10 minutes and a link to Diagnostics filtered to it (`/diagnostics?server=`). It updates with the `traffic` WebSocket topic (every 2 seconds while reports arrive), stops drawing while the tab is hidden. **Diagram** or **List** (each box with its links, for phones) is remembered per browser. Drag boxes to rearrange the diagram; links leave from the sides that face each other, and **Reset layout** puts everything back. Each server box shows the port it listens on ("Auth :1001"; the dashboard and the UGC server their web port; a zone its instance's port while it has one), and an open zone's instance rows show theirs. The ports and each server's machine come from master's server list, which the dashboard asks for every 30 seconds and master sends when a server comes or goes: master keeps the address and port each server reported when it connected, and its machine is the address master sees that connection come from (servers connecting over loopback or from master's `external_ip`, and worlds that haven't connected yet, are on master's machine, named by master's `external_ip`). Hover a box, open its details or use the List view to see its machine. Player rows and the Connections table show remote ports with `network_ips` only, like the addresses. **Several machines.** When the servers run on more than one machine, the boxes of columns 2 to 5 (auth and the worlds, chat, master, the dashboard and UGC) are grouped by machine: each machine's boxes are a band of their own, master's machine first and the others under it, each framed with the machine's name, its servers, connections and bytes per second. A zone with instances on several machines is a box on each. ▾ on a frame folds the machine into one box (its links go to that box, the ones inside it are gone; ▸ on the box shows its servers again); folded machines are remembered per browser. Links between two machines are dash-dotted with a ◇ half way, and their tooltip names both machines. The game and web clients stay on the sides, centred on all the bands. A dragged box takes its frame along. With one machine nothing is framed and the page looks as before. Machines are named by a token without `network_ips` (the summary sent to every open page has only tokens; `/api/diagnostics/network/connections` maps them to addresses in `hosts` for viewers with the permission). Game clients, Web clients and a zone with more than one instance are groups: ▸ on the box (or a double click) opens it in place. The box grows into a list of its members, one row each (players, and HTTP clients of the UGC server only, under Game clients; one row per address and signed-in dashboard account under Web clients; each instance of a zone), with a filter at the top that matches the name, the account or dashboard user, the instance and, with `network_ips`, the address. At most 8 rows show at once and the rest scroll inside the box. Each row in view is a link end of its own and shows only its member's traffic; the open box's header has no links, since its traffic is its rows'. Closed, the box shows the summed links as before. Opening moves only the boxes under it in its column. Open groups are remembered per browser; the filter and the scroll position last while the page is open and survive the live updates. The List view nests an open group's members, with their links, under it, with the same filter. The rows and filters are HTML laid over the SVG diagram, made once and then only moved and updated, so a redraw never takes the focus, the typed text or the scroll position. The links, and how exact they are: | Link | From | Exact? | | --- | --- | --- | | Game clients - auth, worlds | Each server's packets with its own connections | Yes (LU packets, not RakNet's acknowledgements and resends) | | Server - master | Each server's packets with its master link | Yes | | World - chat | Each world's chat link | Yes | | Web clients - dashboard | The dashboard's HTTP requests less other servers' | Requests and answered bytes; request bytes aren't counted per second | | Game clients - UGC server | The UGC server's HTTP requests less the dashboard's | As above; any browser fetching UGC files counts here too | | Dashboard - UGC server | The requests the dashboard made and the bytes answered | Yes | | Anything of a server that reports no split (older server) | Its totals, drawn dashed and marked estimated | No | **Connections** lists every server's remote ends from its last report, grouped by address: game clients (RakNet datagrams and bytes each way, ping, resends, and on worlds the logged-in account and character), web clients (requests and bytes of the dashboard and the UGC server, with the dashboard account each was signed in as) and server links, with the rest of each server's connections summed. The web server counts each client address's requests by the account the request was signed in as (the dashboard's session cookie or an API key, from the auth middleware), so several people behind one address are separate entries; requests made before signing in are one more entry for the address. The UGC server has no sessions (browsers get UGC files through the dashboard, which counts them as that user's requests), so its HTTP clients carry no user. User names (like players' account names) are shown to anyone with `health_view`. IP addresses are personal data: they are shown only with `network_ips` (Network addresses, level 9 by default; grant it like any other permission) and are otherwise replaced by a token that stays the same for the same address until the dashboard restarts. They are kept in memory from the last report only and never written to the database. - `GET /api/diagnostics/network`: the live summary (what the `traffic` topic sends), with each server's listening `port` and its machine as a `host` token (both null until master's server list names the server). - `GET /api/diagnostics/network/server?key=world:1200:3`: one server's details. - `GET /api/diagnostics/network/connections`: the connection list (addresses and remote ports with `network_ips`; `user` and `account_id` on web clients that were signed in; `hosts`, each machine token to its address, with `network_ips` only). **Prometheus**: `/metrics` has the same counters, totals since the dashboard started: `darkflame_net_packets_total`, `darkflame_net_bytes_total` and `darkflame_net_datagrams_total` (labels `server`, `direction`), `darkflame_net_messages_total` (`server`, `direction`, `service`, `message`), `darkflame_net_resends_total`, the gauges `darkflame_net_connections` and `darkflame_net_ping_milliseconds`, `darkflame_http_requests_total` (`server`, `route`, `status` class), `darkflame_http_response_bytes_total` and the histogram `darkflame_http_request_duration_seconds` (buckets at every doubling from 0.1 ms), and the reported gauges as `darkflame_server_`. `server` is `master`, `auth`, `chat`, `dashboard`, `ugc` or `world::`. Use `rate()` for per-second values. ### Performance **Performance** (`health_view`, next to Network) shows how long each server's main loop takes and what it spends the time on. It answers "why did this world stall": a frame that took a minute shows as one bar, with the scopes that took the time (for example `LoadPlayer > CreateEntity > Component INVENTORY > InventoryComponent::LoadXml`, and the `CDClient Objects` lookups under it with how many there were). - **Servers**: every server's frames per second, average, p95 and longest frame, how busy its loop was (the share of wall time spent in frames rather than sleeping or waiting) and its slow frames, over the last 5 minutes. Click one to pick it (`/performance?server=world:1200:3`). - **Frame time** of the picked server (average, p95, longest per second; the dashed line is the slow frame threshold) and **Time per phase**, stacked, in milliseconds per second: packets, entities, physics (on the dashboard and the UGC server, web requests), replica, database, CDClient and other (other includes script timers and log flushes; the tooltip lists every phase). **5 minutes** is at one second, **1 hour** at 10 seconds. - **Longest frames** of the last 10 minutes and **Packet handling** (the packet and game message types that took longest to handle over 5 minutes: count, total, average, longest). - **Slow frames**: the last 50 frames of every server over `slow_frame_ms` (Settings > Logging and crashes, default 250, 0 turns it off). Click one for its timeline: each scope from when it first started, as long as it took, nested. A scope entered many times (a lookup in a loop) is one bar of all its time with the count. The server also logs each one as one line, e.g. `Slow frame: 58213 ms (cdclient 55012 ms, packets 3100 ms): Packet LOAD_LEVEL_COMPLETE 58.2 s > LoadPlayer 58.2 s > CreateEntity 58.1 s > Component INVENTORY 58.0 s, CDClient Objects 55.0 s x9800` (the log shows packet and component numbers; the page names them). Work outside the main loop's frames that has scopes (a world's zone load at startup, a web request handled between the dashboard's ticks) is timed the same way, marked "outside loop", and doesn't count as a frame. - **Profiling**: with `profiling_run` (Run profiling, GM 8 by default; grant it like any other permission) pick a server and 1 to 60 seconds. The server merges every frame's scope tree for that long and sends it back; **Show** draws it as a flame graph (widths are time over all frames, click a bar to zoom in) and **Folded stacks** downloads it in the format other flame graph tools read (`speedscope`, `inferno`, `flamegraph.pl`). One session per server at a time; **Stop** ends one early with what it has. Starting one is in the audit log (`profile_server`). Sessions are kept in memory (the last 20). How it is measured (`dCommon/Profiler.h`): - Every server marks its main loop's frames and scopes inside them on the main thread only (any other thread's scopes do nothing, so worker threads never touch it). A scope costs two `steady_clock` reads and a short search among its parent's children; the frame's scope tree is reused from frame to frame. A scope can name the phase its time counts as; time in nested phases counts once, as the innermost. - Scopes: the loop's phases on every server (packet handling per packet type, entity updates, physics step, ghosting, replica serialization and update, spawners, log flush, saving characters, each dashboard module's update, each web request by route), and the known heavy spots: loading a player (`LoadPlayer`), `CreateEntity`, each component's construction (`Component `), `InventoryComponent::LoadXml`, script timers, a world's zone load, and game database queries (`Database query`, in the MySQL and SQLite query helpers). - CDClient statements are timed by SQLite itself (`sqlite3_trace_v2` on the CDClient connection): each one the main thread runs counts as `CDClient ` under the scope that ran it, in the CDClient phase. - Every 5 seconds the traffic report (`SERVER_TRAFFIC`, see Traffic diagnostics) carries an optional section after marker 3: per second the frames, their total and longest time, a frame time histogram (the traffic diagnostics' mergeable buckets) and each phase's time; the 16 packet types that took longest; the 3 longest frames with their 12 heaviest scopes; and the slow frames with their 40 heaviest scopes. Reports without it (older servers) still read, and older readers stop before it. A server whose loop is stuck sends nothing; the seconds it missed come with the next report as seconds without frames. - Profiling sessions: `PROFILE_REQUEST` (dashboard -> master -> the server) and `PROFILE_RESULT` (server -> master -> dashboard; `dNet/master/Profiling.h`). The dashboard profiles itself without master. Sessions record every scope exactly rather than sampling (the scope tree exists anyway); only instrumented scopes appear, time in a scope's own code is its "own" time. - `GET /api/diagnostics/performance?server=&range=5m|1h`, `GET /api/diagnostics/performance/slow`, `GET /api/diagnostics/performance/profiles`, `GET /api/diagnostics/performance/profiles/:id`, `POST /api/diagnostics/performance/profiles` (`{server, seconds}`, `profiling_run`), `POST /api/diagnostics/performance/profiles/:id/stop` (`profiling_run`). The built-in view shows only instrumented scopes. For a native deep dive: - **Tracy** (BSD-3-Clause, optional): configure with `-DDLU_TRACY=ON` (CMake fetches Tracy 0.11.1's client). Every server's frames (frame marks) and scopes (zones, packets with their packed type as the zone value) then also go to Tracy, which collects only while a viewer is connected. Run Tracy's viewer (`tracy-profiler`, the same version) on your machine and connect to the server's address; each server process listens on port 8086, the next ones started on the same machine on 8087 and up (see the server's log). Keep the port closed to the internet (a firewall or an SSH tunnel, `ssh -L 8086:localhost:8086 server`). Tracy's sampling (every function, call stacks) needs the viewer's permissions on the server machine (on Linux, root or `perf_event_paranoid` lowered). - Or a sampling profiler on the server process: on Linux `perf record -g -p ` then `perf script` into a flame graph tool; any native profiler on Windows and macOS. The dashboard doesn't run native profilers itself. ### Logs and crash dumps **System Log** (GM 8+, `logs_system`) shows the end of any server's log file (the newest by default; pick an older one from the list), says which file you're looking at and when it was last written, and can download it. It also searches the newest log files of one or all servers. The UGC server's logs are `UgcServer_