docs(website): demo step 5 built — deploy files, DM22-DM24

DEMO_MODE.md follows website#227:
- §7.2: the baseline is captured with a power stop (quit saves), DM22; a
  first wipe with no baseline only starts the rig; what the panel calls
  were checked to do (synchronous decompress, binary-safe write); the
  server identifier is now 6eb9d621.
- §7.3: MODULES comes from state/modules.env; MODULE_SOURCE_HOSTS seeds a
  fresh database's allowlist; the exact seed:demo command.
- §7.5: where the bundle is recorded (state/bundle.json and the rig's own
  rust-link/bundle.json) and which plugin files the wipe writes.
- §8: the deploy directory and the full .env contents.
- §9.1: DM22 (power stop), DM23 (/opt/demo.runicgateway.com, host mode),
  DM24 (Module-Rust cut over now, v0.6.0).
- §10: step 5's files built and how each half was tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-10-26 12:24:32 -05:00
parent cf6ee29c60
commit 540b66c304

View File

@@ -310,23 +310,36 @@ The workflow uses the Pterodactyl **client** API (files and power):
**Capturing the baseline** is a second, hand-run workflow (`workflow_dispatch` only): someone sets up **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 the default set through the demo site (the permission groups, a handful of NPCs, events), then runs
it. It saves the world (`server.save` over the console), stops the server, `POST …/files/compress`es it. It stops the server with `POST …/power {"signal":"stop"}`, which runs the egg's stop command,
the three directories into `demo-baseline.tar.gz`, and starts the server again. Re-run it whenever `quit`, and that writes the save, so the subuser needs no console permission (DM22). It then
the default set should change. `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 **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 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 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. **Done:** the (`PTERODACTYL_CLIENT_KEY`). The panel URL and server identifier are plain variables
org lead created the subuser and added the secret on 2026-10-23. (`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 ### 7.3 The site
1. `docker compose down -v` — drops the database **and** the uploads volume. 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 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 (§7.5). the schema, and installs the Rust module from `MODULES=rust@<version>=<manifest>`, which the
3. Wait for the app's healthcheck. workflow writes to `state/modules.env` (§7.5). It installs under the allowlist a fresh database
4. `docker compose exec app npm run seed:demo -- <fixture.json>` (or `DEMO_SEED_FILE`) — one core 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: 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; - 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; - sets the site live with registration disabled, plus any `settings` the fixture adds;
@@ -382,10 +395,14 @@ That is why the module is chosen by protocol, not by date.
1. **The bundle changed since the last wipe.** It downloads the bundle's plugin tarball and sidecar 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 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 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. It records the bundle tag on the and its `with-sidecar.sh` launcher where the egg expects them (`rust-link/`, each `chmod 755`).
`demo-site` host. 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>` 2. **Always.** It picks the module release as above, and writes `MODULES=rust@<version>=<manifest>`
into the deploy directory's `.env` before §7.3. 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 `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 `server/sidecarClient.js`) to the install manifest it already publishes, so the choice needs no
@@ -403,8 +420,9 @@ down.
| `DEMO_MODE` lock, banner field, `seed:demo` script | `website` (core) | | `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` | | 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` | | `DEMO_LOCKED` error kind and the banner in the app | `Android-app` |
| `deploy/demo/` (compose file, `.env.example`, seed fixture) and `.gitea/workflows/demo-wipe.yml` + `demo-baseline.yml` | `website` (DM8) | | `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) |
| `.env` (JWT secret, encryption key, the rig's sidecar token) | On the `demo-site` host only, never in a repo | | 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` | | `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 | | `demo-baseline.tar.gz` | On the rig, at the Pterodactyl server root |
@@ -435,6 +453,9 @@ down.
| 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) | | 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) | | 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) | | 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 |
DNS and HTTPS for `demo.runicgateway.com` come from an external reverse proxy, which the org lead is 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 setting up. The app sits behind it on plain HTTP; core already trusts one proxy hop for
@@ -491,6 +512,10 @@ through that proxy.
event; a denied console command refused at save and at run; and a plugin config changed 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 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 secret field redacted; and the hero editor's draft previewed while its Publish is refused
(DM21). One PR. (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.
6. **Play**: the sign-in details in the Play Console point at `play-review`; 6. **Play**: the sign-in details in the Play Console point at `play-review`;
runicgateway.com `/beta` publishes the logins. runicgateway.com `/beta` publishes the logins.