- USER_SAFETY.md: section 5 becomes decisions S6-S10 (an account to report, game-identity blocks allowed, staff cannot be blocked, a block only hides, staff see hidden chat marked). - DEMO_MODE.md: DM33-DM35 (visibly fake 1234567890-prefixed Steam and Discord ids plus the org lead's own Steam id; Rust-themed invented names). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
727 lines
56 KiB
Markdown
727 lines
56 KiB
Markdown
# Demo mode — `demo.runicgateway.com`
|
|
|
|
**Status:** design **fully decided** 2026-10-23 (DM1-DM18, §9). §10 step 1 (core, `MODULE_API`
|
|
1.18.0) is **built**, and so are steps 2-5 (the module with its core hook `MODULE_API` 1.19.0, the
|
|
kit, the app, and the deployment, live since 2026-10-27). Step 6 is **built on the site side**
|
|
(2026-10-28) and waits on the Play Console.
|
|
**Issue:** `RunicGateway/website#222`. **Why now:** Google Play (`../android/PLAN.md` item 9,
|
|
M8) needs a public HTTPS site with working sign-ins for its reviewers and for the 12 testers who
|
|
must be opted in for 14 days before the app can apply for production.
|
|
|
|
## 1. What the demo is
|
|
|
|
A public Runic Gateway deployment at `demo.runicgateway.com`, linked to the **Oxide Rust rig**,
|
|
that anyone can sign into with published accounts. The org lead set three requirements on
|
|
2026-10-23:
|
|
|
|
1. **Static logins.** Ready-made accounts (player, moderator, admin, and one for Google's
|
|
reviewer) whose names and passwords are published.
|
|
2. **No destructive actions.** Someone signed in as the public admin must not be able to break the
|
|
site for the next visitor, or reach through it to damage the rig.
|
|
3. **An automatic wipe** every couple of hours that brings the site back to a known base: the Rust
|
|
module, the link to the rig, the demo accounts and the seeded content.
|
|
|
|
The `demo-site` Gitea Actions runner is where the demo is deployed and wiped. The runner is only the
|
|
host for those jobs; the design below covers what runs there.
|
|
|
|
## 2. Principle: the same image, switched by one flag
|
|
|
|
The demo runs the normal `website-app` image and the normal Rust module release. Nothing is forked.
|
|
A single env var on the deployment, **`DEMO_MODE=true`**, turns on three things:
|
|
|
|
- a **lock** on a fixed list of write routes (§3), answered before the route's own handler runs;
|
|
- a **banner** on every page and in the app saying this is a demo and when it next resets (§6);
|
|
- **no outbound side effects** that reach a real person (§5).
|
|
|
|
With the flag unset, none of the demo code runs, so a normal shard is unaffected.
|
|
|
|
## 3. The lock
|
|
|
|
### 3.1 How it works
|
|
|
|
A core middleware, mounted ahead of every router when `DEMO_MODE` is on, compares each write
|
|
request (`POST`/`PUT`/`PATCH`/`DELETE`) against a deny list of method + path patterns. A match
|
|
answers:
|
|
|
|
```
|
|
403 { "message": "This is turned off on the demo site.", "code": "DEMO_LOCKED" }
|
|
```
|
|
|
|
The web client shows the message as a toast wherever an action fails with that code. The Android
|
|
app does the same with a snackbar (DM19): an interceptor on its one HTTP client spots the code on
|
|
any `403` and reports the server's sentence, so a module's own wording (a refused console command)
|
|
reaches the screen as written. The screen still shows its own failure line, except where that line
|
|
blames the role ("You don't have permission for that action", on the admin screens), which says the
|
|
demo sentence instead. Load screens get a `DEMO_LOCKED` error kind, with no Retry button.
|
|
|
|
Reads are never locked, with one exception: the bridge's config (§3.5). Every page can be opened
|
|
and every form filled in; only the save is refused. That keeps the demo useful as a tour of the admin panel.
|
|
|
|
**The server is the lock, never the UI (DM10).** Every lock is enforced at the route, so a request
|
|
sent with `curl`, an old app build, or a hand-edited page is refused exactly like a click. The UI may
|
|
explain a lock (the toast, the banner) but never hides or disables a control as the means of
|
|
locking it, and no client-side check is relied on.
|
|
|
|
Module routes are mounted by core but belong to the module, so the module declares its own locked
|
|
routes in `module.json` (DM1, §3.2).
|
|
|
|
### 3.2 The module's declaration
|
|
|
|
`module.json` gains an optional `demoLocks` list, read by core when `DEMO_MODE` is on:
|
|
|
|
```json
|
|
"demoLocks": [
|
|
{ "method": "PUT", "path": "/admin/rust/servers/:id" },
|
|
{ "method": "DELETE", "path": "/admin/rust/servers/:id" },
|
|
{ "method": "DELETE", "path": "/player/rust/links/:steamId" }
|
|
]
|
|
```
|
|
|
|
Paths use the same `:param` syntax as the module's routers, with an optional trailing `/*` for a
|
|
path and everything under it. They must sit under the module's own mounts, which the loader checks
|
|
the same way it checks `mounts`. Only writes can be declared. The config editor's one locked read
|
|
(§3.5) depends on which file is asked for, so the module enforces it behind `ctx.demoMode` rather
|
|
than by route. A module without the field is unaffected. The field is validated on every boot, so a
|
|
wrong declaration fails on a developer's machine and not only on the demo. It is a new manifest field,
|
|
so `MODULE_API_VERSION` gets a **minor** bump to **1.18.0**, `MODULE_API.md` documents it, and the
|
|
Integration Kit's module chapter gains a paragraph (its `ci/core-ref.json` pin then moves, as for any
|
|
API bump).
|
|
|
|
### 3.3 What is locked
|
|
|
|
Paths are relative to `/api/v1`. **Tier A** would break the site for the next visitor and is always
|
|
locked. **Uploads** are locked hard. **Tier B** (the rig) and **Tier C** (content) stay **open**: the
|
|
wipe puts them back (§7).
|
|
|
|
**Tier A — always locked (core)**
|
|
|
|
| Area | Routes | What a visitor could otherwise do |
|
|
| --- | --- | --- |
|
|
| Site mode | `PUT /admin/site-mode` | Put the site into maintenance; every non-admin is locked out |
|
|
| Modules | every write under `/admin/modules` (install, sources, restart, enable, disable, purge, delete) | Disable or purge the Rust module; restart the server |
|
|
| SSO providers | writes under `/admin/auth/providers` | Add a sign-in provider pointing anywhere |
|
|
| Email | `PUT /admin/email/config`, `POST /admin/email/test`, `POST /admin/email/disconnect` | Connect a mail account; send mail from the site |
|
|
| Discord bot | `PUT /admin/discord-bot/config` | Point the bot at someone's server |
|
|
| Team voice | writes under `/admin/teams/voice` | Configure or sync Discord voice channels |
|
|
| Demo accounts, by an admin | `PUT`/`DELETE /admin/users/:id`, `DELETE …/trusted-devices…`, `POST …/mfa/reset` **when `:id` is a demo account** | Demote, disable or delete the account everyone signs in with |
|
|
| Own account, on a demo account | `PATCH /auth/me/account/username`, `…/password`, `…/email`; `POST /auth/me/account/totp/setup`, `…/enable`, `…/disable`; `POST /auth/me/account/recovery-codes/generate`; `DELETE /auth/me/account/identities/:provider` | Change the shared password, or turn on 2FA so nobody else can sign in |
|
|
| Other people's sessions | `DELETE /auth/me/sessions/:id` **on a demo account**, whichever session it names (DM17) | Sign every other visitor out of the shared account |
|
|
| Password reset | `POST /auth/password/forgot`, `POST /auth/password/reset/:token` | Reset a demo account's password |
|
|
| Registration | `POST /auth/register` (DM16) | Settings are open, so a visitor could switch `player_registration` on. The route is what keeps registration closed (§4) |
|
|
| Settings that reach a lock | `PUT /admin/settings` when the body names `site_mode*`, `module_source_hosts`, `teams_voice_*`, `demo_*` or `hero_layout` | That route writes arbitrary keys, so these would get around the site-mode, Modules and team-voice rows above, rewrite the banner and the published logins, or publish the homepage hero (DM21). The answer names the keys (`keys: [...]`) |
|
|
|
|
**Why DM17 locks the whole route.** A mobile access token carries no id for its refresh-token row,
|
|
so the server cannot tell which row in `GET /auth/me/sessions` is the caller's own. Signing out is
|
|
`POST /auth/mobile/logout`, which revokes the caller's own row and stays open.
|
|
|
|
**Tier A — always locked (Rust module)**
|
|
|
|
| Area | Routes | Why |
|
|
| --- | --- | --- |
|
|
| The link to the rig | `PUT`/`DELETE /admin/rust/servers/:id` | Repoint or delete the sidecar link; the site goes dark |
|
|
| The bridge's config, **read or written** | `GET`/`POST /admin/rust/config/:serverId/file`, `GET …/writes…` for the `RunicGateway` plugin's files, which are also left out of `GET …/files` | It holds the bridge's link to the sidecar and its credentials. Read, it would let someone talk to the sidecar directly over HTTP (DM14, §3.5) |
|
|
| Removing a Steam link (DM27) | `DELETE /player/rust/links/:steamId` (`demoLocks`), and staff's `DELETE /admin/users/:id/rust/links/:steamId`, which the module's handler refuses behind `ctx.demoMode` because it serves under core's users mount, where a module may not declare a lock | `demo-player`'s stand-in link is what its Rust pages, rewards and permission grants hang on; unlinked, the player tour is empty until the wipe |
|
|
|
|
**Settings stay open (DM2).** Theme, navigation and the other `PUT /admin/settings` values can be
|
|
changed and come back at the wipe, except the keys in the "Settings that reach a lock" row above.
|
|
Brand images are uploads, so they fall under the upload lock.
|
|
|
|
**The hero editor previews but does not publish (DM21).** Admin → Hero autosaves to
|
|
`hero_layout_draft`, and its Preview opens the homepage with that draft, so both keep working. Its
|
|
Publish writes `hero_layout`, the hero every visitor sees, and is refused; the editor's status line
|
|
and the toast give the demo sentence. Its image uploads fall under the upload lock. The demo admins
|
|
share one login, so they share one draft, and the wipe resets it.
|
|
|
|
**Which accounts are demo accounts.** The four usernames in §4 are fixed in core
|
|
(`server/src/demo/mode.js`), not read from the seed fixture: the lock checks them on every request,
|
|
and a fixture that dropped one would unlock the account everybody shares. The lock decides by the
|
|
account row in the database (`users.getById`), not by the username claim in the token. An account an
|
|
admin creates on the demo (`POST /admin/users` stays open) is an ordinary account. It is gone at the
|
|
next wipe.
|
|
|
|
**Failing closed.** A conditional row that cannot decide, because the database is down, refuses.
|
|
|
|
**Uploads — locked hard (DM4)**
|
|
|
|
| Route | What it stores |
|
|
| --- | --- |
|
|
| `POST /admin/uploads` | The media library |
|
|
| `POST /admin/posts/upload` | Images in posts |
|
|
| `POST /admin/settings/brand-asset/:slot` | Logo, hero, favicon |
|
|
| `POST /player/teams/:slug/forum/uploads` | Images in team forum posts |
|
|
|
|
"Hard" means a second rule beside the list: with `DEMO_MODE` on, core refuses **any** request whose
|
|
body is `multipart/form-data`, before a router sees it. An upload route added later is locked
|
|
without anyone remembering to list it, and a module cannot open one.
|
|
|
|
**A pasted image is an upload too (DM25).** An image pasted into rich text (a post, a wiki page, an
|
|
event description, a forum post, the hero draft) does not use multipart. It travels inside the
|
|
JSON body as a `data:` URI. So with `DEMO_MODE` on, core also refuses **any write**
|
|
(`POST`/`PUT`/`PATCH`/`DELETE`) whose JSON body carries a `data:` URI in **any string**, on any
|
|
route, core's or a module's, before a router sees it. As with multipart, nothing is listed, so a
|
|
rich-text field added later is covered without anyone remembering it. Every `data:` URI counts,
|
|
not only `data:image/*`. A match is `data:` that is not the end of a longer word (so "metadata:"
|
|
does not match), followed by a comma, a `;`, or a `type/subtype` and then a comma. So prose such
|
|
as "Data: none, yet" saves. The string is first read as a browser reads an attribute: HTML
|
|
character references are decoded and tabs and newlines removed, so `data:` and a `data:`
|
|
split by a newline are still refused. Reads are never checked. (Off the demo, the server's HTML
|
|
sanitizer already drops `data:` from `<img>` on news, wiki and forum bodies, but not on every
|
|
rich-text field. The demo rule is the one rule that covers them all.)
|
|
|
|
**Tier B — the rig: open, reset by the wipe (DM3).** Permission writes (policy, grant, revoke,
|
|
groups, members, drift actions, sync), RunicNPC writes (factions, profiles, placements, push,
|
|
respawn), economics and permission grants to a linked player, the world actions events run, and map
|
|
fetch/render all work. The rig starts every cycle from a **demo default set** of permissions, NPCs
|
|
and events, and the wipe restores it (§7.1). Console-command steps stay open too, except for the
|
|
commands on a deny list (§3.4, DM11).
|
|
|
|
**Tier C — content: open (DM4).** Posts, wiki, pages, events, teams and team forums, moderation
|
|
actions and invites work. The contact form is locked (`POST /public/contact`), because it sends mail
|
|
to a person. The demo is served with `X-Robots-Tag: noindex` and a matching `robots.txt`, so nothing
|
|
a visitor writes reaches a search engine.
|
|
|
|
### 3.4 The console-command deny list (DM11)
|
|
|
|
Console-command event steps and command rewards are a free-text box with no allowlist (Rust §16).
|
|
On the demo, the Rust module refuses a command on this **deny list**, which holds the commands that
|
|
would break the rig for every visitor until the next wipe. Everything else runs.
|
|
|
|
| Command (any of these spellings) | Why it is denied |
|
|
| --- | --- |
|
|
| `quit`, `restart` | Stops or restarts RustDedicated. The rig stays down, or the site loses it for minutes, until the wipe |
|
|
| `oxide.unload`, `oxide.reload` (with any argument, `*` included) | Unloads `RunicGateway`, which cuts the site off from the rig, or `RunicNPC`, `Kits`, `ZoneManager` and the rest, which the bridge needs |
|
|
| `rcon.*` | The panel's console runs over RCON, and the wipe's baseline capture uses that console |
|
|
| `fps.limit`, `server.tickrate` | Starves the server tick; the rig crawls for every visitor |
|
|
|
|
**Matching.** The check normalises the command before comparing: it trims it and lower-cases it,
|
|
strips a leading `global.` (Rust accepts `global.quit`), and maps the `o.` short form to `oxide.`
|
|
(`o.unload`). It compares the **first word** only, so the arguments cannot hide a denied command.
|
|
`;` does not split console commands in Rust (the §16 spike), so there is no second command to look
|
|
for.
|
|
|
|
**Where it is enforced (DM10).** In the Rust module's server code, at two points:
|
|
- when an event or reward holding the command is **saved**, answering `403 DEMO_LOCKED` with the
|
|
denied command named;
|
|
- when the step **runs**, so an event saved before the list changed is still refused.
|
|
|
|
Saving an event is core's route, and core never asked a module about a step's params when it saved.
|
|
So the save-time check is a core hook (DM18, `MODULE_API` **1.19.0**): an event action may declare
|
|
`demoRefuses(params)`, which returns a sentence or `null`. With `DEMO_MODE` on, core asks it for every
|
|
step when a definition is created, saved or published, and the first sentence refuses the write
|
|
`403 DEMO_LOCKED` with that sentence as the message. A hook that throws refuses too. The run-time check
|
|
reads a new read-only `ctx.demoMode` boolean, part of the same `MODULE_API` minor bump as `demoLocks`
|
|
(§3.2, 1.18.0). The list lives in the module with tests, and a command added later is a module change.
|
|
|
|
Commands that only change the world or the players (`server.writecfg`, `entity.*`, `env.*`,
|
|
`weather.*`, `say`, `kick`, `ban`, `oxide.grant`) stay open. The baseline restore puts all of them
|
|
back (§7.2).
|
|
|
|
### 3.5 The plugin config editor (DM14)
|
|
|
|
Every plugin's config **except the bridge's** can be read and changed on the demo. A visitor can tune
|
|
RunicNPC, Kits, Economics and the rest and watch the change take effect. Three things make that safe:
|
|
|
|
- **The save reloads through the bridge, not the console.** The bridge backs up the file, writes
|
|
it, reloads the plugin that owns it, and puts the old file back if that plugin fails to load (D178,
|
|
D179). The §3.4 deny list never sees this reload, and a config that breaks its plugin undoes
|
|
itself at once.
|
|
- **The wipe restores `oxide/config/`** from the baseline (§7.2), so a change lasts two hours at
|
|
most.
|
|
- **The bridge's own config is out of reach.** The editor's routes refuse every file the
|
|
`RunicGateway` plugin owns, for reading as well as writing, and the file list leaves those files
|
|
out. This is the one place where §3.1's "reads are never locked" gives way. The file holds the
|
|
bridge's link to the sidecar and its credentials, and anyone holding them could talk to the
|
|
sidecar directly over HTTP, outside every lock in this document.
|
|
|
|
**Secrets in other plugins' configs.** A plugin config can hold a webhook URL or an API key. The
|
|
editor already flags those fields by key name (`SECRET_WORDS` in `configEdit.js`), but today it
|
|
returns the full text and masks them only **in the form**, behind a reveal button. Under DM10 that
|
|
mask is not a lock. On the demo, the server therefore:
|
|
|
|
- **redacts** every flagged value in `GET …/file` and in the write history before it leaves the
|
|
module, replacing it with a placeholder;
|
|
- makes flagged fields **read-only**: a form save that edits one is refused;
|
|
- refuses **raw-JSON saves** (`text` instead of `edits`), because the raw document a visitor sends
|
|
back would hold the placeholders and overwrite the real values. Form saves splice only the fields
|
|
that changed, so untouched secrets stay as they are on disk.
|
|
|
|
**The link on the Servers screen.** The module's admin view of a server never returns the sidecar
|
|
token; it returns `hasToken`. On the demo it also leaves out the sidecar's **URL**, so the screen
|
|
gives away nothing that helps someone reach the sidecar. Editing the link stays locked (§3.3).
|
|
|
|
All of this is enforced in the module's server code behind `ctx.demoMode` (DM10), with tests.
|
|
|
|
## 4. Static logins
|
|
|
|
| Account | Role | Notes |
|
|
| --- | --- | --- |
|
|
| `demo-admin` | admin | The admin panel, read in full; writes per §3 |
|
|
| `demo-moderator` | moderator | Moderation queue, appeals |
|
|
| `demo-player` | player | Self-service; linked to a stand-in Steam ID that has played on the rig, so the Rust player screens have data (DM6) |
|
|
| `play-review` | admin | Google's reviewer. Same powers as `demo-admin`, but a separate account, so a reviewer is never signed out because someone else revoked a session on the shared one |
|
|
|
|
Usernames and passwords are published on the demo's sign-in page and on runicgateway.com `/beta`.
|
|
They are not secrets. The seed writes them into `demo_state`, `GET /public/settings` serves them as
|
|
`demo.logins`, and both sign-in pages (`/account/login`, `/admin/login`) list them with a **Use**
|
|
button that fills the form. A fixture account with `"published": false` is created but not listed. The seed recreates them at every wipe, so a password that somehow changed is
|
|
back within one cycle. Public registration is **closed** on the demo: there is no mail to verify an
|
|
address with (§5), and the static logins cover every role.
|
|
|
|
## 5. Outbound effects
|
|
|
|
| Channel | On the demo |
|
|
| --- | --- |
|
|
| Email | Not configured. Registration closed, password reset locked, digest and engagement mail never send |
|
|
| Push (ntfy) | Not deployed. The app's notification settings still save; nothing is delivered |
|
|
| Discord bot | Not deployed |
|
|
| The rig | Open, and restored to the demo default set at every wipe (§7.1) |
|
|
|
|
## 6. The banner
|
|
|
|
`GET /api/v1/public/settings` gains `demo: { resetsAt, logins }` while the flag is on, and only then.
|
|
The seed writes both into one settings row, `demo_state`, which `PUT /admin/settings` cannot touch
|
|
(§3.3). The web client shows a thin strip on every page: "Demo site — anything you change is wiped at
|
|
14:00 UTC. Sign-ins: demo-admin / demo-player … — passwords on the sign-in page". The app shows the same
|
|
strip on Home, but it does not list the passwords: its strip names the site's sign-in page instead
|
|
("passwords at demo.runicgateway.com/account/login"), DM20. Both read `resetsAt`, so neither needs to know the schedule. The time is said in UTC,
|
|
not converted, so it matches the schedule published on `/beta`.
|
|
|
|
**The toast.** Every web request goes through one request primitive (`client/src/api/client.js`),
|
|
installed modules' requests included. A `DEMO_LOCKED` answer is reported from there to one toast
|
|
(`DemoToast`), and the caller still gets its error as usual. So no screen has to learn the code.
|
|
|
|
## 7. The wipe
|
|
|
|
A scheduled workflow on the `demo-site` runner runs **every 2 hours on the hour, UTC** (DM7). It
|
|
resets the rig first and the site second, and the order matters.
|
|
|
|
### 7.1 Why the rig goes first
|
|
|
|
The site's first sync with a server **imports what it finds there**. A new site's first permission
|
|
read adopts every grant and group on the rig (D198), and RunicNPC profiles are adopted before the
|
|
first push (D244). So wiping only the site would bring back every change a visitor made on the rig,
|
|
now owned by the fresh site. The rig has to be back at the demo default set **before** the new site
|
|
first talks to it. Then that first import *is* the reset: the site adopts the default set, and the
|
|
default set never has to be described twice.
|
|
|
|
**Except for two parts of it (DM26, found on the first restored wipes, 2026-10-27).** Permissions,
|
|
groups and members come back through the import as described. **NPC profiles and factions** do not:
|
|
the restored rig's RunicNPC says it is already managed, by the site before this one, so the new site
|
|
adopts nothing (D244), and its first push sends an empty profile set. **Group chat styles** are
|
|
never adopted by the import. These two come from the fixture instead (§7.3): the seed writes them
|
|
into the site before its first sync, the site pushes them, and the baseline's NPC placements spawn
|
|
again. The baseline still holds the placements, routes and everything else on the rig.
|
|
|
|
### 7.2 The rig: restore a baseline through Pterodactyl (DM3)
|
|
|
|
The demo rig is the existing `rust-oxide` Pterodactyl server (DM5). On it sits one archive,
|
|
`demo-baseline.tar.gz`, holding the demo default set:
|
|
|
|
- `oxide/data/` — permissions and groups, RunicNPC profiles and placements, Economics balances, the
|
|
bridge plugin's own data;
|
|
- `oxide/config/` — plugin configs, so even a config that changed some other way comes back;
|
|
- `server/<identity>/` — the world save and `cfg/`, so anything an event built or a console command
|
|
wrote with `server.writecfg` comes back too.
|
|
|
|
`oxide/plugins/` and the sidecar are **not** in the baseline. Their versions follow the Rust bundle
|
|
(§7.5, DM12), not the snapshot.
|
|
|
|
The workflow uses the Pterodactyl **client** API (files and power):
|
|
|
|
1. `POST …/power {"signal":"kill"}` — no save, since the world is about to be replaced.
|
|
2. `POST …/files/delete` the three directories.
|
|
3. `POST …/files/decompress` `demo-baseline.tar.gz` at the server root.
|
|
4. `POST …/power {"signal":"start"}`. Rust takes a few minutes to boot; the site does not wait.
|
|
|
|
**Capturing the baseline** is a second, hand-run workflow (`workflow_dispatch` only): someone sets up
|
|
the default set through the demo site (the permission groups, a handful of NPCs, events), then runs
|
|
it. It stops the server with `POST …/power {"signal":"stop"}`, which runs the egg's stop command,
|
|
`quit`, and that writes the save, so the subuser needs no console permission (DM22). It then
|
|
`POST …/files/compress`es the three directories, renames the archive to `demo-baseline.tar.gz`, and
|
|
starts the server again. Re-run it whenever the default set should change.
|
|
|
|
The first wipe on a rig with no baseline yet leaves the rig as it is and only starts it, with a
|
|
warning in the run. Set the default set up after that first wipe, then capture.
|
|
|
|
**Checked against the panel (2026-10-26)**, on the offline `egg-oxide` test server: `files/compress`
|
|
returns the archive's name and size; `files/decompress` is synchronous (every entry is back when it
|
|
answers); `files/write` takes a raw binary body byte-exact, so the 8 MB sidecar goes up the same way
|
|
as a plugin file; one `files/chmod` per file.
|
|
|
|
**The key.** Pterodactyl client keys belong to a user and reach every server that user can see. The
|
|
demo gets its own Pterodactyl **subuser**, with access to `rust-oxide` only and the file and power
|
|
permissions only, and that subuser's client key is the `demo-site` workflow's secret
|
|
(`PTERODACTYL_CLIENT_KEY`). The panel URL and server identifier are plain variables
|
|
(`PTERODACTYL_URL`, `PTERODACTYL_SERVER`), defaulting to `http://192.168.0.12` and `6eb9d621`, the
|
|
current `rust-oxide`. **Done:** the org lead created the subuser and added the secret on 2026-10-23.
|
|
|
|
### 7.3 The site
|
|
|
|
1. `docker compose down -v` — drops the database **and** the uploads volume.
|
|
2. `docker compose pull && docker compose up -d` — the app boots on the latest `main` image, creates
|
|
the schema, and installs the Rust module from `MODULES=rust@<version>=<manifest>`, which the
|
|
workflow writes to `state/modules.env` (§7.5). It installs under the allowlist a fresh database
|
|
seeds from `MODULE_SOURCE_HOSTS`, so the demo's `.env` sets that to `gitea.whitlocktech.com`.
|
|
3. Wait for the app's healthcheck (the demo compose file's own: `GET /api/v1/public/status`).
|
|
4. `docker compose exec app npm run --prefix server seed:demo -- /app/deploy/demo/seed.json` (the
|
|
compose file mounts the fixture there; `DEMO_SEED_FILE` also works) — one core
|
|
script (`server/db/seedDemo.js`) that, from a fixture file in the deploy directory:
|
|
- creates the four accounts (§4), or resets them if they exist: password, role, active, 2FA off;
|
|
- sets the site live with registration disabled, plus any `settings` the fixture adds;
|
|
- writes the seeded site content: news posts, wiki pages, pages and the demo events. It does
|
|
this only on the first run against a database, so a re-run never duplicates it. With the
|
|
events it switches on, in the events switchboard (EVENTS.md §K), every action their steps
|
|
use, uncapped. A fresh database starts every `change` and `irreversible` action switched
|
|
off, so without this the seeded events could not run (DM28);
|
|
- hands each installed module its `modules.<id>` section through the module's `onDemoSeed` hook
|
|
(DM15, `MODULE_API` 1.18.0). The Rust module writes its own server row, with the rig's sidecar URL
|
|
and token, and `demo-player`'s stand-in Steam link (DM6), and the default set's NPC faction
|
|
table, NPC profiles and group chat styles (DM26, §7.1). Core never writes a module's tables;
|
|
- stamps `demo_state`: `resetsAt`, the next `DEMO_RESET_EVERY_HOURS` slot (default 2) on the hour
|
|
UTC, and the published logins.
|
|
|
|
The fixture is JSON: `{ accounts, settings?, content?: { posts, wiki, pages, events }, modules? }`.
|
|
The Rust module's section is `{ servers: [{ id, name, sidecarBaseUrl, sidecarToken, protocol?,
|
|
enabled?, sortOrder? }], links: [{ username, steamId, name?, serverId? }], npcs?: { factions?:
|
|
[{ a, b, relation }], profiles?: [{ name, servers?, allServers?, killsScope?, body }] }, chat?:
|
|
[{ serverId, group, style }] }`. A profile's `body` lists only what differs from RunicNPC's defaults,
|
|
and is checked as the admin page checks one. A `style` lists only the BetterChat fields that differ
|
|
from the defaults. Its group is created on the site for that server if the import has not brought
|
|
it in yet; the import then fills in the rig's title, rank and permissions.
|
|
Any string `"$env:NAME"` in it is replaced with that variable from the host's `.env`, so the
|
|
sidecar token is never in the repo. An unset variable fails the run instead of seeding an empty
|
|
credential. The script checks the fixture before writing anything. It must name exactly the four
|
|
accounts, each with a role and a password of 8 or more characters. It refuses to run at all without
|
|
`DEMO_MODE=true`. The demo's `.env` leaves `ADMIN_USERNAME` unset, so the boot seed creates no fifth
|
|
admin.
|
|
|
|
**There is no seeded team.** Core's Teams are synced from the game (a Rust clan becomes a Team), so
|
|
the demo's team comes from a clan in the rig's baseline (§7.2), not from the fixture.
|
|
5. The module's sync connects when the rig finishes booting. Its first import adopts the
|
|
baseline's permissions, and its first NPC push sends the seeded profiles, which the baseline's
|
|
placements spawn from (§7.1).
|
|
6. Smoke check, after a bounded wait: `GET /api/v1/public/status` answers, and the Rust server
|
|
reports a live connection. A rig that has not come back fails the run, and the site still
|
|
serves with the server shown offline.
|
|
|
|
### 7.4 What a visitor sees
|
|
|
|
The site is down for about a minute; the rig for a few. Signed-in visitors, including app users,
|
|
are signed out, because their sessions and refresh tokens were in the database.
|
|
|
|
The same workflow runs on `workflow_dispatch` and on a push to `main` that changes `deploy/demo/`,
|
|
so a redeploy is the same path as a wipe.
|
|
|
|
### 7.5 Versions: the latest Rust bundle (DM9, DM12)
|
|
|
|
"Latest" is decided per piece, so the three halves of the Rust link can never disagree on protocol:
|
|
|
|
| Piece | Follows |
|
|
| --- | --- |
|
|
| Website image | `website-app:latest`, the newest `main` |
|
|
| Rig plugins + sidecar | The Rust bundle's `current.json` (`installer`, branch `bundles`, `v2/rust/`). A bundle is the protocol-checked pair of `Rust-Link` and `Rust-Plugins` that CI already publishes |
|
|
| Rust module | The newest `Module-Rust` release whose `protocol` **equals** that bundle's `protocol` |
|
|
|
|
The Rust game link has no `409`: a plugin on a different protocol would mis-parse, not be refused.
|
|
That is why the module is chosen by protocol, not by date.
|
|
|
|
**What the wipe adds.** Before §7.2's rig restore, the workflow reads `current.json`:
|
|
|
|
1. **The bundle changed since the last wipe.** It downloads the bundle's plugin tarball and sidecar
|
|
binary, checks each `sha256`, and writes them onto the rig through the same Pterodactyl file API
|
|
while the server is stopped. That means the plugin files into `oxide/plugins/`, and the sidecar
|
|
and its `with-sidecar.sh` launcher where the egg expects them (`rust-link/`, each `chmod 755`).
|
|
It records the bundle in `state/bundle.json` on the `demo-site` host, which is what "changed"
|
|
compares against, and in the rig's own `rust-link/bundle.json`, so the rig says what it runs.
|
|
The plugin files are the bridge's tarball and RunicNPC's (`payload` and `npc`); the other plugins
|
|
in `oxide/plugins/` (Kits, ZoneManager and the optional ones) are left alone.
|
|
2. **Always.** It picks the module release as above, and writes `MODULES=rust@<version>=<manifest>`
|
|
to `state/modules.env` in the deploy directory before §7.3. The compose file reads it as a second
|
|
`env_file`, so the workflow never edits the `.env` that holds the secrets.
|
|
|
|
`Module-Rust`'s release CI adds `protocol` (read from `PROTOCOL_VERSION` in
|
|
`server/sidecarClient.js`) to the install manifest it already publishes, so the choice needs no
|
|
guessing. Core's installer reads only the fields it names (`modules/install.js`), so the extra
|
|
field needs no core change.
|
|
|
|
If no module release matches the new bundle yet, the wipe **keeps the previous bundle and module**,
|
|
finishes the reset on them, and warns in the run. A half-released protocol bump never takes the demo
|
|
down.
|
|
|
|
## 8. Where it lives
|
|
|
|
| Piece | Repo |
|
|
| --- | --- |
|
|
| `DEMO_MODE` lock, banner field, `seed:demo` script | `website` (core) |
|
|
| The Rust module's `demoLocks`, the console-command deny list (§3.4), the config-editor rules (§3.5), and `protocol` in its install manifest (§7.5) | `Module-Rust` |
|
|
| `DEMO_LOCKED` error kind and the banner in the app | `Android-app` |
|
|
| `deploy/demo/` (compose file, `.env.example`, seed fixture `seed.json`, `rig.mjs` for the rig and the versions, README) and `.gitea/workflows/demo-wipe.yml` + `demo-baseline.yml` | `website` (DM8) |
|
|
| The deploy directory, `/opt/demo.runicgateway.com` (DM23): the copied deploy files, `.env`, `state/` (`bundle.json`, `modules.env`, `plan.json`), `modules/` | The `demo-site` host |
|
|
| `.env` (JWT secret, encryption key, `BOT_INTERNAL_KEY`, database passwords, the four demo passwords, the stand-in Steam id, the rig's sidecar URL and token) | On the `demo-site` host only, never in a repo |
|
|
| `PTERODACTYL_CLIENT_KEY` (the demo subuser's key) | A Gitea Actions secret on `website` |
|
|
| `demo-baseline.tar.gz` | On the rig, at the Pterodactyl server root |
|
|
|
|
## 9. Decisions
|
|
|
|
### 9.1 Decisions (org lead, 2026-10-23)
|
|
|
|
| # | Question | Decision |
|
|
| --- | --- | --- |
|
|
| DM1 | How the module declares its locked routes | A `demoLocks` list in `module.json`; `MODULE_API` minor bump (§3.2) |
|
|
| DM2 | Is anything on Tier A to stay open? | Settings stay open (theme, navigation and the rest of `PUT /admin/settings`); the rest of Tier A stays locked (§3.3). Confirmed as "settings only" (DM13) |
|
|
| DM3 | What may a visitor do to the rig? | Permissions, NPCs and events start from a **demo default set**, can be changed through the site, and are put back each cycle by a CI script that restores the rig through Pterodactyl (§7.2) |
|
|
| DM4 | May visitors publish content? | Yes. **Uploads are locked hard** (§3.3); the contact form is locked; the site is `noindex` |
|
|
| DM5 | Which rig? | The existing Oxide rig becomes the demo's. A new Oxide server is spun up for testing when a walk needs one |
|
|
| DM6 | A Steam link for `demo-player`? | Yes, a stand-in Steam ID that has played on the rig |
|
|
| DM7 | Cadence | Every 2 hours |
|
|
| DM8 | Where the files live | `website/deploy/demo/` and `website/.gitea/workflows/` |
|
|
| DM9 | Version | Latest, at every wipe |
|
|
| DM10 | (the org lead, unprompted) | **Every lock is at the server route level, never the UI level** (§3.1) |
|
|
| DM11 | (F1) Console-command steps on the demo | A **deny list** of the commands that would break the rig (`quit`, `oxide.unload RunicGateway` and the like), not an allowlist; everything else runs (§3.4) |
|
|
| DM12 | (F2) What "latest" means across the Rust link | The latest **Rust bundle** for the rig's plugins and sidecar, the newest module release on the same protocol, and the website's `main` image; the wipe updates the rig when the bundle changes (§7.5) |
|
|
| DM13 | (F3) DM2 read correctly? | Yes: only the settings stay open; every other Tier A row stays locked |
|
|
| DM14 | (after DM13) May visitors change plugin configs? | Yes, every plugin except the bridge. The bridge's config can be **neither read nor written**, because it holds credentials that would let someone talk to the sidecar directly (§3.5) |
|
|
| DM15 | (build) How does the seed create rows in a module's tables (the Rust server link, `demo-player`'s Steam link)? | Through a new optional module hook, **`api.onDemoSeed(data, { accounts })`**, in the same `MODULE_API` minor as `demoLocks`. The module writes its own rows with its own model; core never writes a module's tables (§7.3) |
|
|
| DM16 | (build) Settings are open, so `player_registration` can be switched on. Lock registration? | Yes: **`POST /auth/register`** is a Tier A lock. Invites stay open (§3.3) |
|
|
| DM17 | (build) The server cannot tell which mobile session is the caller's own. What does the other-sessions lock do? | **`DELETE /auth/me/sessions/:id` is locked outright on a demo account**; `POST /auth/mobile/logout` stays open (§3.3) |
|
|
| DM18 | (build, step 2) Saving an event is core's route, and core never asks a module about a step's params. How is a denied command refused at save? | **A new core hook:** an event action's optional `demoRefuses(params)`, asked at create, save and publish on the demo, refusing `403 DEMO_LOCKED` (§3.4). `MODULE_API` 1.19.0; Module-Rust then needs `^1.19.0` |
|
|
| DM19 | (build, step 4) Each app screen writes its own failure line, and the admin screens turn every `403` into "You don't have permission for that action". How does the app show a refusal? | **One app-wide snackbar** with the server's sentence, like the web's toast, and the screens whose `403` line blames the role say the demo sentence instead. `ErrorKind.DEMO_LOCKED` for load screens (§3.1) |
|
|
| DM20 | (build, step 4) Should the app's sign-in screen list the logins, as the web's does? | **No, the banner only.** The Home strip names the site's sign-in page for the passwords; Google's reviewer gets `play-review`'s from the Play Console (§6) |
|
|
| DM21 | (amendment, 2026-10-26) Settings are open, so the hero editor's Publish changes the homepage every visitor sees. What may a visitor do in the hero editor? | **Edit and preview, never publish.** `hero_layout` joins the protected settings keys; the draft and Preview stay open; uploads were already locked. Server lock only: Publish stays clickable and answers the demo sentence (§3.3) |
|
|
| DM22 | (build, step 5) The subuser has file and power permissions only, so `server.save` over the console is out of reach. How does the baseline capture save the world? | **A power `stop`**, which runs the egg's `quit` and writes the save; no console permission (§7.2) |
|
|
| DM23 | (build, step 5) Where does the demo live on its host? | **`/opt/demo.runicgateway.com`**, the `demo-site` runner in host mode, like `rgcom`'s `/opt/runicgateway.com`. The org lead writes `.env` from `deploy/demo/.env.example` |
|
|
| DM24 | (build, step 5) The module's demo code was on `edge` only, so no release could be installed. Cut over? | **Yes, now:** `Module-Rust` `edge` → `main` releases v0.6.0, the first with `protocol` in its install manifest |
|
|
| DM25 | (amendment, 2026-10-27) Uploads are locked, but an image pasted into rich text is saved as a `data:` URI inside the JSON body. Block it? | **Yes, on every write and for every `data:` URI.** Core refuses any write whose JSON body has a `data:` URI in any string, on any route, the same way the multipart rule needs no list (§3.3) |
|
|
| DM26 | (build, step 5, 2026-10-27) The restored rig is marked managed, so a fresh site adopts none of its NPC profiles and empties them on its first push; chat styles are never adopted. How does the default set bring them back? | **Seed them.** Module-Rust's `onDemoSeed` also takes `npcs` (factions, profiles) and `chat` (a style per group), written before the site's first sync (§7.1, §7.3). Patch release v0.6.1 |
|
|
| DM27 | (step-5 walk, W4, 2026-10-28) `demo-player` can remove its stand-in Steam link, and staff can remove it from Admin → Users. Lock it, or leave it to the wipe? | **Lock it, both ways.** The player's route through `demoLocks`; the staff route in its handler, because it serves under core's users mount (§3.3). Module-Rust patch release |
|
|
| DM28 | (step-5 walk, W1, 2026-10-28) The seeded events' actions start switched off on a fresh database, so they fail when run. Fix? | **Yes.** `seed:demo` switches on every action a seeded event uses, uncapped, with the events (§7.3). Core only, no `MODULE_API` change |
|
|
| DM29 | (step-5 walk, W2, 2026-10-28) `SECRET_WORDS` also hides harmless fields such as "Allow Master Key" and "TC Auth Only". Narrow it? | **No, leave it as is** |
|
|
| DM30 | (step 6, 2026-10-28) How does runicgateway.com `/beta` publish the logins? | **Live from the demo.** `/beta` reads this site's `GET /public/settings` `demo.logins` on its server, caches it for a minute, and links to `/account/login` when the demo does not answer (runicgateway.com D82) |
|
|
| DM31 | (step 6) runicgateway.com's "See it running" links were UO routes, which 404 here. What do they do? | **Per game.** A deep link names its game, and that site's mounted `demoGames` (`["rust"]` for this demo) says which games' links show. Rust's point at `/rust` and `/rust/servers/demo?tab=…`, so that site now quotes the fixture's server id `demo` (runicgateway.com D83) |
|
|
| DM32 | (2026-10-29) The moderator tour shows empty queues, and other surfaces are empty too (§11). Which get seeded? | **All four of §11's groups:** moderation history, Teams with their forum and reports, event history with the calendar, and Rust player stats |
|
|
| DM33-DM35 | (2026-10-29) Which Steam IDs, Discord IDs and names do the invented players use? | **Visibly fake IDs** starting `1234567890` (plus the org lead's own Steam ID), and **Rust-themed invented names** (§11.3) |
|
|
|
|
DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is
|
|
setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for
|
|
`X-Forwarded-*` by default (`TRUST_PROXY`), and the deploy PR checks secure cookies and client IPs
|
|
through that proxy.
|
|
|
|
### 9.2 Set up by the org lead
|
|
|
|
- **Done 2026-10-23:** the Pterodactyl subuser (access to `rust-oxide` only, file and power
|
|
permissions) and its client key as the `PTERODACTYL_CLIENT_KEY` Actions secret (§7.2).
|
|
- **In progress:** DNS and HTTPS through the external reverse proxy.
|
|
- **To confirm in the deploy phase:** the `demo-site` host reaches the Pterodactyl panel and the rig's
|
|
sidecar over the network.
|
|
|
|
## 10. Build order
|
|
|
|
1. **Core** (`website`): the `DEMO_MODE` lock (the deny list, the `multipart/form-data` rule, the
|
|
demo-account and other-session checks), `demoLocks` loading, `ctx.demoMode`, the banner field,
|
|
`seed:demo`, `noindex`, the web banner and toast, the `MODULE_API` minor bump, and the OpenAPI
|
|
note that a route may answer `DEMO_LOCKED`. Tests for every lock. One PR, plus `MODULE_API.md`
|
|
and this doc. **Built 2026-10-23** as `MODULE_API` 1.18.0, with `onDemoSeed` (DM15), the
|
|
registration lock (DM16), the protected settings keys (§3.3) and the published logins on the
|
|
sign-in pages (§4). The code is in `server/src/demo/` (`mode.js`, `paths.js`, `locks.js`),
|
|
`server/db/seedDemo.js`, and the client's `DemoBanner`, `DemoToast` and `DemoLogins`.
|
|
`test/demoLocks.test.js` holds every core row against `routes.manifest.json`, so a row that stops
|
|
naming a real route fails the build.
|
|
2. **Module** (`Module-Rust`): `demoLocks` in `module.json`; behind `ctx.demoMode`, the console-command
|
|
deny list (§3.4) and the config-editor rules (§3.5); and `protocol` in the release's install
|
|
manifest (§7.5). Each with tests. One PR. The save-time half of the deny list needed a core hook
|
|
first (DM18): `demoRefuses` in `website`, `MODULE_API` 1.19.0, its own PR ahead of the module's.
|
|
**Built 2026-10-26:** core `website#224`, module `Module-Rust#62` (`coreApi` `^1.19.0`). The
|
|
module's rules are in `server/demo.js`, and its seed hook is `server/demoSeed.js`, which reads
|
|
`modules.rust.servers` and `modules.rust.links`. A bridge path is recognised after
|
|
canonicalising it (dropping `.` segments, trimming, lower-casing), so `./RunicGateway.json`
|
|
counts. The plugin name `RunicGateway` is a floor beside the catalogue's `self`, so a catalogue
|
|
that cannot be read never unlocks the file. A config that does not parse is withheld whole on the
|
|
demo, because its secrets cannot be found to redact.
|
|
3. **Kit** (`Integration-kit`): a paragraph on `demoLocks` and `ctx.demoMode`, and the
|
|
`core-ref.json` re-pin. One PR. **Built 2026-10-26** as `Integration-kit#17`, pinned to the
|
|
sha `website#224` merged as (1.19.0). Chapter 2 gains a section on a public demo site, which
|
|
also covers `onDemoSeed` (DM15), and chapter 5 covers `demoRefuses` (DM18). Both hooks were
|
|
decided after this list was written. Module-Rust's own pin moved to the same sha in
|
|
`Module-Rust#63`.
|
|
4. **App** (`Android-app`): the `DEMO_LOCKED` error kind and the banner on Home. One PR. **Built
|
|
2026-10-26** as `Android-app#61`, into `edge`, with the snackbar (DM19) and the banner only (DM20).
|
|
The refusal is read in `core/result/DemoLocked.kt` and spotted by `core/net/DemoLockInterceptor`;
|
|
the strip is `ui/demo/DemoBanner`. Checked on an emulator against `website` `main` with
|
|
`DEMO_MODE=true` and `seed:demo`: the strip, a shared login's username change refused, and the
|
|
admin site-mode switch refused with the demo sentence in place of the permission line.
|
|
5. **Deployment** (`website`): `deploy/demo/`, `demo-wipe.yml` (with §7.5's bundle step) and
|
|
`demo-baseline.yml`; the first deploy on `demo-site`; setting up the default set and capturing
|
|
the baseline. Then a walk of every static login, every Tier A lock and the upload lock, in the
|
|
browser and the app, plus one full wipe cycle that restores a changed permission, NPC and
|
|
event; a denied console command refused at save and at run; and a plugin config changed and
|
|
reloaded through the editor, with the bridge's config refused for reading and another plugin's
|
|
secret field redacted; and the hero editor's draft previewed while its Publish is refused
|
|
(DM21). One PR. **Files built 2026-10-26** (DM22-DM24): `deploy/demo/` and the two workflows.
|
|
The site half (compose, `down -v`/`up`, healthcheck, `seed:demo` with this fixture and the Rust
|
|
module at 0.6.0) ran locally against an image built from `main`; `rig.mjs plan` and `smoke` ran
|
|
against the real Gitea; the Pterodactyl file calls ran against `egg-oxide` (§7.2). The first
|
|
deploy, the baseline and the walk wait on the host's `.env` and `Module-Rust` v0.6.0.
|
|
**First wipes 2026-10-27:** the host's `.env` was written and v0.6.0 released. Wipe 358's rig
|
|
half worked (bundle 2026.10.22 written, rig restarted), but the seed loaded no module: the
|
|
runner is not root, so its `chown` of `modules/` and `logs/` failed silently. The wipe now does
|
|
the chown in a root container. The same day, DM25 added the `data:` URI rule (§3.3) to the walk's
|
|
upload checks. Wipe 362 was the first green one. **The default set** was then built through the
|
|
site as `demo-admin`. It holds `vip` and a new `moderator` group with chat titles, `demo-player` in
|
|
`vip`, bandit and guard NPC profiles with an allied faction pair, and `guard-1` at the bandit camp.
|
|
It was captured as the baseline (run 366, 499 MB with the world save). The first three restored
|
|
wipes (367-369) brought back the groups, permissions and members, but not the NPC profiles or the
|
|
chat titles (DM26). **Public URL:** `demo.runicgateway.com` is served through Pangolin. Cloudflare's
|
|
proxy in front of it made the app log Cloudflare's addresses as the visitors' IPs, under the
|
|
default `TRUST_PROXY=1`. Cloudflare was set to DNS only, and the app now logs the real visitor.
|
|
DM26 then seeded the NPC profiles and chat titles, and restored wipes 378 and 381 brought the
|
|
whole default set back.
|
|
|
|
**The walk (2026-10-28)**, on `website` `main`, `Module-Rust` v0.6.1 and Rust bundle 2026.10.22,
|
|
with the rig on Rust build 26139947 (the public branch that day):
|
|
- **Every lock, through the API:** all 87 checks passed. They cover each Tier A row for all three
|
|
published logins, the case and trailing-slash forms, the four upload routes, multipart sent to
|
|
an unlisted core route and to a module route, the `data:` URI forms (plain, `data:`, split
|
|
by a newline, in the hero draft, in a module write), both server-link locks, the bridge's config
|
|
under four spellings for reading and writing, the file list and write history without the
|
|
bridge, the Servers view without the sidecar URL, a redacted field, the raw-JSON save, a form
|
|
edit of a flagged field, and `noindex`. Prose containing `Data:` still saves.
|
|
- **The deny list at save:** 21 checks. Every §3.4 command was refused in a run step, under
|
|
each spelling (`global.`, `o.`, upper case, padding), in a teardown command, in a reward step
|
|
and in an edit of an existing event. The open commands (`say`, `server.writecfg`, `env.*`,
|
|
`weather.*`, `oxide.grant`, `kick`) all saved.
|
|
- **The deny list at run:** this half cannot be reached on the live demo, because every save path
|
|
asks first. It was run on a local core instead, on the v0.6.1 release tarball: an event holding
|
|
`quit` was saved and published with `DEMO_MODE` off, the core was restarted with it on, and the
|
|
run's step failed with the deny sentence before any command was sent.
|
|
- **In the browser**, as `demo-player`, `demo-moderator` and `demo-admin`: the strip, the logins
|
|
and **Use** buttons, the toast and the inline line on a refused username change, the moderator's
|
|
queues, and the hero editor saving and previewing its draft while Publish is refused. The config
|
|
editor left the bridge out and showed a redacted field read-only. A config change to AutoDoors
|
|
saved, reloaded the plugin on the rig and appeared in the history. The browser half ran
|
|
against the host's LAN address, because Chrome on the walking machine would not open the
|
|
public name, although `curl` and the emulator did.
|
|
- **In the app** (`Android-app` `edge`, debug build, public HTTPS address): the Home strip, sign-in
|
|
as `demo-admin`, and the maintenance switch refused with the inline line and the snackbar.
|
|
- **One wipe cycle:** before the 08:00 UTC wipe the walk changed AutoDoors' config, took a
|
|
permission out of `vip` and retitled it, deleted the `guard-1` placement, edited the greeting
|
|
event, added to the hero draft and enabled an event action. After the wipe all of them were
|
|
back as the default set has them, with all four NPCs alive. The site reseeded at 08:07 and the
|
|
rig was fully back at 08:17.
|
|
|
|
**Found by the walk** (the org lead's answers, 2026-10-28):
|
|
- **The seeded events could not run.** A fresh database has every event action switched off, so
|
|
Server greeting, Treasure drop and Payday showed as ready but failed when started ("not
|
|
enabled on this deployment"). **Fixed (DM28):** the seed switches on the actions they use.
|
|
- **The credential check over-flags.** `SECRET_WORDS` matches words such as `key` and `auth`
|
|
inside longer names ("Allow Master Key", "TC Auth Only", a drone preset key), so on the demo
|
|
those harmless fields are hidden and read-only. **Left as is (DM29).**
|
|
- **`play-review` was not walked**: its password is only in the host's `.env`. The org lead read
|
|
it there and handed it over, and it was walked with the re-check below.
|
|
- `demo-player` could remove its stand-in Steam link, and staff could remove it for them.
|
|
**Locked (DM27).**
|
|
- The public URL did not open in Chrome on the walker's PC, while curl and the emulator reached
|
|
it; the org lead opened it through the proxy without trouble, so it is local to that PC.
|
|
- Nothing seeds the moderation queues, so the moderator tour shows empty lists.
|
|
|
|
**Re-checked after the fixes** (2026-10-28, the 12:00 UTC wipe, Module-Rust **v0.6.3**, whose run
|
|
tagged a v0.6.2 first and then failed to publish it):
|
|
- **`play-review`, 19/19.** It is not among the published logins; it signs in as admin and reads
|
|
settings, users, servers and events; its own username, password, email, two-factor, recovery
|
|
codes and sessions are refused, as are the site's admin locks. The app's path works too: mobile
|
|
sign-in, a bearer read, a refused lock and sign-out. Its web sign-in was checked through the
|
|
API, which is the call the page makes.
|
|
- **Seeded events, W1.** The three actions are on, uncapped, straight after the wipe, and
|
|
Server greeting ran to completion.
|
|
- **Steam link, W4.** `demo-player`'s unlink and staff's unlink both answer `403 DEMO_LOCKED`, and
|
|
the link stays.
|
|
6. **Play**: the sign-in details in the Play Console point at `play-review`;
|
|
runicgateway.com `/beta` publishes the logins. **Site side built 2026-10-28** in
|
|
`runicgateway.com` (DM30, DM31). `/beta` lists the published logins and the next wipe as this site
|
|
serves them, marks its "somewhere to point it" gate in place, and names the host to type into the
|
|
app. The homepage, `/features/` and `/modules/rust/` link into the demo, and UO's links stay
|
|
hidden. Against the live demo: three logins listed (`play-review` not among them), the 22:00 UTC
|
|
wipe shown, seven Rust links and three core links filled, and `/uo/guilds` left empty. **It goes
|
|
live** when that site's mounted `brand.json` gains `"demoUrl": "https://demo.runicgateway.com"` and
|
|
`"demoGames": ["rust"]` and its container restarts.
|
|
**Play Console:** the *Sign in details* declaration (formerly *App access*) is filled in but not
|
|
saved: *Yes, restricted*, the name *Admin account (demo site)*, the username `play-review`, and
|
|
instructions (enter `demo.runicgateway.com` on the first screen, the two-hour reset, the refused
|
|
locks). The org lead types the password, ticks the full-access attestation and saves. The closed
|
|
track does not exist yet (Test and release shows *Get started* for closed and internal testing), so
|
|
there is no opt-in URL for `betaOptInUrl`, and `/beta`'s first gate stays shut.
|
|
7. **Demo content** (DM32, §11): the seed's two passes and the rig's clans. Planned 2026-10-29.
|
|
One PR in `website` (core seed, the wipe's second pass), one in `Module-Rust` (`onDemoSeed`
|
|
stats and feed lines, into `edge`), a new baseline, and a walk of every surface §11 names after
|
|
the next wipe.
|
|
|
|
## 11. Demo content (DM32)
|
|
|
|
**Why.** A read-only count of the live demo on 2026-10-29 found these surfaces empty after a wipe:
|
|
every moderation queue (recent actions, filter hits, spam hits, member events, appeals, reports),
|
|
Teams (public, the player's own, staff's review queue), event runs and history, the public calendar
|
|
(the three seeded events are never scheduled), `demo-player`'s inbox, and on the Rust side the
|
|
leaderboard, wipes, clans and the events feed. Only the four demo accounts exist. A reviewer touring
|
|
as `demo-moderator` finds nothing to moderate.
|
|
|
|
### 11.1 Two seed passes
|
|
|
|
Some of it can be written the moment the site is up. The rest depends on the rig: Teams are
|
|
**module-authoritative** and re-synced every 15 minutes (`TEAMS.md`), so a Team written straight
|
|
into core would be archived by the next sync, and the Rust stats need the wipe the rig is on. So:
|
|
|
|
- **Pass 1, `seed:demo`, as today**: accounts, settings, content, events, and the new core rows
|
|
below that need nothing from the rig.
|
|
- **Pass 2, `seed:demo --after-rig`**, a new step in `demo-wipe.yml` after the rig reports in and
|
|
the first Teams sync has run: Team forum content, reports, Rust stats and feed lines. It waits for
|
|
the Teams it names, with a time limit, and fails the wipe if they never appear, as the rig smoke
|
|
check does.
|
|
|
|
The rig's wipe id comes from the restored world save, so it is the same on every wipe
|
|
(`w-20261005T165054Z` today). Pass 2 reads it from the server state rather than the fixture, so a
|
|
new baseline with a new map still works.
|
|
|
|
### 11.2 What each group seeds
|
|
|
|
- **Moderation history** (pass 1, core). Discord-shaped rows for six invented members: a few
|
|
`mod_actions` (warn, timeout, ban), `warnings`, `filter_hits` and `spam_hits`, join and leave
|
|
`member_events`, staff `mod_notes` (one admin-only), and three `appeals`: one pending to claim,
|
|
one under review, one resolved. Times are relative to the seed, so the 24 h / 7 d / 30 d counts
|
|
are never zero. There is no bot on the demo, so approving an appeal records the reversal as
|
|
failed. That is the shipped behaviour when the bot is down, and the appeal still closes.
|
|
- **Teams, forum and reports.** Two or three clans **in the rig's baseline** (the Clans plugin's
|
|
data, which already reports an empty board), with stand-in members and `demo-player`'s stand-in
|
|
Steam ID in one, so sync brings them in as Teams. One clan's name contains a reserved term, so it
|
|
lands in staff's review queue. Pass 2 then writes forum threads and posts in them, and two open
|
|
reports. Once report, block and hide exist (`USER_SAFETY.md`), it also writes reports on chat
|
|
lines.
|
|
- **Event history and the calendar** (pass 1, core). Two finished runs of the seeded events, with
|
|
their steps, log, participants (stand-ins, and `demo-player` through its link) and published
|
|
results, so the history and results pages fill and `demo-player`'s inbox has the results
|
|
notification. The three seeded events are scheduled in the coming days, so the public calendar
|
|
shows them.
|
|
- **Rust player stats** (pass 2, Module-Rust `onDemoSeed`). About ten stand-in players with wipe
|
|
stats (kills, deaths, NPC kills, playtime, gather totals, weapon kills) on the rig's current
|
|
wipe, an earlier wipe for the Wipes tab, and a day of feed lines (kills, joins, chat), so the
|
|
leaderboard, profiles and feed are populated. The online list stays honest at zero.
|
|
|
|
### 11.3 Identities (DM33-DM35, org lead, 2026-10-29)
|
|
|
|
- **DM33, Steam IDs: visibly fake, plus the org lead's own.** Every real individual Steam ID starts
|
|
`7656119`, so an invented one in that range could belong to a stranger and would appear on public
|
|
leaderboards. The invented players use IDs that are visibly not Steam IDs, which start
|
|
`1234567890`: `12345678900000001`, `…02`, and so on. They are 17 digits because Module-Rust checks
|
|
that length on links, profiles and event steps (`links.model.js`, `eventWorld.js`), and a shorter
|
|
one would break those pages. `demoSeed.js` currently insists on `7656119…` for links and is
|
|
widened for these. The one real Steam ID on the demo is the org lead's own (`76561198038695917`).
|
|
**To confirm:** it replaces `demo-player`'s stand-in link (`76561199910000001`, which could
|
|
belong to anyone).
|
|
- **DM34, Discord IDs: the same rule.** The invented moderation history uses visibly fake ids
|
|
starting `1234567890`, padded to a snowflake's 18 digits. The org lead did not answer this one
|
|
separately; it follows DM33 and is flagged on the PR.
|
|
- **DM35, names: Rust-themed and invented.** Fun gamer tags from the game's world, such as
|
|
"Rusty_Pete", "Scrap Baron", "Naked_On_The_Beach", "Rock Enthusiast", "Big Oil Bandit", "TC
|
|
Thief", "Hazmat Hank" and "Bean Can Dan", and clans such as "The Scrap Collective" and
|
|
"Outpost Loiterers". Never a real person's name, and nothing offensive: the seeded filter hits
|
|
show a placeholder word.
|
|
|