Merge pull request 'docs(journey): phase 7 — the installation path and administration' (#10) from feat/phase-7-docs into main
All checks were successful
PR checks / checks (push) Successful in 1m7s
All checks were successful
PR checks / checks (push) Successful in 1m7s
Reviewed-on: #10
This commit is contained in:
@@ -99,3 +99,19 @@ jobs:
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: npm run check:facts
|
||||
|
||||
- name: Quickstart against website main
|
||||
# PLAN.md §12, phase 7 (D35). /docs/getting-started/install-the-site/ prints a
|
||||
# Compose file and an environment file the reader copies without leaving the page,
|
||||
# which is the one place this site knowingly keeps a copy of another repo's file.
|
||||
#
|
||||
# So the copy is checked in BOTH directions: every value it states must match
|
||||
# website's own docker-compose.yml and .env.example on main, and every service and
|
||||
# variable THEY have must be either included or listed as deliberately omitted with
|
||||
# a reason. A new variable upstream turns this repo red until someone decides
|
||||
# whether a first install needs it — the same intent as the facts check above.
|
||||
#
|
||||
# Same token, and for the same reason: it reads another repository in the org.
|
||||
env:
|
||||
GITEA_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||
run: npm run check:quickstart
|
||||
|
||||
87
PLAN.md
87
PLAN.md
@@ -236,7 +236,7 @@ Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not r
|
||||
|
||||
**Decisions after D13 are recorded where they were taken**, in the section describing the phase that
|
||||
raised them, rather than appended here — a decision is only re-litigated when its reasoning is
|
||||
somewhere other than the thing it decided. The count of record is **thirty-three**:
|
||||
somewhere other than the thing it decided. The count of record is **thirty-seven**:
|
||||
|
||||
| # | Where | What it settled |
|
||||
|---|---|---|
|
||||
@@ -245,6 +245,7 @@ somewhere other than the thing it decided. The count of record is **thirty-three
|
||||
| D20–D25 | §10, "How phase 4 built the marketing pages" | `/features/` as the same list with detail, `/architecture/` as reasons not reference, the absences as data, the two absorbed scope items, `needsModule`, the demo deep links |
|
||||
| D26–D29 | §8, "How phase 5 built the app and the beta" | The screenshot slot reserved for phase 9, the demo as the tester target, `/beta` handling its own POST, equal billing for the APK and the beta |
|
||||
| D30–D33 | §9, "How phase 6 built the legal pages" | One logging hop and no edge provider, eighteen or older, no governing-law clause, the Data Safety notes as a generated document |
|
||||
| D34–D37 | §10, "How phase 7 built the documentation journey" | One PR for all twenty pages, a self-contained install quickstart with a drift check, every admin screen walked before it was described, a thirteenth Administration page for content |
|
||||
|
||||
---
|
||||
|
||||
@@ -821,7 +822,7 @@ Getting started What is Runic Gateway? · Requirements · Install the site ·
|
||||
Verify the whole stack
|
||||
|
||||
Administration Configuration · Branding and theming · Navigation and pages ·
|
||||
Users and roles · Authentication · Teams · Moderation ·
|
||||
Content · Users and roles · Authentication · Teams · Moderation ·
|
||||
Notifications and email · Managing modules ·
|
||||
The shard connection · Maintenance and upgrades · Troubleshooting
|
||||
|
||||
@@ -836,7 +837,7 @@ Reference Environment variables · Installer CLI · sidecar.toml ·
|
||||
Bridge.cfg · HTTP API · Event catalog · Canonical documents
|
||||
```
|
||||
|
||||
Roughly 37 pages. Every Reference page is a **navigable summary plus a link to the canonical
|
||||
Roughly 38 pages — 37 planned, plus the Content page D37 added in phase 7. Every Reference page is a **navigable summary plus a link to the canonical
|
||||
document** — never a re-specification, per §1.
|
||||
|
||||
### The installation path
|
||||
@@ -851,13 +852,81 @@ website; the website is a separate Docker deployment.
|
||||
3. First run — first admin, maintenance → live
|
||||
4. Install a game module — admin panel, `MODULES` env, or by hand
|
||||
5. Connect a game server — the installer binary on the shard host (ServUO-specific today)
|
||||
6. Paste the four values into Admin → Shard — **protocol 4**, per §2
|
||||
6. Paste the four values into **Shard (uo-link)**, `/admin/uo/link` — **protocol 4**, per §2.
|
||||
(Not `/admin/shard`: the screen belongs to the module now, and the installer still prints the
|
||||
old path — see "How phase 7 built the documentation journey" below)
|
||||
7. Verify — `[bridge status` in game, `/health` reporting `plugin_connected: true`, then `doctor`
|
||||
8. Configure authentication and integrations
|
||||
|
||||
Each step states what the operator should expect to see, and links the failure modes to
|
||||
Troubleshooting.
|
||||
|
||||
### How phase 7 built the documentation journey
|
||||
|
||||
Four decisions taken before a page was written (org lead, 2026-08-24), and three things the live
|
||||
site disproved while it was being written.
|
||||
|
||||
**D34 — one PR for all twenty pages.** Twenty, not nineteen: see D37. The alternative on the table
|
||||
was splitting Getting started from Administration so the installation path could land first; the
|
||||
org lead kept the phase whole, as every phase before it has been.
|
||||
|
||||
**D35 — the install page is self-contained.** `/docs/getting-started/install-the-site/` prints a
|
||||
complete Compose file and a complete `.env` that an operator copies without going to another
|
||||
repository first. §1 argues at length against exactly this — it is a second copy of somebody else's
|
||||
file, free to rot — so the copy is not trusted, it is checked. `src/data/quickstart.mjs` holds both
|
||||
files and the page renders them; `scripts/checkQuickstart.mjs` re-reads `website`'s own
|
||||
`docker-compose.yml` and `.env.example` from `main` over the Gitea API and fails the build on any
|
||||
disagreement, in **both** directions:
|
||||
|
||||
- every value the quickstart states must match upstream's;
|
||||
- every service and variable upstream has must be **either included or listed as deliberately
|
||||
omitted, with a reason**, so a new variable in `.env.example` turns this repo red until someone
|
||||
decides whether a first install needs it;
|
||||
- and an entry in either omission list that upstream no longer has fails too, so the lists cannot
|
||||
rot into permanent exemptions.
|
||||
|
||||
Same mechanism and same intent as `checkFacts.mjs`. It caught two stale entries on its first run —
|
||||
`TOTP_ISSUER` and `MODULES`, which are commented *suggestions* upstream rather than keys — which is
|
||||
the check earning its place before the page had shipped.
|
||||
|
||||
**D36 — every Administration screen was walked before it was described.** Not read from source:
|
||||
opened, in a browser, on a real deployment. The rig was the quickstart itself — the exact two files
|
||||
from D35, against the published image — so one run proved the install page and produced the
|
||||
screenshots' worth of detail the admin pages needed. Three of the four defects below came from that
|
||||
walk, and no check could have found any of them.
|
||||
|
||||
**D37 — a thirteenth Administration page.** §10's planned twelve named no home for Posts, Pages,
|
||||
Wiki, Activity, Invites, the Hero editor or Web Bot Activity, all of which are real admin nav rows.
|
||||
Rather than mirror the panel one page per row — which would organise the docs by the app's menu,
|
||||
against this section's own principle — content authoring became one page, **Content**, and the
|
||||
other four folded into the page that already owned their subject: Invites into Users and roles, the
|
||||
Hero editor into Branding and theming, Web Bot Activity into Authentication.
|
||||
|
||||
**What the live deployment disproved.**
|
||||
|
||||
- **The documented Compose deploy does not boot.** `SECRET_ENC_KEY` is required in production —
|
||||
`utils/secretBox.js` throws at require time, so the container crash-loops before it listens — and
|
||||
it is **missing from website's root `.env.example`**, the file Compose actually reads. It is
|
||||
present in `server/.env.example`, which is the file local development copies, which is why this
|
||||
has never bitten anyone in dev. The quickstart carries it, declared as an upstream omission so the
|
||||
check fails the day it is fixed.
|
||||
- **The installer points operators at a screen that no longer exists.** It prints
|
||||
`<site>/admin/shard`, and INSTALL.md §5 repeats it. Since the module-system cutover a module owns
|
||||
one path segment, and the screen is **`/admin/uo/link`**, labelled *Shard (uo-link)*. Both the
|
||||
binary (`installer` `main:src/sidecar.rs`) and the guide are stale; the journey documents the real
|
||||
path and says the printed one does not resolve.
|
||||
- **The admin "Restart the server" button opens a `window.confirm`.** Its text is the honest
|
||||
warning that a deployment with no supervisor does not come back — which is exactly why
|
||||
`restart: unless-stopped` is called out as load-bearing on the install page rather than left as
|
||||
boilerplate.
|
||||
|
||||
**And the fourth defect, the one only a look found — three phases running.** The `.env` block's
|
||||
prose says *every highlighted line must be changed*, and `mark` given the variable **names**
|
||||
highlighted the names alone, leaving the values a reader has to replace unmarked. The build passed,
|
||||
every check passed, and the page was quietly wrong about its own highlighting. Marking the whole
|
||||
`KEY=value` string fixed it. See phase 4 (cascade), phase 5 (literal backticks)
|
||||
and phase 6 (the card void) for the same lesson.
|
||||
|
||||
---
|
||||
|
||||
## 11. Visual direction
|
||||
@@ -952,6 +1021,12 @@ a mechanism rather than diligence:
|
||||
links (D25) — lifting the pattern out of `applyBrand.mjs` and running it against the stock markup,
|
||||
so a template and a script that share no code cannot drift apart. Both are invisible in a stock
|
||||
build, which is exactly why they need a check rather than a look.
|
||||
- **`scripts/checkQuickstart.mjs`** — added in phase 7 for D35. The install page prints a Compose
|
||||
file and an `.env` verbatim, which is the one place this site knowingly copies another repo's
|
||||
file; this re-reads `website` `main:docker-compose.yml` and `main:.env.example` and fails on any
|
||||
disagreement. Two-directional, like `PLANNED_ROUTES`: a value that drifts fails, **and** a service
|
||||
or variable that appears upstream fails until it is either included or recorded as deliberately
|
||||
omitted with a reason. Its own first run found two stale entries.
|
||||
- **`scripts/checkTokens.mjs`** — no colour literal outside the token file (§7).
|
||||
- `astro check` plus a production build, in CI on every PR.
|
||||
|
||||
@@ -968,11 +1043,11 @@ a mechanism rather than diligence:
|
||||
| **4** | Marketing: `/features/`, `/architecture/`, `/modules/`, `/integrations/`, **and `/community/`** — plus `checkLinks.mjs`, the capability `detail` lines, `notBuilt.mjs` and the demo deep links. See D20–D25 |
|
||||
| **5** | The app and the beta: `/app/`, `/beta/`, the signup handler, the SQLite store, rate limiting, the export CLI (§8). **Also the repository's first `node --test` suite**, and phase 9 inherits an emulator pass (D26) |
|
||||
| **6** | Legal: `/privacy/`, `/terms/`, footer links, and the Play Data Safety notes (§9) |
|
||||
| **7** | Docs — the journey: Getting started (7) + Administration (12). **The installation path is the priority of the whole project** |
|
||||
| **7** | Docs — the journey: Getting started (7) + Administration (**13**, per D37) — twenty pages in one PR (D34), with the install page self-contained and drift-checked (D35) and every admin screen walked before it was described (D36). **The installation path is the priority of the whole project** |
|
||||
| **8** | Docs — builder and reference: Modules (8) + Architecture (5) + Reference (7) |
|
||||
| **9** | Screenshots (D4): stand up the local review stack, seed presentable content, capture the admin panel, Teams, forums, marketplace, spawn atlas and shard console; build the screenshot components. **Plus an emulator pass against the same seeded stack** to fill `/app/`'s reserved slot (D26) |
|
||||
| **10** | Polish: responsive, accessibility, SEO/OpenGraph/sitemap/robots, full-text search, CSP headers |
|
||||
| **11** | Validation: `astro check`, production build, **all five check scripts** (tokens, brand, links, facts, types), mobile layout verified in a real browser, a signup walked end to end |
|
||||
| **11** | Validation: `astro check`, production build, **all six check scripts** (tokens, brand, links, facts, quickstart, data safety), mobile layout verified in a real browser, a signup walked end to end |
|
||||
| **12** | Delivery: Dockerfile, `docker-compose.yml` with both bind mounts documented, Gitea Actions workflow publishing to the registry, README, CONTRIBUTING with the AI-disclosure requirement, and an operator note covering DNS, TLS and the reverse proxy (D6) |
|
||||
|
||||
Phases 5 and 6 are deliberately adjacent and early: the beta cannot start without `/privacy`, and
|
||||
|
||||
3
package-lock.json
generated
3
package-lock.json
generated
@@ -20,7 +20,8 @@
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"opentype.js": "^2.0.0",
|
||||
"typescript": "^6.0.3"
|
||||
"typescript": "^6.0.3",
|
||||
"yaml": "^2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
|
||||
@@ -19,11 +19,12 @@
|
||||
"check:brand": "node scripts/checkBrand.mjs",
|
||||
"check:links": "node scripts/checkLinks.mjs",
|
||||
"check:datasafety": "node scripts/playDataSafety.mjs --check",
|
||||
"check:quickstart": "node scripts/checkQuickstart.mjs",
|
||||
"play:datasafety": "node scripts/playDataSafety.mjs",
|
||||
"beta": "node scripts/beta.mjs",
|
||||
"test": "node --test test/beta.test.mjs test/legal.test.mjs",
|
||||
"brand:assets": "node scripts/buildBrandAssets.mjs",
|
||||
"verify": "npm run check:tokens && npm run check:brand && npm run check:datasafety && npm run check && npm test && npm run build && npm run check:links && npm run check:facts"
|
||||
"verify": "npm run check:tokens && npm run check:brand && npm run check:datasafety && npm run check && npm test && npm run build && npm run check:links && npm run check:facts && npm run check:quickstart"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^11.1.4",
|
||||
@@ -37,6 +38,7 @@
|
||||
"devDependencies": {
|
||||
"@astrojs/check": "^0.9.10",
|
||||
"opentype.js": "^2.0.0",
|
||||
"yaml": "^2.8.1",
|
||||
"typescript": "^6.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
216
scripts/checkQuickstart.mjs
Normal file
216
scripts/checkQuickstart.mjs
Normal file
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* checkQuickstart.mjs — PLAN.md §12, added in phase 7 for D35.
|
||||
*
|
||||
* The org lead chose a self-contained quickstart: `/docs/getting-started/install-the-site/`
|
||||
* prints a Compose file and an environment file the reader can copy without going to
|
||||
* another repository first. That is the one place this site knowingly keeps a copy of
|
||||
* somebody else's file, and §1 is a long argument about why copies rot.
|
||||
*
|
||||
* So the copy is checked rather than trusted. Every service, image, published port, mount
|
||||
* and environment key in `src/data/quickstart.mjs` is re-read from `website`'s own
|
||||
* `docker-compose.yml` and `.env.example` on `main`, over the Gitea API — never from a
|
||||
* working tree, per §1's process rule — and any disagreement fails the build.
|
||||
*
|
||||
* It checks in BOTH directions, which is the property that keeps it honest:
|
||||
*
|
||||
* - every value the quickstart states must match upstream's;
|
||||
* - every service and variable upstream has must be either included or listed as
|
||||
* deliberately omitted, WITH a reason. A new variable in `.env.example` therefore turns
|
||||
* this repo red until someone decides whether a first install needs it — the same
|
||||
* intent as checkFacts.mjs and the Integration Kit's checkCoreApi.js;
|
||||
* - and an entry in either omission list that upstream no longer has fails too, so the
|
||||
* lists cannot rot into permanent exemptions.
|
||||
*
|
||||
* GITEA_TOKEN=<token> node scripts/checkQuickstart.mjs
|
||||
*
|
||||
* Anonymous raw fetches fail on this instance, so the token is required. A check that
|
||||
* silently skips itself is worse than no check.
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import path from 'node:path';
|
||||
import { parse as parseYaml } from 'yaml';
|
||||
|
||||
import {
|
||||
compose,
|
||||
services,
|
||||
omittedServices,
|
||||
env,
|
||||
envOmitted,
|
||||
notInUpstreamEnvExample,
|
||||
} from '../src/data/quickstart.mjs';
|
||||
|
||||
const ROOT = fileURLToPath(new URL('..', import.meta.url));
|
||||
const platform = JSON.parse(readFileSync(path.join(ROOT, 'src/data/platform.json'), 'utf8'));
|
||||
|
||||
const BASE = platform.gitea.base;
|
||||
const ORG = platform.gitea.org;
|
||||
const TOKEN = process.env.GITEA_TOKEN?.trim();
|
||||
|
||||
const failures = [];
|
||||
const checked = [];
|
||||
|
||||
const ok = (what) => checked.push(what);
|
||||
const fail = (what, detail) => failures.push({ what, detail });
|
||||
|
||||
/** Same raw-file accessor checkFacts.mjs uses, and for the same reason. */
|
||||
async function raw(repo, filePath, ref) {
|
||||
const url = `${BASE}/api/v1/repos/${ORG}/${repo}/raw/${filePath}?ref=${encodeURIComponent(ref)}`;
|
||||
const res = await fetch(url, { headers: { Authorization: `token ${TOKEN}` } });
|
||||
if (!res.ok) throw new Error(`${res.status} ${res.statusText} for ${url}`);
|
||||
return res.text();
|
||||
}
|
||||
|
||||
/**
|
||||
* `KEY=value` lines from a dotenv file. Commented-out suggestions (`# MODULES=…`) are NOT
|
||||
* keys: they are prose about a variable, and treating them as declared would make the
|
||||
* omission list argue with documentation rather than with configuration.
|
||||
*/
|
||||
function envKeys(text) {
|
||||
const out = new Map();
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
const m = line.match(/^([A-Z][A-Z0-9_]*)=(.*)$/);
|
||||
if (m) out.set(m[1], m[2].replace(/\s+#.*$/, '').trim());
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Published host:container port pairs, as written. */
|
||||
const portsOf = (svc) => (svc.ports ?? []).map(String);
|
||||
|
||||
/** Container-side paths of every volume entry, which is what a reader's site depends on. */
|
||||
const mountTargets = (svc) => (svc.volumes ?? []).map((v) => String(v).split(':')[1]);
|
||||
|
||||
async function run() {
|
||||
if (!TOKEN) {
|
||||
console.error('checkQuickstart: GITEA_TOKEN is not set. This check cannot run anonymously.');
|
||||
process.exit(2);
|
||||
}
|
||||
|
||||
const upstreamComposeText = await raw('website', 'docker-compose.yml', 'main');
|
||||
const upstreamEnvText = await raw('website', '.env.example', 'main');
|
||||
|
||||
const upstream = parseYaml(upstreamComposeText);
|
||||
const ours = parseYaml(compose);
|
||||
|
||||
if (!upstream?.services) throw new Error('website main:docker-compose.yml has no services block — the file shape changed.');
|
||||
|
||||
// ── 1. The services we ship ───────────────────────────────────────────────
|
||||
for (const name of services) {
|
||||
const mine = ours.services?.[name];
|
||||
const theirs = upstream.services?.[name];
|
||||
if (!mine) { fail(`service ${name}`, 'declared in quickstart.mjs but absent from its own compose text'); continue; }
|
||||
if (!theirs) { fail(`service ${name}`, 'no longer exists in website main:docker-compose.yml'); continue; }
|
||||
|
||||
if (String(mine.image) !== String(theirs.image)) {
|
||||
fail(`service ${name}: image`, `quickstart "${mine.image}" vs upstream "${theirs.image}"`);
|
||||
} else ok(`service ${name}: image`);
|
||||
|
||||
const minePorts = portsOf(mine).join(', ');
|
||||
const theirPorts = portsOf(theirs).join(', ');
|
||||
if (minePorts !== theirPorts) {
|
||||
fail(`service ${name}: ports`, `quickstart [${minePorts}] vs upstream [${theirPorts}]`);
|
||||
} else ok(`service ${name}: ports`);
|
||||
|
||||
// Every mount we keep must land where upstream lands it. Upstream may have mounts we
|
||||
// dropped (the schema bind, which needs a checkout); dropping one is safe, moving one
|
||||
// is not.
|
||||
for (const target of mountTargets(mine)) {
|
||||
if (!mountTargets(theirs).includes(target)) {
|
||||
fail(`service ${name}: mount ${target}`, 'upstream mounts nothing at that container path');
|
||||
} else ok(`service ${name}: mount ${target}`);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(mine.environment ?? {})) {
|
||||
const theirValue = theirs.environment?.[key];
|
||||
if (theirValue === undefined) {
|
||||
fail(`service ${name}: ${key}`, 'upstream no longer sets it in the compose file');
|
||||
} else if (String(theirValue) !== String(value)) {
|
||||
fail(`service ${name}: ${key}`, `quickstart "${value}" vs upstream "${theirValue}"`);
|
||||
} else ok(`service ${name}: ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 2. The services we left out, and any that appeared ────────────────────
|
||||
const upstreamServiceNames = Object.keys(upstream.services);
|
||||
for (const [name, reason] of Object.entries(omittedServices)) {
|
||||
if (!upstreamServiceNames.includes(name)) {
|
||||
fail(`omitted service ${name}`, 'upstream no longer has this service — drop it from omittedServices');
|
||||
} else if (!reason?.trim()) {
|
||||
fail(`omitted service ${name}`, 'listed without a reason');
|
||||
} else ok(`omitted service ${name}`);
|
||||
}
|
||||
for (const name of upstreamServiceNames) {
|
||||
if (!services.includes(name) && !(name in omittedServices)) {
|
||||
fail(`service ${name}`, 'is new in website main:docker-compose.yml — include it in the quickstart or record why not');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 3. The environment file ───────────────────────────────────────────────
|
||||
const theirEnv = envKeys(upstreamEnvText);
|
||||
const mineEnv = new Map(env.map((e) => [e.key, e]));
|
||||
|
||||
for (const entry of env) {
|
||||
const theirValue = theirEnv.get(entry.key);
|
||||
const excused = notInUpstreamEnvExample[entry.key];
|
||||
|
||||
if (theirValue === undefined) {
|
||||
if (excused) {
|
||||
ok(`env ${entry.key} (absent upstream, declared: ${excused})`);
|
||||
} else {
|
||||
fail(`env ${entry.key}`, 'not in website main:.env.example — either it is gone, or it needs a reason in notInUpstreamEnvExample');
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (excused) {
|
||||
fail(
|
||||
`env ${entry.key}`,
|
||||
'is now in website main:.env.example — remove it from notInUpstreamEnvExample, and re-read the prose that describes it as missing',
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A value an operator is told to replace is a placeholder on both sides; comparing two
|
||||
// placeholders would only ever assert that two people picked the same filler words.
|
||||
if (!entry.fill && theirValue !== String(entry.value)) {
|
||||
fail(`env ${entry.key}`, `quickstart "${entry.value}" vs upstream "${theirValue}"`);
|
||||
} else ok(`env ${entry.key}`);
|
||||
}
|
||||
|
||||
for (const [key, reason] of Object.entries(envOmitted)) {
|
||||
if (!theirEnv.has(key)) {
|
||||
fail(`omitted env ${key}`, 'upstream .env.example no longer sets it — drop it from envOmitted');
|
||||
} else if (!reason?.trim()) {
|
||||
fail(`omitted env ${key}`, 'listed without a reason');
|
||||
} else ok(`omitted env ${key}`);
|
||||
}
|
||||
|
||||
for (const key of theirEnv.keys()) {
|
||||
if (!mineEnv.has(key) && !(key in envOmitted)) {
|
||||
fail(`env ${key}`, 'is new in website main:.env.example — add it to the quickstart or record why a first install does not need it');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Report ────────────────────────────────────────────────────────────────
|
||||
if (failures.length === 0) {
|
||||
console.log(`checkQuickstart: ${checked.length} checks passed against website main.`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.error(`checkQuickstart: ${failures.length} disagreement(s) with website main:\n`);
|
||||
for (const f of failures) console.error(` ✗ ${f.what}\n ${f.detail}`);
|
||||
console.error(
|
||||
'\nThe quickstart on /docs/getting-started/install-the-site/ is a copy of website\'s own\n'
|
||||
+ 'deployment files (D35). Either update src/data/quickstart.mjs to match, or record the\n'
|
||||
+ 'difference with a reason. Do not "fix" the check.',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
run().catch((err) => {
|
||||
console.error(`checkQuickstart: ${err.message}`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -16,7 +16,33 @@
|
||||
export const docsSidebar = [
|
||||
{
|
||||
label: 'Getting started',
|
||||
items: [{ label: 'What is Runic Gateway?', slug: 'docs' }],
|
||||
items: [
|
||||
{ label: 'What is Runic Gateway?', slug: 'docs' },
|
||||
{ label: 'Requirements', slug: 'docs/getting-started/requirements' },
|
||||
{ label: 'Install the site', slug: 'docs/getting-started/install-the-site' },
|
||||
{ label: 'First run', slug: 'docs/getting-started/first-run' },
|
||||
{ label: 'Install a game module', slug: 'docs/getting-started/install-a-game-module' },
|
||||
{ label: 'Connect a game server', slug: 'docs/getting-started/connect-a-game-server' },
|
||||
{ label: 'Verify the whole stack', slug: 'docs/getting-started/verify-the-whole-stack' },
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Administration',
|
||||
items: [
|
||||
{ label: 'Configuration', slug: 'docs/administration/configuration' },
|
||||
{ label: 'Branding and theming', slug: 'docs/administration/branding-and-theming' },
|
||||
{ label: 'Navigation and pages', slug: 'docs/administration/navigation-and-pages' },
|
||||
{ label: 'Content', slug: 'docs/administration/content' },
|
||||
{ label: 'Users and roles', slug: 'docs/administration/users-and-roles' },
|
||||
{ label: 'Authentication', slug: 'docs/administration/authentication' },
|
||||
{ label: 'Teams', slug: 'docs/administration/teams' },
|
||||
{ label: 'Moderation', slug: 'docs/administration/moderation' },
|
||||
{ label: 'Notifications and email', slug: 'docs/administration/notifications-and-email' },
|
||||
{ label: 'Managing modules', slug: 'docs/administration/managing-modules' },
|
||||
{ label: 'The shard connection', slug: 'docs/administration/the-shard-connection' },
|
||||
{ label: 'Maintenance and upgrades', slug: 'docs/administration/maintenance-and-upgrades' },
|
||||
{ label: 'Troubleshooting', slug: 'docs/administration/troubleshooting' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
82
src/content/docs/docs/administration/authentication.mdx
Normal file
82
src/content/docs/docs/administration/authentication.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Authentication
|
||||
description: Local accounts and two-factor, SSO providers and the link-only policy, and the layer that keeps automated traffic out.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
**Admin → Authentication** has four tabs: Local Accounts, Google, Discord and Custom
|
||||
Providers. One session model sits behind all of them — a web cookie, a mobile bearer token
|
||||
and an SSO sign-in all produce the same session.
|
||||
|
||||
## Local accounts
|
||||
|
||||
Username and password sign-in is **always enabled and cannot be turned off**. It is how you
|
||||
manage accounts and how SSO identities get linked in the first place, so there is no
|
||||
configuration on this tab beyond that statement.
|
||||
|
||||
**Two-factor** is a per-account, opt-in TOTP code, set up by each person under **Account**
|
||||
in the sidebar. Nobody can enable it on someone else's behalf, and staff accounts are the
|
||||
ones worth insisting on.
|
||||
|
||||
## SSO providers
|
||||
|
||||
Google and Discord each need a client ID and secret from that provider's developer console;
|
||||
Custom Providers takes any OAuth2/OIDC issuer. Secrets are encrypted at rest with
|
||||
`SECRET_ENC_KEY` and are never returned to any client.
|
||||
|
||||
<Aside type="caution" title="SSO is link-only, by policy">
|
||||
An external identity can only sign in to an account it is **already linked to**. Signing in
|
||||
with Google does not create an account, ever. People link a provider themselves from their
|
||||
own account screen, and that link is what grants the access — so a stranger with a Google
|
||||
account is still a stranger.
|
||||
</Aside>
|
||||
|
||||
Configuring Google here also unlocks **email delivery**, which reuses the same OAuth client
|
||||
— see [Notifications and email](/docs/administration/notifications-and-email/).
|
||||
|
||||
## Trusted devices
|
||||
|
||||
A second factor that asks on every sign-in on the same laptop trains people to click
|
||||
through it. A device can be remembered after a successful two-factor challenge, and the
|
||||
trust rides the browser's own cookie jar — including the in-app browser tab the Android app
|
||||
opens for SSO, which is why signing in there does not ask again.
|
||||
|
||||
Trust is per device and revocable, and it survives signing out: signing out ends a session,
|
||||
not the statement that this machine is yours.
|
||||
|
||||
## What keeps the automated traffic out
|
||||
|
||||
Four layers, all on by default:
|
||||
|
||||
- **Rate limiting and backoff** on the login routes, so a password guess costs time.
|
||||
- **A honeypot field** that a human never fills in and a naive bot always does.
|
||||
- **Bot scoring**, which accumulates points against an address for behaviour no human
|
||||
produces, and bans it automatically past a threshold.
|
||||
- **IP bans** from that scoring.
|
||||
|
||||
**Admin → Web Bot Activity** shows the live state: currently banned addresses with their
|
||||
score and expiry, and the recent events with the reason, path and points that produced
|
||||
them. It is deliberately **read-only apart from an emergency unban** — there is nothing to
|
||||
tune here, and the panel exists so that a legitimate user locked out by their office's
|
||||
shared address can be let back in.
|
||||
|
||||
<Aside type="note" title="The scoring state is in memory, and resets when the server restarts">
|
||||
So a restart clears every automatic ban. That is a reasonable escape hatch when you have
|
||||
locked yourself out, and a reason not to treat this list as a permanent record.
|
||||
</Aside>
|
||||
|
||||
## Getting locked out
|
||||
|
||||
Two situations worth knowing before they happen at three in the morning:
|
||||
|
||||
- **Your address is banned.** Restart the app container — the in-memory state goes with it.
|
||||
- **You lost your second factor.** Use one of the recovery codes issued when you enabled
|
||||
it. If those are gone too, another administrator opens **Users → View** on your account
|
||||
and presses **Reset two-factor**, which turns TOTP off, revokes your trusted devices and
|
||||
clears your recovery codes so a password sign-in works again. That is the practical
|
||||
argument for a site never having exactly one admin.
|
||||
|
||||
The same screen lists an account's trusted devices and revokes them individually or all at
|
||||
once — the right response to a lost or stolen laptop, and something to reach for before
|
||||
resetting the whole second factor.
|
||||
@@ -0,0 +1,85 @@
|
||||
---
|
||||
title: Branding and theming
|
||||
description: Colours, fonts and corners from the Appearance screen; logo, hero and favicon from a mounted directory; the portal hero from its own editor.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
One prebuilt image runs as any community's site. Nothing about your identity is compiled
|
||||
in — it is a theme row in the database, three image files on a mount, and a few environment
|
||||
variables for the values that must exist before the database does.
|
||||
|
||||
## Appearance
|
||||
|
||||
**Admin → Appearance** themes the public site, the admin panel and the player portal
|
||||
together.
|
||||
|
||||
**Presets** — *Runic Gateway*, *Modern*, *Fantasy*, *Custom* — set a whole palette at once.
|
||||
Anything you set below a preset overrides it field by field, and a colour you never set
|
||||
keeps following the preset. That is the useful property: pick the preset closest to what
|
||||
you want, change the two colours that are wrong, and the rest still moves with it.
|
||||
|
||||
| Group | What is in it |
|
||||
|---|---|
|
||||
| **Colors** | Background, deep background, panel top and bottom, accent, bright accent, ink/headings, body text |
|
||||
| **Fonts** | Body serif, display/headings, interface sans — each with a "follow the preset" default |
|
||||
| **Corners & depth** | Radius for pills and buttons, flat panels, cards, inputs; and card shadow |
|
||||
|
||||
Two things the screen tells you that are easy to miss:
|
||||
|
||||
- **Live and maintenance status colours are never themed.** Green has to keep meaning live.
|
||||
- **The accent reaches the mobile app and the Discord bot**, both of which theme themselves
|
||||
from this site's public branding. Changing it here changes them.
|
||||
|
||||
## Brand assets
|
||||
|
||||
The same screen uploads three images, and each applies as soon as the upload finishes —
|
||||
there is nothing to save.
|
||||
|
||||
| Asset | Where it shows | Limit |
|
||||
|---|---|---|
|
||||
| **Logo** | Site header, admin sidebar, player portal, and link previews when a page is shared | 1 MB |
|
||||
| **Hero image** | Behind the portal hero, unless the hero editor has its own background | 8 MB |
|
||||
| **Favicon** | The browser tab. PNG only; 32×32 or 64×64 works everywhere | 512 KB |
|
||||
|
||||
Underneath, these are files on the `./brand` bind mount from
|
||||
[Install the site](/docs/getting-started/install-the-site/), pointed at by `BRAND_LOGO`,
|
||||
`BRAND_HERO` and `BRAND_FAVICON`. An upload writes there; so does copying a file in by
|
||||
hand. Both are supported, and the mount is why replacing a logo never means rebuilding an
|
||||
image.
|
||||
|
||||
<Aside type="note" title="The “powered by Runic Gateway” mark in the footer is not yours to theme">
|
||||
It is the project's badge rather than your instance's, and it does not change with the
|
||||
theme.
|
||||
</Aside>
|
||||
|
||||
## The text that comes from the environment
|
||||
|
||||
A few identity values are read before the database is available — the server templates them
|
||||
into `index.html` at boot so that link previews and the tab title are right on the very
|
||||
first request:
|
||||
|
||||
`BRAND_NAME`, `BRAND_SHORT_NAME`, `BRAND_TAGLINE`, `BRAND_DESCRIPTION`,
|
||||
`BRAND_ACCENT_COLOR`, `BRAND_URL`, `BRAND_CONTACT_EMAIL`.
|
||||
|
||||
Where an admin-editable setting exists for the same thing — site title, contact email — the
|
||||
**setting wins**. The variable is the value a fresh deployment starts from.
|
||||
|
||||
## The portal hero
|
||||
|
||||
**Admin → Hero Editor** composes the front page's hero directly: drag elements to place
|
||||
them, drag the corner handle to resize (text scales with the box), Delete removes the
|
||||
selected one. The palette adds text, buttons, the moon, a badge or an image.
|
||||
|
||||
Its own background image and overlay darkness are set at the bottom of the editor, and a
|
||||
background set here **wins over** the Appearance screen's hero image.
|
||||
|
||||
Work is not live until you press **Publish**; **Preview** opens it in a new tab, and
|
||||
**Revert to live** throws away an unpublished draft. Until anything is published at all,
|
||||
the portal renders the shipped hero with the homepage teaser from
|
||||
[Settings](/docs/administration/configuration/) underneath it.
|
||||
|
||||
<Aside type="caution" title="Check a hero on a phone before publishing it">
|
||||
The editor is a canvas, and a layout that reads well at desktop width can put text over a
|
||||
face or off the edge on a narrow screen. Preview it there.
|
||||
</Aside>
|
||||
99
src/content/docs/docs/administration/configuration.mdx
Normal file
99
src/content/docs/docs/administration/configuration.mdx
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
title: Configuration
|
||||
description: What is set in the environment file, what is set in the admin panel, and why the split is where it is.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Two places hold configuration, and the line between them is not arbitrary.
|
||||
|
||||
| | Environment (`.env`) | Admin panel |
|
||||
|---|---|---|
|
||||
| **What** | How the process runs: ports, database, secrets, proxy trust, log level | How the site behaves: titles, registration, forums, integrations |
|
||||
| **Changing it** | Edit the file, `docker compose up -d` | Save the form; effective immediately |
|
||||
| **Who** | Whoever has the host | Whoever has an admin account |
|
||||
| **Where it lives** | A file on the host | The database |
|
||||
|
||||
The rule behind the split: **anything that needs a restart or a shell is environment;
|
||||
anything an administrator should be able to change without either is in the panel.** That
|
||||
is why the Discord bot token, the OAuth client secrets and the shard's auth token are *not*
|
||||
environment variables — they are entered in the panel and stored encrypted.
|
||||
|
||||
## Settings
|
||||
|
||||
**Admin → Settings**, the screen most of a new deployment's decisions live on.
|
||||
|
||||
| Field | What it does |
|
||||
|---|---|
|
||||
| **Site title** | Overrides `BRAND_NAME` in the page title, the header and link previews. |
|
||||
| **Homepage teaser** | Rich text under the hero heading, when no custom hero layout is published. |
|
||||
| **Maintenance message** | What visitors see while the site is in maintenance mode. |
|
||||
| **Status message** | A short line for announcements — a maintenance window, an outage. |
|
||||
| **Contact email** | Where the contact form delivers, and the address it falls back to as a `mailto:` link while email is unconfigured. |
|
||||
| **Player registration** | Disabled, password, SSO, or both. **Off by default.** |
|
||||
|
||||
### Player registration is off until you turn it on
|
||||
|
||||
A new site accepts no self-registration at all. The three ways to let people in:
|
||||
|
||||
- **Password** — a normal sign-up form.
|
||||
- **SSO** — sign-up through a linked provider, which needs a provider configured first.
|
||||
- **Invites** — leave registration off entirely and issue invitations from
|
||||
**Admin → Invites**. See [Users and roles](/docs/administration/users-and-roles/).
|
||||
|
||||
## Team forums
|
||||
|
||||
The same screen carries the forum switches, because they are site-wide policy rather than
|
||||
per-Team settings:
|
||||
|
||||
- **Enable team forums** — off by default. Switching them off hides them completely (every
|
||||
forum route answers *not found*) but **deletes nothing**: threads, posts, access grants
|
||||
and notification preferences all survive and come back exactly as they were.
|
||||
- **Images in forum posts** — disabled, remote URLs only, or uploads to your server.
|
||||
Enabling uploads means content stored on infrastructure you are responsible for, and the
|
||||
screen says so at some length before you can agree to it.
|
||||
- **Post edit window** — how long an author may edit their own post. Staff are not bound by
|
||||
it. Zero makes posts permanent once written; some bound is what stops a post being
|
||||
rewritten out from under someone quoting it.
|
||||
|
||||
## Email
|
||||
|
||||
Configured on the same screen and covered in
|
||||
[Notifications and email](/docs/administration/notifications-and-email/): it is Gmail over
|
||||
OAuth2, it reuses the Google authentication client, and it must be set up on the
|
||||
[Authentication](/docs/administration/authentication/) page first.
|
||||
|
||||
<Aside type="note" title="Until email is connected, the contact form is a mailto: link">
|
||||
That is a deliberate fallback rather than a failure — but it does mean the *Contact email*
|
||||
setting is doing real work on a site that has never configured delivery, and an unset one
|
||||
leaves a contact form that goes nowhere.
|
||||
</Aside>
|
||||
|
||||
## The environment file, in three groups
|
||||
|
||||
You wrote these in [Install the site](/docs/getting-started/install-the-site/); this is
|
||||
what they mean when you come back to them.
|
||||
|
||||
**Identity and process** — `NODE_ENV`, `PORT`, `INTERNAL_PORT`, `IMAGE_TAG`. `INTERNAL_PORT`
|
||||
is the server-to-bot channel and must never be published or proxied.
|
||||
|
||||
**Data and secrets** — the `DB_*` group, `JWT_SECRET`, `SECRET_ENC_KEY`, `BOT_INTERNAL_KEY`.
|
||||
The last two are required in production, and `SECRET_ENC_KEY` is the key everything else
|
||||
encrypted at rest is keyed by: change it and the stored secrets become unreadable.
|
||||
|
||||
**Behaviour at the edge** — `TRUST_PROXY`, `COOKIE_SECURE`, `COOKIE_NAME`,
|
||||
`JWT_EXPIRES_IN`. `COOKIE_NAME` is worth one warning: changing it on a live site logs
|
||||
everybody out.
|
||||
|
||||
<Aside type="caution" title="`MODULE_SOURCE_HOSTS` is bootstrap only">
|
||||
It seeds the module install allowlist the first time a site boots without one. After that
|
||||
the **setting** is authoritative and is edited in Admin → Modules — changing the variable on
|
||||
an existing deployment does nothing, deliberately, so a redeploy cannot silently undo an
|
||||
administrator's choice.
|
||||
</Aside>
|
||||
|
||||
## Branding is data, not configuration
|
||||
|
||||
The `BRAND_*` variables and the `/brand` mount are how one prebuilt image runs as any
|
||||
community's site. They get their own page:
|
||||
[Branding and theming](/docs/administration/branding-and-theming/).
|
||||
62
src/content/docs/docs/administration/content.mdx
Normal file
62
src/content/docs/docs/administration/content.mdx
Normal file
@@ -0,0 +1,62 @@
|
||||
---
|
||||
title: Content
|
||||
description: Posts and their categories, the wiki and its sections, and the activity log that records who changed what.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Three content surfaces, one for each shape of writing a community does.
|
||||
|
||||
| Surface | For | Lives at |
|
||||
|---|---|---|
|
||||
| **Posts** | Dated writing: news, the newsletter, screenshots | `/site/news` and friends |
|
||||
| **Pages** | Standing pages: About, Rules, Donate — see [Navigation and pages](/docs/administration/navigation-and-pages/) | its own slug |
|
||||
| **Wiki** | Reference the community maintains: guides, lore, systems | `/wiki` |
|
||||
|
||||
## Posts
|
||||
|
||||
**Admin → Posts**, filtered by category. A new deployment seeds four:
|
||||
|
||||
- **News** — the default, and the one wired to announcements.
|
||||
- **Five on Friday** — a recurring short-form format.
|
||||
- **Newsletter** — longer, periodic.
|
||||
- **Screenshots** — image posts.
|
||||
|
||||
Each post is a draft until it is published, and the Posts list shows status and date at a
|
||||
glance.
|
||||
|
||||
<Aside type="caution" title="Publishing a news post announces it">
|
||||
Publishing is what triggers the announcement pipeline — the Discord `#news` leg, and any leg
|
||||
an installed module adds, such as the `uo` module's in-game town crier. It fires on
|
||||
publication, so an accidental publish is an accidental announcement. See
|
||||
[Notifications and email](/docs/administration/notifications-and-email/).
|
||||
</Aside>
|
||||
|
||||
## The wiki
|
||||
|
||||
**Admin → Wiki** lists every page with its section and status, and **Manage sections**
|
||||
edits the grouping itself. A new site starts with eight pages in four sections — Guides,
|
||||
World & Lore, Systems & Gameplay, Community & Rules — as a skeleton to write into.
|
||||
|
||||
They are placeholders. None of them describes your game, and leaving them published means
|
||||
publishing an empty guide to it; either write them or unpublish them before you go live.
|
||||
|
||||
## Who may write what
|
||||
|
||||
Roles decide it, and the split is the useful part:
|
||||
|
||||
- **Editor** — the content roles. Posts, pages, wiki, and the activity log.
|
||||
- **Moderator** — moderation and Teams, not content authoring.
|
||||
- **Admin** — everything, including the system screens.
|
||||
|
||||
Full table in [Users and roles](/docs/administration/users-and-roles/).
|
||||
|
||||
## The activity log
|
||||
|
||||
**Admin → Activity** records what staff did: the action, a detail line, who did it, from
|
||||
which address, and when. Module installs, logins, content changes and moderation all land
|
||||
here.
|
||||
|
||||
Two things it is good for beyond curiosity: reconstructing what changed just before
|
||||
something broke, and confirming that an account which should not have done something did
|
||||
not. It is a record, not a workflow — nothing is actioned from this screen.
|
||||
@@ -0,0 +1,125 @@
|
||||
---
|
||||
title: Maintenance and upgrades
|
||||
description: Upgrading the image, pinning a build, what to back up and how, where the logs are, and the reverse proxy.
|
||||
---
|
||||
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
## Upgrading the site
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
That is the whole routine. The image carries the server and the built client together;
|
||||
schema changes are applied on boot, and installed modules are on a volume the upgrade does
|
||||
not touch.
|
||||
|
||||
**Pin a build when you want a deploy you can reproduce.** `IMAGE_TAG` defaults to `latest`;
|
||||
every merge also publishes `sha-<7>`, so
|
||||
|
||||
```bash
|
||||
IMAGE_TAG=sha-042a151 docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
deploys an exact build, and putting that value in `.env` makes it the one this host runs
|
||||
until you change it. Rolling back is the same command with the previous tag — with one
|
||||
caveat that decides whether it works.
|
||||
|
||||
<Aside type="caution" title="A rollback is only safe if the schema did not move">
|
||||
Upgrades apply schema changes on boot; nothing un-applies them. Rolling the image back to a
|
||||
build that predates a schema change leaves the older code looking at a newer database.
|
||||
Restore the backup you took first, or stay forward.
|
||||
</Aside>
|
||||
|
||||
## Back up before you upgrade
|
||||
|
||||
Two volumes and one directory hold everything that cannot be re-downloaded: the database,
|
||||
the uploads, and `./modules`.
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Dump the database.** From the deployment directory, while the stack is up:
|
||||
|
||||
```bash
|
||||
docker compose exec -T db sh -c \
|
||||
'mariadb-dump -u root -p"$MARIADB_ROOT_PASSWORD" --single-transaction --routines runic_gateway' \
|
||||
> backup-$(date +%F).sql
|
||||
```
|
||||
|
||||
`--single-transaction` is what makes it consistent without locking the site.
|
||||
|
||||
2. **Copy the uploads volume.**
|
||||
|
||||
```bash
|
||||
docker run --rm -v <deployment>_uploads:/from -v "$PWD":/to alpine \
|
||||
tar czf /to/uploads-$(date +%F).tgz -C /from .
|
||||
```
|
||||
|
||||
The volume is named after the directory Compose runs in — `docker volume ls` shows the
|
||||
exact names.
|
||||
|
||||
3. **Keep `./modules`, `./brand` and your two files.** They are ordinary host directories;
|
||||
whatever backs up the rest of the host covers them.
|
||||
|
||||
</Steps>
|
||||
|
||||
Restoring the database is the same command inverted — `mariadb … < backup.sql` — into a
|
||||
stack whose image is the one the dump came from.
|
||||
|
||||
## Logs
|
||||
|
||||
`./logs/app.log` on the host, because the Compose file bind-mounts it there. `docker compose
|
||||
logs -f app` shows the same stream live.
|
||||
|
||||
`LOG_LEVEL` sets console verbosity and `FILE_LOG_LEVEL` the file's — the file keeps the
|
||||
fuller record on purpose. Nothing rotates them for you.
|
||||
|
||||
## Restarting
|
||||
|
||||
`docker compose restart app` is the ordinary restart, and it is what the admin panel's
|
||||
**Restart the server** button amounts to. Restarts are needed after installing, enabling or
|
||||
uninstalling a module, and are harmless otherwise.
|
||||
|
||||
`docker compose down` stops everything and keeps the data. **`docker compose down -v` also
|
||||
deletes the volumes** — the database and every upload. There is no undo.
|
||||
|
||||
## The reverse proxy
|
||||
|
||||
The app publishes port 3000 and binds all interfaces, so any proxy that can reach the host
|
||||
can serve it. Two settings make it correct rather than merely working, both covered in
|
||||
[Install the site](/docs/getting-started/install-the-site/): `TRUST_PROXY`, so the address
|
||||
your rate limiting and IP bans act on is the visitor's rather than the proxy's, and
|
||||
`COOKIE_SECURE=auto`.
|
||||
|
||||
Three rules for whatever proxy you use:
|
||||
|
||||
- **Forward only 3000.** `INTERNAL_PORT` (3001) is the server-to-bot channel and must never
|
||||
be reachable from outside; the Compose file deliberately does not publish it.
|
||||
- **Deny `/api/v1/internal` at the proxy** as well. Belt and braces: that route no longer
|
||||
rides the public listener, and an explicit deny costs nothing.
|
||||
- **Terminate TLS at the proxy.** The app speaks HTTP; it is not meant to hold a
|
||||
certificate.
|
||||
|
||||
## Upgrading the shard side
|
||||
|
||||
A different deployment on a different host, and it moves on its own schedule:
|
||||
|
||||
```bash
|
||||
sudo runicgateway update # re-resolves the bundle; --verify to see it first
|
||||
sudo runicgateway doctor # confirm afterwards
|
||||
```
|
||||
|
||||
`update` replaces the sidecar and restarts its service, re-syncs the overlay, and tells you
|
||||
when ServUO needs restarting — it never restarts your shard itself. Because it resolves a
|
||||
**bundle**, the sidecar and the plugin move together and cannot end up disagreeing about the
|
||||
protocol.
|
||||
|
||||
<Aside type="note" title="Update the two sides in either order, but verify after each">
|
||||
They are independent deployments joined by a version-checked contract: a mismatch is
|
||||
rejected with a `409` rather than mis-parsed. So the worst case is a bridge that refuses to
|
||||
pair until both sides are current — visible on
|
||||
[the shard connection screen](/docs/administration/the-shard-connection/), and not silent
|
||||
corruption.
|
||||
</Aside>
|
||||
104
src/content/docs/docs/administration/managing-modules.mdx
Normal file
104
src/content/docs/docs/administration/managing-modules.mdx
Normal file
@@ -0,0 +1,104 @@
|
||||
---
|
||||
title: Managing modules
|
||||
description: The five states a module can be in, installing and upgrading, disable versus uninstall versus purge, and what to do when one fails to start.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Installing your first module is [Getting started](/docs/getting-started/install-a-game-module/).
|
||||
This is what the screen means afterwards.
|
||||
|
||||
## The five states
|
||||
|
||||
`installed → enabled → started`, with `disabled` and `startup_failed` as recoverable
|
||||
states.
|
||||
|
||||
| State | Means |
|
||||
|---|---|
|
||||
| **Installed** | Files are on the volume; it mounts at the next restart |
|
||||
| **Enabled** | Allowed to run, and about to be loaded. Every boot resets each non-disabled module to this, then records the outcome |
|
||||
| **Started** | Running: routes mounted, schema applied |
|
||||
| **Disabled** | An operator switched it off. Its routes answer *not found* |
|
||||
| **Startup failed** | It tried and could not. The site came up without it |
|
||||
|
||||
**A module that fails to load never takes the site down.** Failure is caught across the
|
||||
whole lifecycle — require, schema, routes, registration, boot hook — and the site starts
|
||||
with that module's routes and navigation absent, and the reason recorded on this screen.
|
||||
|
||||
Two consequences of how boots work:
|
||||
|
||||
- **A failed module is retried on every restart.** Fix the underlying cause and restart; you
|
||||
do not need to touch the panel. A deterministically broken module re-records its failure
|
||||
each boot, which is the honest thing for it to do.
|
||||
- **Disabled is the only state a boot leaves alone.** Disabling is an operator's decision
|
||||
rather than an outcome, so it survives restarts untouched.
|
||||
|
||||
## Upgrading
|
||||
|
||||
Paste the new release's install-manifest URL and press Install. The bundle is verified
|
||||
against its `sha256`, unpacked over the old one, and takes effect at the restart.
|
||||
|
||||
An upgrade **deliberately leaves the state alone**: upgrading an enabled module must not
|
||||
silently switch it off, and re-installing a disabled one must not silently switch it on.
|
||||
|
||||
<Aside type="caution" title="Check the Module API version before upgrading">
|
||||
A module declares which core API versions it accepts. If a module release requires a newer
|
||||
core than your image, upgrade the site first — see
|
||||
[Maintenance and upgrades](/docs/administration/maintenance-and-upgrades/).
|
||||
</Aside>
|
||||
|
||||
## Disable, uninstall, purge
|
||||
|
||||
Three different actions, in increasing order of destruction.
|
||||
|
||||
**Disable** flips the row and dispatches that module's shutdown hook, so it actually stops
|
||||
— releases its sockets, closes its streams — rather than merely becoming unreachable. Enable
|
||||
is deliberately not the mirror image: there is no boot hook re-dispatch, so enabling offers
|
||||
a restart.
|
||||
|
||||
**Uninstall** is non-destructive by default: the row goes to `disabled`, the directory is
|
||||
removed, and **the module's tables and data are retained**.
|
||||
|
||||
**Purge** runs the module's own `purge.sql` and destroys its data. It is never implied by
|
||||
an uninstall, and it is offered in two places — as a standalone action on an installed
|
||||
module, and as an opt-in checkbox in the uninstall dialog.
|
||||
|
||||
<Aside type="caution" title="Purge only works while the files are still there">
|
||||
`purge.sql` lives inside the directory an uninstall deletes. Uninstalling without ticking
|
||||
the box keeps the tables, and getting rid of them later means **reinstalling the module
|
||||
first**. Decide at the uninstall, not afterwards.
|
||||
</Aside>
|
||||
|
||||
## Where modules may be installed from
|
||||
|
||||
The allowlist at the bottom of the screen. Installing a module runs its code inside your
|
||||
server, so only listed hosts are permitted, over HTTPS, re-checked on every redirect. An
|
||||
empty list forbids every install.
|
||||
|
||||
`MODULE_SOURCE_HOSTS` seeds this list on a site's first boot and is ignored afterwards —
|
||||
the setting is authoritative, so a redeploy cannot silently undo your choice.
|
||||
|
||||
## The declarative path
|
||||
|
||||
`MODULES` in `.env` declares the set this deployment runs, resolved at every container
|
||||
start, each entry `<id>@<version>=<install manifest URL>`.
|
||||
|
||||
The division of ownership is the thing to remember: **the variable owns what is on the
|
||||
volume; the panel owns whether a module runs.** Uninstall a declared module from the panel
|
||||
and its files come back at the next start — disabled.
|
||||
|
||||
A module already unpacked at the declared version is a no-op that makes **no network call
|
||||
at all**, so a restart with no route to the internet comes up unchanged. A version that
|
||||
cannot be fetched is logged, shown on this screen, and never stops the site starting.
|
||||
|
||||
## Placing one by hand
|
||||
|
||||
Unpacking a module tarball into `./modules/<id>/` and restarting is a supported install —
|
||||
it is why that path is a bind mount rather than a named volume. The row it produces has no
|
||||
provenance columns, because nothing downloaded it.
|
||||
|
||||
<Aside type="note" title="Do not delete the `modules` directory itself">
|
||||
Docker recreates a missing bind-mount source as `root`, and the container user can then no
|
||||
longer write it — which breaks installing from the panel. If that happens,
|
||||
`chown 1000:1000 modules` on the host.
|
||||
</Aside>
|
||||
68
src/content/docs/docs/administration/moderation.mdx
Normal file
68
src/content/docs/docs/administration/moderation.mdx
Normal file
@@ -0,0 +1,68 @@
|
||||
---
|
||||
title: Moderation
|
||||
description: Three screens that do three different jobs — Discord moderation, content reports, and appeals against a sanction.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
The **Moderation** group in the sidebar holds three screens that are easy to confuse and do
|
||||
not overlap.
|
||||
|
||||
| Screen | Is about | Comes from |
|
||||
|---|---|---|
|
||||
| **Moderation** | Your **Discord** guild — bans, kicks, mutes, warnings, joins, leaves, filter and spam hits | the bot, captured live |
|
||||
| **Reports** | **Team forum content** members have reported | the site |
|
||||
| **Appeals** | Sanctions people are asking you to reverse | the site |
|
||||
|
||||
## Moderation (Discord)
|
||||
|
||||
Counts across a window you choose — 24 hours, 7 days, 30 days — for bans, kicks, mutes,
|
||||
warnings, joins, leaves, filter hits and spam hits, with a filterable list of recent
|
||||
actions and a tabbed event log (members, filter hits, spam hits).
|
||||
|
||||
Everything here arrives from the Discord bot, so a site with no bot configured shows zeros
|
||||
and empty lists rather than an error. Setting the bot up is
|
||||
[Notifications and email](/docs/administration/notifications-and-email/).
|
||||
|
||||
**Look up** takes you to a per-user view when you are investigating one account rather than
|
||||
browsing the window.
|
||||
|
||||
## Reports
|
||||
|
||||
Reports raised by members about Team forum content. Two design decisions show through in
|
||||
how this screen behaves:
|
||||
|
||||
- **They come to site staff, and a Team's own leaders never see them.** A leader moderates
|
||||
their own forum, so a report *about a leader* has to reach someone above them.
|
||||
- **Handling a report records a decision about the report.** It does not touch the content:
|
||||
hiding or removing a post is done in the forum, or as a sanction against the account.
|
||||
|
||||
The filters are *Open*, *Reviewing*, *Actioned*, *Dismissed* and *All*, and the count of
|
||||
open reports sits at the top so the screen is glanceable.
|
||||
|
||||
<Aside type="note" title="Dismissing is a real outcome, not a failure to act">
|
||||
A report that was not a problem should be dismissed rather than left open — an open queue
|
||||
that never empties stops being read, and the reporter's next report is the one that
|
||||
matters.
|
||||
</Aside>
|
||||
|
||||
## Appeals
|
||||
|
||||
An appeal is a request to reverse a sanction, filtered by *Open*, *Pending*, *Under
|
||||
review*, *Approved*, *Denied*, *Withdrawn* or *All*. Each row carries the target, the
|
||||
action being appealed, the appeal itself, who submitted it, its age and whether a reversal
|
||||
happened.
|
||||
|
||||
Two things worth building a habit around:
|
||||
|
||||
- **Age is the column that matters.** An appeal that nobody has looked at for three weeks
|
||||
is a worse outcome than a denial.
|
||||
- **The decision is recorded either way.** Approving an appeal records the reversal, so the
|
||||
history explains itself later without anyone having to remember.
|
||||
|
||||
## What is recorded, and where
|
||||
|
||||
Every staff action lands in **Admin → Activity** — who did what, from which address, when.
|
||||
That log is the thing to read when reconstructing a disputed decision, and it is a record
|
||||
rather than a workflow: nothing is actioned from it. See
|
||||
[Content](/docs/administration/content/).
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
title: Navigation and pages
|
||||
description: Renaming, reordering and hiding navigation entries in three navs, and composing standalone pages from blocks.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
## Navigation
|
||||
|
||||
**Admin → Navigation** edits three separate navigations — **Public site**, **Admin** and
|
||||
**Player portal** — each with the same tools: rename an entry, reorder it, hide it, group
|
||||
entries into a dropdown section, or add a link of your own.
|
||||
|
||||
A fresh site's public nav is the seeded one: the portal, News, Screenshots, Five on Friday,
|
||||
Newsletter, the wiki, and About. Until you change anything, the nav "renders exactly as
|
||||
coded" — there is no stored copy to drift from the code.
|
||||
|
||||
Two properties are worth understanding before you rely on this screen.
|
||||
|
||||
**It advertises; it does not authorise.** Renaming or hiding an entry changes what is
|
||||
listed, never what exists or who may reach it. Hiding *Wiki* does not close the wiki. Access
|
||||
is decided by roles and by a module's visibility settings, and this screen "can never show
|
||||
anyone a link their role, or the visibility settings of an installed module, would hide".
|
||||
|
||||
**You only edit what you can see.** Entries hidden from *you* — by your role, or by a
|
||||
module's visibility rules — are not listed, and they keep whatever setting they already
|
||||
had. So an administrator's view of this screen is not necessarily the whole nav, and
|
||||
editing it cannot damage the parts you cannot see.
|
||||
|
||||
<Aside type="note" title="A module's pages appear here like anything else">
|
||||
An installed module adds its own entries, and they can be renamed, reordered, grouped and
|
||||
hidden exactly like core's. What you cannot do is *reach past* the module's own visibility
|
||||
settings — those are set with the module, not here.
|
||||
</Aside>
|
||||
|
||||
**Reset to default** discards your customisation for that nav and goes back to the coded
|
||||
one. It is per-nav, not global.
|
||||
|
||||
## Pages
|
||||
|
||||
**Admin → Pages** composes standalone pages from blocks. A published page is live at its
|
||||
slug — `/about`, `/rules`, `/donate` — and a draft is visible only to staff.
|
||||
|
||||
This is the right tool for content that is not news and not a wiki article: the pages a
|
||||
navigation entry points at. A page you create is not linked from anywhere until you add it
|
||||
in **Navigation** — deliberately, because the two are separate decisions.
|
||||
|
||||
For everything else — news posts, the newsletter, screenshots, the wiki — see
|
||||
[Content](/docs/administration/content/).
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
title: Notifications and email
|
||||
description: Email over Gmail OAuth2, the announcement pipeline and its legs, the Discord bot, and opt-in push to the mobile app.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Four separate delivery paths, each optional, each off until you configure it. A site that
|
||||
configures none of them still works — it just never reaches anyone who is not looking at
|
||||
it.
|
||||
|
||||
## Email
|
||||
|
||||
**Admin → Settings → Email delivery.** The site sends contact-form messages (and test
|
||||
messages) through **Gmail over OAuth2**, delivered to the *Contact email* setting.
|
||||
|
||||
It reuses the **Google authentication client**, so the order is fixed: configure Google on
|
||||
the [Authentication](/docs/administration/authentication/) page first, then press **Connect
|
||||
Gmail** here. Until then the panel reads *Unconfigured* and says exactly that.
|
||||
|
||||
The refresh token it stores is encrypted at rest like every other secret.
|
||||
|
||||
<Aside type="note" title="There is no SMTP option">
|
||||
Gmail over OAuth2 is the only supported delivery path today. Until it is connected, the
|
||||
contact form falls back to a `mailto:` link to the contact address — which works, and puts
|
||||
the message in the visitor's own mail client rather than in your logs.
|
||||
</Aside>
|
||||
|
||||
## Announcements
|
||||
|
||||
Publishing a **news** post fans it out to every registered delivery leg. The dispatcher is
|
||||
an in-process poller, tuned by `ANNOUNCE_POLL_MS` (15 seconds by default), and the links in
|
||||
an announcement are built from `APP_BASE_URL` — so set that in production or the links point
|
||||
at the wrong host.
|
||||
|
||||
Which legs exist depends on what has registered one:
|
||||
|
||||
- **Discord `#news`** is core's, and needs the bot below.
|
||||
- **A module may add its own.** The `uo` module adds an in-game town crier, so a news post
|
||||
is announced to players who are logged into the game and never visit the site.
|
||||
|
||||
A leg brings its own settings with it — the town crier's duration is a module setting, not
|
||||
a core one — which is why they are documented with the module rather than here.
|
||||
|
||||
## The Discord bot
|
||||
|
||||
**Admin → Discord Bot**: enable it, give it the guild (server) ID and the bot token, and
|
||||
save. The token is stored **encrypted in the database** and is never an environment
|
||||
variable.
|
||||
|
||||
The bot is a separate container. On the quickstart deployment from
|
||||
[Install the site](/docs/getting-started/install-the-site/) it is not running at all, and
|
||||
the panel says so — *bot unreachable* is the honest state of a site that never started one,
|
||||
not a failure. Add the `bot` service from the project's shipped Compose file when you want
|
||||
it.
|
||||
|
||||
What it does once connected: posts announcements, captures the moderation events on the
|
||||
[Moderation](/docs/administration/moderation/) screen, serves slash commands, and — if you
|
||||
switch them on — the Team notification bridge and per-Team voice channels from
|
||||
[Teams](/docs/administration/teams/).
|
||||
|
||||
## Push notifications
|
||||
|
||||
Opt-in push to the Android app, over a **self-hosted ntfy relay** — the `ntfy` service in
|
||||
the project's Compose file, plus `NTFY_BASE_URL` and friends.
|
||||
|
||||
Two properties matter for what you have to trust:
|
||||
|
||||
- **The relay only ever carries a content-free tickle.** The message says something
|
||||
happened; the app then fetches the actual content from the site over its own
|
||||
authenticated connection. So the relay never sees notification text.
|
||||
- **A device may only register an endpoint on an allowed origin**, derived from
|
||||
`NTFY_BASE_URL`. That is what stops a device pointing your server at somebody else's.
|
||||
|
||||
Without `NTFY_PUBLIC_URL` / `NTFY_ALLOWED_ORIGINS`, the app simply shows push as
|
||||
unavailable for your instance — nothing breaks.
|
||||
|
||||
## Who receives what
|
||||
|
||||
The per-person side of this lives in the player portal, not the admin panel: each member
|
||||
chooses which Team and forum notifications they want, and how. Two defaults are worth
|
||||
knowing because they are not symmetrical:
|
||||
|
||||
- **Push is opt-out** once a device is registered.
|
||||
- **Email is opt-in.**
|
||||
|
||||
<Aside type="caution" title="Nothing here retries">
|
||||
The announcement dispatcher sends once, and the Team notification bridge states plainly that
|
||||
a message is sent once and not retried. If Discord is down when a post is published, that
|
||||
announcement is gone — the post is still on the site, which is the thing that matters.
|
||||
</Aside>
|
||||
77
src/content/docs/docs/administration/teams.mdx
Normal file
77
src/content/docs/docs/administration/teams.mdx
Normal file
@@ -0,0 +1,77 @@
|
||||
---
|
||||
title: Teams
|
||||
description: Core owns the Team machinery and cannot create a Team. What that means in practice, and what the admin screen controls.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Teams are a core platform primitive: membership, roles, forums, notifications, moderation
|
||||
and the Discord integrations are all core's, and none of it knows what a Team *is* in your
|
||||
game.
|
||||
|
||||
**Core cannot create a Team.** Teams arrive from the installed module — with the `uo`
|
||||
module, they are the shard's guilds. On a deployment with no module, the Team machinery is
|
||||
present and permanently empty. That is not a bug to work around; it is the contract that
|
||||
lets the same forum, notification and moderation code serve any game.
|
||||
|
||||
<Aside type="note" title="What that means when you are looking at an empty screen">
|
||||
*No Teams in the projection yet* on a site with no module installed is the correct and
|
||||
final state. Install a module, connect its game server, and Teams appear as that module
|
||||
reconciles them.
|
||||
</Aside>
|
||||
|
||||
## The projection, and why it can be stale
|
||||
|
||||
**Admin → Teams** shows a sync panel per module: last attempt, last success, consecutive
|
||||
failures and the last error, with **Sync now** and **Resync now**.
|
||||
|
||||
The wording on that panel is exact and worth reading:
|
||||
|
||||
> Core has never had an answer it could trust. What is shown below is not a confirmed empty
|
||||
> shard.
|
||||
|
||||
An empty list therefore means one of two very different things — there are no Teams, or
|
||||
nobody could ask. The panel tells you which, and a *last success: never* with a *last
|
||||
error* of `no uo-link configured` is the second. Fix
|
||||
[the shard connection](/docs/administration/the-shard-connection/) and sync again.
|
||||
|
||||
## Forums
|
||||
|
||||
Team forums are switched on site-wide in **Settings**, along with whether images are
|
||||
allowed and how long an author may edit a post — see
|
||||
[Configuration](/docs/administration/configuration/).
|
||||
|
||||
Two rules are structural rather than settings:
|
||||
|
||||
- **A Team's leaders moderate their own forum.** That is the point of a Team forum.
|
||||
- **Reports about that forum do not go to them.** They go to site staff, because a report
|
||||
about a leader has to reach someone above them. See
|
||||
[Moderation](/docs/administration/moderation/).
|
||||
|
||||
## The Discord bridges
|
||||
|
||||
Two integrations, both optional, both configured from **Admin → Teams**.
|
||||
|
||||
**Notification bridge** — sends Team notifications to a Discord channel: a default for
|
||||
every Team, overridable per Team. A message is sent once and never retried; the bridge is a
|
||||
courtesy, and nothing on the site depends on it arriving. With nothing configured, no Team
|
||||
event leaves the site.
|
||||
|
||||
**Voice channels** — gives each Team a Discord voice channel of its own, with access
|
||||
granted by a per-Team role, so a Team's members can see and join theirs and nobody else
|
||||
can. It needs the bot reachable, and members need a linked Discord account and guild
|
||||
membership.
|
||||
|
||||
Its three settings deserve a thought each:
|
||||
|
||||
| Setting | What it decides |
|
||||
|---|---|
|
||||
| **Minimum members** | How large a Team must be to get a channel. Every active member counts, linked account or not. |
|
||||
| **Grace window (days)** | How long a Team keeps its channel after it stops qualifying. A Team that recovers inside the window keeps the same channel; zero removes it on the next pass. |
|
||||
| **Staff roles** | Roles that can see and join every Team's channel. Guild administrators already can, so this is for staff who are not administrators. |
|
||||
|
||||
<Aside type="caution" title="Voice channels are a per-guild ceiling, not a per-Team one">
|
||||
Discord's role and channel limits apply to the whole guild, so a site with many small Teams
|
||||
can exhaust them. The minimum-members setting is the lever that keeps the count sane, and
|
||||
it is easier to raise it before provisioning than to unpick channels afterwards.
|
||||
</Aside>
|
||||
@@ -0,0 +1,98 @@
|
||||
---
|
||||
title: The shard connection
|
||||
description: The module's shard screen — connection settings, what the status line means, game-account creation, the town crier, and what reaches the public.
|
||||
---
|
||||
|
||||
import platform from '../../../../data/platform.json';
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
With the `uo` module installed, **Shard (uo-link)** appears in the admin sidebar at
|
||||
`/admin/uo/link`. It is the site's half of the bridge: the connection to the sidecar, and
|
||||
the controls that ride on it.
|
||||
|
||||
Setting it up for the first time is
|
||||
[Connect a game server](/docs/getting-started/connect-a-game-server/).
|
||||
|
||||
## Connection
|
||||
|
||||
Four fields, all four printed by the installer, plus the switch that turns the integration
|
||||
on:
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| **Base URL (REST)** | `http://<shard host>:8080` — point-in-time queries |
|
||||
| **WebSocket URL (feed)** | `ws://<shard host>:8080/ws` — the live event feed |
|
||||
| **Auth token** | The sidecar's token |
|
||||
| **Protocol** | {platform.protocol} today |
|
||||
|
||||
Saving restarts the ingest client, so a change takes effect immediately.
|
||||
|
||||
**The token is write-only.** It is encrypted at rest and never returned to any client, so
|
||||
the field is blank when you come back to the screen — losing it means reading it back from
|
||||
`sidecar.toml` on the shard host, not from the website.
|
||||
|
||||
## Reading the status line
|
||||
|
||||
The header carries the connection state, *Shard link*, *WS ingest*, *Reconnects* and *SSE
|
||||
clients*. Together they say **which** link is broken:
|
||||
|
||||
| Reading | Means |
|
||||
|---|---|
|
||||
| Disconnected, shard link down | The site cannot reach the sidecar at all — URL, firewall, or the service is not running |
|
||||
| Connected, but shard link down | The sidecar is up and the *game* is not talking to it |
|
||||
| Reconnects climbing | An unstable path between site and sidecar |
|
||||
| Live feed silent, everything else green | The bridge is fine and the shard is quiet |
|
||||
|
||||
A `409` in the logs is a protocol mismatch — set the Protocol field to what the sidecar's
|
||||
`/health` reports rather than guessing; it rejects rather than mis-parsing. A `401` is the
|
||||
token.
|
||||
|
||||
<Aside type="note" title="The site is designed to look normal while this is broken">
|
||||
Every read through the sidecar returns a result rather than throwing, so the public site
|
||||
renders with the shard shown offline. That is deliberate graceful degradation, and it is
|
||||
also why a broken bridge can go unnoticed — this screen, or `runicgateway doctor` on the
|
||||
shard host, is how you find out.
|
||||
</Aside>
|
||||
|
||||
## Game-account creation
|
||||
|
||||
Whether players can create a **game** account (for the game client) from the website. The
|
||||
game server's own `SignupMode` in `Bridge.cfg` has to agree.
|
||||
|
||||
| Mode | Behaviour |
|
||||
|---|---|
|
||||
| **Disabled** | Players may only link an account that already exists |
|
||||
| **Website** | The site creates game accounts |
|
||||
| **Hybrid** | Site or in-game — the recommended setting |
|
||||
| **Game only** | Created in the game client; the site only links |
|
||||
|
||||
With creation enabled, a *Create a game account* form appears in the player portal and
|
||||
after an invite is accepted.
|
||||
|
||||
## Town crier
|
||||
|
||||
Broadcast a message every in-game town crier announces until it expires: an id, one or more
|
||||
lines, and a duration in seconds. Re-posting the same id **replaces** that message, and
|
||||
**Remove by id** takes it down early.
|
||||
|
||||
The id is the useful part — give a recurring announcement a stable one and you can update or
|
||||
withdraw it without waiting for it to expire.
|
||||
|
||||
## What reaches the public
|
||||
|
||||
Events from the shard fan out over two separate streams, and the split is a security
|
||||
boundary rather than a preference:
|
||||
|
||||
- **The public stream** carries an allowlist of event kinds.
|
||||
- **The admin stream** adds staff audit events, cheat detection, login attempts and IP
|
||||
addresses.
|
||||
|
||||
The live feed at the bottom of this screen is the admin one — everything, as it arrives.
|
||||
Treat it accordingly: it is the screen you do not put in a screenshot.
|
||||
|
||||
<Aside type="caution" title="Visibility is decided on the website, not on the sidecar">
|
||||
The sidecar is a dumb forwarder. What is public, what is staff-only and what is off is
|
||||
decided on the site, so changing your mind is a settings change rather than a shard
|
||||
redeploy — and it also means an unreviewed default is a decision you have made by not
|
||||
making it.
|
||||
</Aside>
|
||||
118
src/content/docs/docs/administration/troubleshooting.mdx
Normal file
118
src/content/docs/docs/administration/troubleshooting.mdx
Normal file
@@ -0,0 +1,118 @@
|
||||
---
|
||||
title: Troubleshooting
|
||||
description: The failures a deployment actually hits, what each one looks like, and the fix.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Grouped by where the problem is, because the first useful question is always *which half is
|
||||
broken*.
|
||||
|
||||
## The site will not start
|
||||
|
||||
Read the log first — `docker compose logs app` — because the server says exactly why.
|
||||
|
||||
| What the log says | What it means |
|
||||
|---|---|
|
||||
| `SECRET_ENC_KEY must be set in production` | The key that encrypts stored secrets is missing. Set it in `.env` and start again. The container crash-loops until you do. |
|
||||
| A `BOT_INTERNAL_KEY` complaint | Blank, still a placeholder, or shorter than 16 characters. Required in production even when the bot is not running. |
|
||||
| A database connection error, repeatedly | The app came up before the database was ready, or `DB_*` is wrong. The Compose file's health check handles the first case; check the values for the second. |
|
||||
| Nothing at all, container restarting | The image did not pull. `docker compose pull` on its own shows the error. |
|
||||
|
||||
<Aside type="caution" title="Both of those key errors happen on the FIRST boot, not later">
|
||||
They are checked at require time, before the server listens. A deployment that has ever
|
||||
served a request has both of them set.
|
||||
</Aside>
|
||||
|
||||
## Nobody can sign in
|
||||
|
||||
- **Your address is rate-limited or bot-banned.** Both are working as designed. Check
|
||||
**Admin → Web Bot Activity** from another network, or restart the app container — the
|
||||
scoring state is in memory and resets with it.
|
||||
- **The password is right and the form still fails.** Check the log for the actual status:
|
||||
a `429` is the rate limiter, a `403` is usually the honeypot, and a `401` really is the
|
||||
password.
|
||||
- **SSO returns to the login page.** SSO is link-only: an identity that is not already
|
||||
linked to an account cannot sign in, and that is the expected outcome rather than a
|
||||
misconfiguration. Link it from the account screen first.
|
||||
- **Two-factor is lost.** Recovery codes, or another admin's **Reset two-factor** on
|
||||
**Users → View**. See [Authentication](/docs/administration/authentication/).
|
||||
|
||||
## A module will not start
|
||||
|
||||
**Admin → Modules** names the stage and the reason. The usual three:
|
||||
|
||||
| Reason | Fix |
|
||||
|---|---|
|
||||
| `module directory not present on the volume` | The row exists and the files do not — someone deleted the directory by hand. Reinstall, or remove the row with an uninstall. |
|
||||
| A schema failure | The module's schema fragment could not be applied. The log carries the SQL error. |
|
||||
| A version refusal | The module wants a newer core API than this image. Upgrade the site. |
|
||||
|
||||
Whatever the reason, **the site is up and the module's routes are absent** — that is by
|
||||
design, and it is why a broken module is an inconvenience rather than an outage. Fix the
|
||||
cause and restart: failed modules are retried on every boot.
|
||||
|
||||
**The install button rejects a URL.** The host must be in the allowlist on the same screen,
|
||||
and the URL must be HTTPS. An empty allowlist forbids every install.
|
||||
|
||||
**The install succeeds and nothing appears.** It needs a restart. The banner says so, and
|
||||
the row reads *Restart to start* until then.
|
||||
|
||||
## The Restart button did not bring the site back
|
||||
|
||||
The button exits the process and relies on a supervisor to start it again. If your
|
||||
deployment has nothing supervising it — `npm start` in a terminal, a container without
|
||||
`restart:` — the site stays down until you start it yourself. Compose with
|
||||
`restart: unless-stopped` is the supported shape.
|
||||
|
||||
## The game screens are empty or say offline
|
||||
|
||||
Work outwards from the game, and stop at the first check that fails.
|
||||
|
||||
1. **In game:** `[bridge status` — `connected=False` means the shard cannot reach the
|
||||
sidecar.
|
||||
2. **On the shard host:** `curl -s http://127.0.0.1:8080/health` — `plugin_connected: true`
|
||||
is the value that matters.
|
||||
3. **On the shard host:** `runicgateway doctor` — checks the install record, every overlay
|
||||
file hash, the service, and that the sidecar and overlay agree on a protocol.
|
||||
4. **On the site:** the [shard connection screen](/docs/administration/the-shard-connection/)
|
||||
— its four indicators say which link is broken.
|
||||
|
||||
Two log lines with specific meanings: **`409`** is a protocol mismatch (set the Protocol
|
||||
field to what `/health` reports), and **`401`** is the auth token (read the live one back
|
||||
with `uo-link-sidecar --print-config`; do not retype it from a screenshot).
|
||||
|
||||
<Aside type="note" title="“Nothing changed and it stopped working” usually means a ServUO update">
|
||||
An update to the server tree can revert `Scripts.csproj`, at which point the plugin sits in
|
||||
the tree and never compiles — and ServUO ignores the script build's exit code, so the boot
|
||||
looks clean. `doctor` catches it by comparing file hashes against the install record.
|
||||
</Aside>
|
||||
|
||||
## Teams are missing
|
||||
|
||||
Check the sync panel on **Admin → Teams** before anything else: *last success: never* with
|
||||
`no uo-link configured` means the shard connection, not the Team machinery. And on a site
|
||||
with **no module installed**, an empty Team list is correct and final — core cannot create
|
||||
a Team. See [Teams](/docs/administration/teams/).
|
||||
|
||||
## Email and announcements never arrive
|
||||
|
||||
- **The contact form opens a mail client.** Email delivery is not connected; that is the
|
||||
documented fallback. Connect Gmail in **Settings → Email delivery** — after configuring
|
||||
the Google provider, which it reuses.
|
||||
- **A published post announced nothing.** The Discord bot is a separate container. If the
|
||||
Discord Bot screen says *bot unreachable*, it is not running.
|
||||
- **A missed announcement does not come back.** Nothing retries; the post itself is still
|
||||
on the site.
|
||||
|
||||
## Uploads and modules fail with permission errors
|
||||
|
||||
Docker created a bind-mount source that the container user cannot write — usually because
|
||||
the directory was deleted and recreated by Docker as `root`. `chown 1000:1000 modules` (or
|
||||
`logs`, or `brand`) on the host fixes it. Do not delete those directories.
|
||||
|
||||
## When you need to ask for help
|
||||
|
||||
Bring three things: the relevant lines from `docker compose logs app`, the output of
|
||||
`runicgateway doctor` if a game server is involved, and what you changed last. The
|
||||
[community page](/community/) has where to ask.
|
||||
67
src/content/docs/docs/administration/users-and-roles.mdx
Normal file
67
src/content/docs/docs/administration/users-and-roles.mdx
Normal file
@@ -0,0 +1,67 @@
|
||||
---
|
||||
title: Users and roles
|
||||
description: The four roles and what each one reaches, creating accounts, and inviting people to a site that is not open for registration.
|
||||
---
|
||||
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
## The four roles
|
||||
|
||||
| Role | Reaches |
|
||||
|---|---|
|
||||
| **Player** | The player portal: their own profile, their own characters and game account links, their Teams, forum access, notification preferences |
|
||||
| **Moderator** | Everything a player has, plus Moderation, Appeals, Reports and the Teams admin screen |
|
||||
| **Editor** | Everything a player has, plus Posts, Pages, Wiki and the Activity log |
|
||||
| **Admin** | All of it, including Users, Invites, Settings, Modules, Appearance, Navigation, Authentication and the module's own admin screens |
|
||||
|
||||
<Aside type="note" title="Staff are players too">
|
||||
Every self-service screen in the player portal is role-agnostic: it serves whoever is signed
|
||||
in. An administrator has characters and Teams like anyone else, and reaches them through the
|
||||
same portal. Nothing about being staff removes the player half of an account.
|
||||
</Aside>
|
||||
|
||||
Admin routes are re-validated against the database on **every request**, not just at sign-in.
|
||||
Demoting an account takes effect at once — the open session does not keep its access until
|
||||
it expires.
|
||||
|
||||
## Creating an account
|
||||
|
||||
**Admin → Users → + Add user** creates one directly: username, password, role, and it is
|
||||
active immediately. That is the right path for staff, and for the handful of accounts you
|
||||
create yourself.
|
||||
|
||||
The list shows each account's role, status and last login, with **View** and **Edit** on
|
||||
every row.
|
||||
|
||||
## Invites
|
||||
|
||||
**Admin → Invites** is the way to let a specific person in when self-registration is off —
|
||||
which is how every deployment starts.
|
||||
|
||||
Enter an email, pick the access level (player, moderator, editor or admin), and either
|
||||
**create and email** the invitation or generate a link to share yourself. The table tracks
|
||||
status, expiry and creation date, so an unaccepted invite is visible rather than forgotten.
|
||||
|
||||
This is worth preferring over creating accounts by hand for real people: the recipient sets
|
||||
their own password, and you never handle it.
|
||||
|
||||
## Opening registration
|
||||
|
||||
When you do want a public sign-up, that is **Settings → Player registration**: password,
|
||||
SSO, or both. See [Configuration](/docs/administration/configuration/).
|
||||
|
||||
Before opening it, know what is protecting the door: rate limiting, login backoff, a
|
||||
honeypot, bot scoring and automatic IP bans — all covered in
|
||||
[Authentication](/docs/administration/authentication/), along with two-factor and the SSO
|
||||
policy that an external identity can only ever sign in to an account it is already linked
|
||||
to.
|
||||
|
||||
## Status, and why deleting is the last resort
|
||||
|
||||
Editing an account sets its **status** as well as its role: *active*, *disabled*, *banned*
|
||||
or *pending*. Disabled and banned both stop the account being used; the difference is what
|
||||
you are recording — an account switched off versus an account sanctioned.
|
||||
|
||||
Prefer either to the **Delete** button. Content, moderation history and Team membership all
|
||||
reference the account, and a disabled one keeps those records readable while a deleted one
|
||||
leaves the history to explain itself.
|
||||
133
src/content/docs/docs/getting-started/connect-a-game-server.mdx
Normal file
133
src/content/docs/docs/getting-started/connect-a-game-server.mdx
Normal file
@@ -0,0 +1,133 @@
|
||||
---
|
||||
title: Connect a game server
|
||||
description: The installer binary on the shard host — what it deploys, what it asks, and the four values it prints for the website.
|
||||
---
|
||||
|
||||
import platform from '../../../../data/platform.json';
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
This is the second of the two installs, and it happens on the machine that runs your game
|
||||
server. One binary deploys the plugin, installs the sidecar, registers its service, and
|
||||
prints four values for you to paste into the website.
|
||||
|
||||
It never contacts your website, and it never starts or stops your shard.
|
||||
|
||||
## What gets deployed
|
||||
|
||||
| # | Component | Where it goes |
|
||||
|---|---|---|
|
||||
| 1 | **The plugin overlay** — C# source ServUO compiles at boot | into your ServUO tree |
|
||||
| 2 | **The uo-link sidecar** — a small Rust service | a system directory, plus a service |
|
||||
| 3 | **A record of the run** | `install.json`, with per-file hashes and backups |
|
||||
|
||||
```
|
||||
ServUO shard ──loopback TCP 127.0.0.1:7788──► uo-link sidecar ──HTTP + WebSocket──► website
|
||||
```
|
||||
|
||||
The shard **dials out**. It never listens for the website and is never reachable from the
|
||||
internet; only the sidecar is exposed, and only to your site.
|
||||
|
||||
## Install
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Download the binary for your OS, and `SHA256SUMS`**, from the
|
||||
[installer releases page](https://gitea.whitlocktech.com/RunicGateway/installer/releases)
|
||||
({platform.releases.installer}).
|
||||
|
||||
Releases are **unsigned** — there is no code-signing certificate, so that checksum file
|
||||
is the whole trust anchor. Check it:
|
||||
|
||||
```bash
|
||||
sha256sum -c SHA256SUMS --ignore-missing
|
||||
chmod +x runicgateway-installer-linux-x86_64
|
||||
```
|
||||
|
||||
On Windows, `(Get-FileHash .\runicgateway-installer-windows-x86_64.exe -Algorithm SHA256).Hash`
|
||||
and compare. Windows will also show a SmartScreen prompt on first run, for the same
|
||||
reason.
|
||||
|
||||
2. **Stop the shard.** `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit, so
|
||||
the installer refuses to deploy under a running server.
|
||||
|
||||
3. **Run it, elevated.**
|
||||
|
||||
```bash
|
||||
sudo ./runicgateway-installer-linux-x86_64 install
|
||||
```
|
||||
|
||||
Add `--verify` first if you want to see every change it would make and write nothing.
|
||||
|
||||
It asks four things: your ServUO root, whether to apply the optional patch tier, the
|
||||
hostname your website should use to reach this machine, and your site's URL (used only
|
||||
to print a link at the end).
|
||||
|
||||
4. **Read the summary.** It reports the overlay sync file by file, the sidecar binary and
|
||||
its verified hash, the config and database paths, and the service state. Then it says
|
||||
what you must do next — restart ServUO yourself, because it will not do that for you.
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="note" title="It installs a bundle, not “latest of each”">
|
||||
The three components version independently but must agree on one wire protocol, so what it
|
||||
resolves is a **bundle**: an exact, protocol-checked pair of sidecar and overlay versions
|
||||
({platform.bundle.tag} today — sidecar {platform.bundle.sidecar}, overlay {platform.bundle.overlay}).
|
||||
`--bundle <tag>` pins an exact past combination, so a reinstall in six months reproduces
|
||||
today's install rather than tomorrow's.
|
||||
</Aside>
|
||||
|
||||
## The patch tier is optional
|
||||
|
||||
Most of the plugin is *added* files, which is why the base install is a safe copy. Two
|
||||
features need edits to stock ServUO sources, and those are opt-in, off unless you say yes,
|
||||
and refused where the target lines are not stock. Skipping the tier costs you vendor-sale
|
||||
events and in-game moderation audit forwarding; everything else works.
|
||||
|
||||
The tier is written and tested against stock ServUO {platform.bundle.servuoMin}. On any
|
||||
other version it is unsupported and untested, and the prompt makes you answer past a
|
||||
warning.
|
||||
|
||||
## Paste the four values into the site
|
||||
|
||||
A successful run ends by printing the one step it cannot do for you:
|
||||
|
||||
```
|
||||
Base URL http://shard.example.com:8080
|
||||
WebSocket URL ws://shard.example.com:8080/ws
|
||||
Protocol version 4
|
||||
Auth token 4f9c… (also in sidecar.toml)
|
||||
```
|
||||
|
||||
Every value comes from asking the installed sidecar itself, so it cannot drift from what
|
||||
the service actually runs.
|
||||
|
||||
On the site, sign in as an administrator and open **Shard (uo-link)** in the admin
|
||||
sidebar — `/admin/uo/link`. Tick *Enable the shard integration*, paste **Base URL**,
|
||||
**WebSocket URL**, **Auth token** and **Protocol**, and save. The ingest client restarts
|
||||
immediately.
|
||||
|
||||
<Aside type="caution" title="The installer prints an older path for that screen">
|
||||
It prints `…/admin/shard`. Since the shard screens became part of the `uo` module, a module
|
||||
owns one path segment and the screen moved to **`/admin/uo/link`**. Use the sidebar, or
|
||||
that path; the printed link no longer resolves.
|
||||
</Aside>
|
||||
|
||||
The token is encrypted at rest and **never returned to any client** — losing it means
|
||||
reading it back from `sidecar.toml` on the shard host, not from the website.
|
||||
|
||||
## If the website is on a different machine
|
||||
|
||||
The sidecar binds `127.0.0.1:8080`, reachable only from the shard host. If the site runs
|
||||
elsewhere, widen the bind and then narrow the access:
|
||||
|
||||
1. Set `[web] bind` in `sidecar.toml` to `0.0.0.0:8080` and restart the service.
|
||||
2. **Firewall that port to your website's address only.** The auth token is always on, but
|
||||
it travels as a plain bearer token — the sidecar speaks HTTP, not HTTPS.
|
||||
3. If the two hosts are not on a trusted network, put the sidecar behind a TLS reverse
|
||||
proxy or a VPN link and give the website the `https://` / `wss://` URLs.
|
||||
|
||||
Leave `[shard] bind` on `127.0.0.1:7788`. That socket accepts *inbound commands to the
|
||||
game*, and being loopback-only is what makes that safe.
|
||||
|
||||
Next: [Verify the whole stack](/docs/getting-started/verify-the-whole-stack/) — because a
|
||||
successful file copy is not a working bridge.
|
||||
78
src/content/docs/docs/getting-started/first-run.mdx
Normal file
78
src/content/docs/docs/getting-started/first-run.mdx
Normal file
@@ -0,0 +1,78 @@
|
||||
---
|
||||
title: First run
|
||||
description: Signing in as the first admin, what the site does before anyone visits, and the switch from maintenance to live.
|
||||
---
|
||||
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
The site is up and nobody can see it yet. That is the intended state: a new deployment
|
||||
**starts in maintenance mode**, showing visitors a "coming soon" page while the admin panel
|
||||
stays reachable.
|
||||
|
||||
## Sign in
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Open `/admin/login`** — not `/`. The public site and the admin panel have separate
|
||||
sign-in screens, and in maintenance mode the public one is behind the coming-soon page.
|
||||
|
||||
2. **Use `ADMIN_USERNAME` and `ADMIN_PASSWORD` from your `.env`.**
|
||||
|
||||
That account was created on the first boot, and only because the `users` table was
|
||||
empty. The variables do nothing on later boots, so you can blank them once you are in.
|
||||
|
||||
3. **Set up two-factor**, under **Account** at the bottom of the sidebar. Optional,
|
||||
per-account, and the right moment is now rather than after the site is public.
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="caution" title="If the login screen rejects a password you are sure about">
|
||||
Login is rate-limited and backs off after repeated failures from one address, and the
|
||||
bot-scoring layer can ban an address outright. Both are working as designed. Give it a
|
||||
minute, and see [Authentication](/docs/administration/authentication/) for what the
|
||||
**Web Bot Activity** screen shows and how to lift a ban.
|
||||
</Aside>
|
||||
|
||||
## What is already there
|
||||
|
||||
The first boot seeds a working site rather than an empty one:
|
||||
|
||||
- **A wiki with eight pages**, arranged in sections — Guides, World & Lore, Systems &
|
||||
Gameplay, Community & Rules — as a skeleton to write into, not as content to keep.
|
||||
- **Post categories**: News, Five on Friday, Newsletter, Screenshots.
|
||||
- **A public navigation** covering those, the wiki and an About page.
|
||||
- **A portal hero** with placeholder copy that names no game.
|
||||
|
||||
None of it mentions a specific game, because core does not know about one. That arrives
|
||||
with a [module](/docs/getting-started/install-a-game-module/).
|
||||
|
||||
## The three things to set before going live
|
||||
|
||||
All three are on **Settings**:
|
||||
|
||||
| Setting | Why now |
|
||||
|---|---|
|
||||
| **Site title** | Overrides `BRAND_NAME` for the page title, the header and link previews. |
|
||||
| **Contact email** | Where the contact form delivers. Until email is configured, the form falls back to a `mailto:` link to this address — so an unset one means a contact form that goes nowhere. |
|
||||
| **Player registration** | **Off by default**: nobody can create an account. Choose password, SSO, both, or leave it off and invite people individually from **Invites**. |
|
||||
|
||||
The maintenance message and the homepage teaser are on the same screen, and both are worth
|
||||
a minute before anyone reads them.
|
||||
|
||||
## Switch to live
|
||||
|
||||
**Dashboard → Switch to Live.** The public site opens immediately; nothing else changes.
|
||||
|
||||
You can flip back at any time, and an admin who is signed in can preview the live site
|
||||
while the rest of the world still sees the maintenance page — so there is no need to go
|
||||
live in order to check your work.
|
||||
|
||||
<Aside type="note" title="Going live is not the same as being reachable">
|
||||
Live mode only decides what visitors are shown. Whether anyone can reach the site at all is
|
||||
your DNS, TLS and reverse proxy — see
|
||||
[Maintenance and upgrades](/docs/administration/maintenance-and-upgrades/).
|
||||
</Aside>
|
||||
|
||||
Next: [Install a game module](/docs/getting-started/install-a-game-module/), or skip
|
||||
straight to [Administration](/docs/administration/configuration/) if this deployment is a
|
||||
community site with no game server behind it.
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
title: Install a game module
|
||||
description: Everything game-specific is a module. Installing one, what it adds, and the restart that makes it live.
|
||||
---
|
||||
|
||||
import platform from '../../../../data/platform.json';
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
Core knows nothing about any game. Every game-specific screen — shard status, the map
|
||||
atlas, the player marketplace, character sheets — comes from a **module**, a directory on a
|
||||
mounted volume that the server loads at start.
|
||||
|
||||
Today there is one: **`uo`**, for ServUO shards, published as
|
||||
[`Module-uo`](https://gitea.whitlocktech.com/RunicGateway/Module-uo) ({platform.releases['Module-uo']}).
|
||||
|
||||
## Install it
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Open Admin → Modules.**
|
||||
|
||||
2. **Paste the URL of a release's install manifest** into *Release install-manifest URL*
|
||||
and press **Install**.
|
||||
|
||||
For the current `uo` release that is the `module-uo-<version>.json` asset on
|
||||
[its releases page](https://gitea.whitlocktech.com/RunicGateway/Module-uo/releases).
|
||||
The site downloads the bundle, checks it against the `sha256` the manifest declares, and
|
||||
unpacks it onto the modules volume.
|
||||
|
||||
There is no catalog to browse, deliberately: a catalog would make core's release cadence
|
||||
decide which modules are allowed to exist.
|
||||
|
||||
3. **Restart when it asks.** A banner appears — *Modules are read from disk when the server
|
||||
starts* — with a **Restart the server** button. The row reads *Restart to start* until
|
||||
you do.
|
||||
|
||||
The button exits the process and lets your supervisor bring it back; on the Compose
|
||||
deployment from [Install the site](/docs/getting-started/install-the-site/), that is
|
||||
`restart: unless-stopped` doing its job. `docker compose restart app` is exactly
|
||||
equivalent.
|
||||
|
||||
4. **Confirm it started.** The module's row should read *Started*, and its screens should
|
||||
have appeared in the navigation.
|
||||
|
||||
</Steps>
|
||||
|
||||
<Aside type="note" title="Only listed hosts may be installed from">
|
||||
Installing a module runs its code inside your server, so the URL must be HTTPS and its host
|
||||
must be in the allowlist at the bottom of the same screen — re-checked on every redirect.
|
||||
It is seeded with `gitea.whitlocktech.com`, and an empty list forbids every install.
|
||||
</Aside>
|
||||
|
||||
## What the `uo` module adds
|
||||
|
||||
Watch the log at the restart and you will see exactly what it mounted:
|
||||
|
||||
```
|
||||
[uo] registered routes: public:/shard,/atlas admin:/shard,/uo-link player:/shard
|
||||
[modules] schema ensured for module "uo"
|
||||
[modules] module "uo" started
|
||||
```
|
||||
|
||||
Its capabilities are {platform.moduleUoCapabilities.join(', ')} — the shard console, the
|
||||
map atlas, the player-vendor marketplace, city governors, guilds, houses and IDOCs, champion
|
||||
boards, and the cliloc strings that make item names readable.
|
||||
|
||||
A module owns **one path segment** wherever it appears, so its pages live under `/uo/…`,
|
||||
`/admin/uo/…` and `/player/uo/…`. That boundary is visible in the URL on purpose.
|
||||
|
||||
<Aside type="caution" title="A module with no game server behind it is empty, not broken">
|
||||
Installing `uo` does not connect anything. Its screens exist and report the shard as
|
||||
offline until you
|
||||
[connect a game server](/docs/getting-started/connect-a-game-server/) — which is the same
|
||||
thing the public site does when the shard goes down, and is designed to be unremarkable.
|
||||
</Aside>
|
||||
|
||||
## The declarative alternative
|
||||
|
||||
A host whose Compose file is version-controlled can skip the panel entirely: set `MODULES`
|
||||
in `.env`, one entry per module, `<id>@<version>=<install manifest URL>`. The container
|
||||
resolves that set at every start.
|
||||
|
||||
A module already unpacked at the declared version is left alone **without a single network
|
||||
call**, so a restart with no route to the internet comes up unchanged. A failure is logged
|
||||
and shown in Admin → Modules, and never stops the site from starting.
|
||||
|
||||
The two surfaces agree on a rule worth knowing: **the variable owns what is on the volume,
|
||||
the admin panel owns whether a module runs.** A module you disable in the panel stays
|
||||
disabled even though its files are put back at the next start.
|
||||
|
||||
More on both in [Managing modules](/docs/administration/managing-modules/).
|
||||
|
||||
Next: [Connect a game server](/docs/getting-started/connect-a-game-server/).
|
||||
140
src/content/docs/docs/getting-started/install-the-site.mdx
Normal file
140
src/content/docs/docs/getting-started/install-the-site.mdx
Normal file
@@ -0,0 +1,140 @@
|
||||
---
|
||||
title: Install the site
|
||||
description: A complete Docker Compose deployment you can copy from this page — two files, two commands.
|
||||
---
|
||||
|
||||
import { Aside, Code, Steps } from '@astrojs/starlight/components';
|
||||
import { compose, env, omittedServices } from '../../../../data/quickstart.mjs';
|
||||
|
||||
export const envText = env.map((e) => `${e.key}=${e.value}`).join('\n');
|
||||
export const fillLines = env.filter((e) => e.fill).map((e) => `${e.key}=${e.value}`);
|
||||
|
||||
The site is a Docker deployment: a MariaDB container, the prebuilt application image, and
|
||||
two files you write. Nothing is compiled on your host, and there is no repository to clone
|
||||
— everything you need is on this page.
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **Make a directory for the deployment.**
|
||||
|
||||
Everything below is relative to it, and the bind mounts want to exist before the
|
||||
containers do — Docker creates a missing mount source as `root`, and the container user
|
||||
then cannot write it.
|
||||
|
||||
```bash
|
||||
mkdir -p runic-gateway/logs runic-gateway/brand runic-gateway/modules
|
||||
cd runic-gateway
|
||||
```
|
||||
|
||||
2. **Write `docker-compose.yml`.**
|
||||
|
||||
<Code code={compose} lang="yaml" title="docker-compose.yml" />
|
||||
|
||||
This file only ever *pulls*. There is no `build:` anywhere in it, which is deliberate:
|
||||
a production host should not be able to build an image by accident.
|
||||
|
||||
3. **Write `.env` beside it.**
|
||||
|
||||
Every highlighted line must be changed before this is a real deployment. The secrets
|
||||
want to be long random strings — `openssl rand -base64 36` three times is enough.
|
||||
|
||||
<Code code={envText} lang="ini" title=".env" mark={fillLines} />
|
||||
|
||||
4. **Pull and start.**
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The database comes up first; the app waits for its health check, creates its schema,
|
||||
seeds defaults, creates your first admin, and starts listening.
|
||||
|
||||
5. **Check that it is up.**
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:3000/api/health
|
||||
```
|
||||
|
||||
```json
|
||||
{"status":"ok"}
|
||||
```
|
||||
|
||||
If that answers, the site is running. Go to
|
||||
[First run](/docs/getting-started/first-run/).
|
||||
|
||||
</Steps>
|
||||
|
||||
## What you just deployed
|
||||
|
||||
```
|
||||
localhost:3000 ──► app (the website: API + the built React client, one process)
|
||||
│
|
||||
└──► db (MariaDB, no host port — only the app can reach it)
|
||||
```
|
||||
|
||||
Four host directories and two volumes hold everything that survives a container:
|
||||
|
||||
| Path | What is in it |
|
||||
|---|---|
|
||||
| `./logs/` | `app.log`, readable from the host without `docker exec` |
|
||||
| `./modules/` | Installed [modules](/docs/getting-started/install-a-game-module/). A bind mount, so placing one by hand is a supported install |
|
||||
| `./brand/` | Your logo, hero and favicon, if you replace the defaults ([Branding and theming](/docs/administration/branding-and-theming/)) |
|
||||
| `dbdata` volume | The database |
|
||||
| `uploads` volume | Everything uploaded through the site |
|
||||
|
||||
<Aside type="caution" title="`restart: unless-stopped` is load-bearing">
|
||||
It is not boilerplate. Installing a module needs a restart, and the admin panel offers a
|
||||
button for it — that button exits the process and lets the supervisor bring it back. On a
|
||||
deployment with nothing supervising the process, the button takes the site down and leaves
|
||||
it down. Docker Compose is the supervisor here, and this line is what makes it one.
|
||||
</Aside>
|
||||
|
||||
## Two variables worth reading twice
|
||||
|
||||
**`SECRET_ENC_KEY`** encrypts secrets at rest — OAuth client secrets, the Discord bot
|
||||
token, the shard's auth token. In production the server **refuses to start** without it.
|
||||
Changing it later does not re-encrypt anything: what was stored under the old key can no
|
||||
longer be read, and every stored secret has to be entered again.
|
||||
|
||||
**`BOT_INTERNAL_KEY`** authenticates the internal channel between the site and the Discord
|
||||
bot. The server also refuses to start in production if it is blank, left at a placeholder,
|
||||
or shorter than 16 characters — even when, as here, you are not running the bot yet.
|
||||
|
||||
<Aside type="note" title="Both of those are set once, before the first boot">
|
||||
They are not "fill in later" values. The first boot is when your admin account and the
|
||||
site's defaults are written, and it will not happen at all until both are set.
|
||||
</Aside>
|
||||
|
||||
## What this quickstart leaves out
|
||||
|
||||
The project's shipped Compose file has two more services. Neither is needed to boot, and
|
||||
each is introduced where it is configured:
|
||||
|
||||
<ul>
|
||||
{Object.entries(omittedServices).map(([name, why]) => (
|
||||
<li key={name}><strong><code>{name}</code></strong> — {why}</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
It also leaves out the branding, logging and session variables, which have working
|
||||
defaults and their own admin screens. The full file and the full environment reference are
|
||||
in the [website repository](https://gitea.whitlocktech.com/RunicGateway/website).
|
||||
|
||||
## Behind a reverse proxy
|
||||
|
||||
Not required to get started, and required before anyone else uses the site. Two settings
|
||||
here are what make it correct:
|
||||
|
||||
- **`TRUST_PROXY=1`** tells the app to read the client's address from `X-Forwarded-For`.
|
||||
Rate limiting, login backoff and the bot-scoring IP bans are all only as accurate as
|
||||
that. Set it to the number of proxies in front of the app, or pin it to the proxy's
|
||||
address; a blanket `true` is rejected on purpose, because it would let anyone spoof
|
||||
their address by sending a header.
|
||||
- **`COOKIE_SECURE=auto`** issues a `Secure` session cookie when the request arrives over
|
||||
HTTPS and a plain one otherwise, so logging in works both through the proxy and directly
|
||||
on the LAN while you are setting up.
|
||||
|
||||
Point the proxy at port 3000. Do not forward `INTERNAL_PORT` (3001) — it is the
|
||||
server-to-bot channel, it is deliberately not published by the Compose file, and it must
|
||||
never be reachable from outside.
|
||||
60
src/content/docs/docs/getting-started/requirements.mdx
Normal file
60
src/content/docs/docs/getting-started/requirements.mdx
Normal file
@@ -0,0 +1,60 @@
|
||||
---
|
||||
title: Requirements
|
||||
description: What you need on the website host, and what you need on the game server host, before you begin.
|
||||
---
|
||||
|
||||
import platform from '../../../../data/platform.json';
|
||||
import { Aside } from '@astrojs/starlight/components';
|
||||
|
||||
Two hosts, two lists. They can be the same machine, but they are separate deployments and
|
||||
have nothing in common except the four values you will paste between them.
|
||||
|
||||
## The website host
|
||||
|
||||
| Requirement | Detail |
|
||||
|---|---|
|
||||
| **Docker** with Compose v2 | `docker compose version` should print v2.x. The site ships as prebuilt images and pulls them; nothing is built on your host. |
|
||||
| **Outbound HTTPS** to `gitea.whitlocktech.com` | To pull the images, and later to install a module. Nothing inbound is required for the install itself. |
|
||||
| **~2 GB of disk to start** | Two images, a MariaDB volume, and an uploads volume. Uploads grow with what your community posts. |
|
||||
| **A hostname and TLS, eventually** | Not needed to boot — you can reach it on `http://localhost:3000` first. Needed before anyone else uses it: see [Maintenance and upgrades](/docs/administration/maintenance-and-upgrades/) for the reverse-proxy notes. |
|
||||
|
||||
There is no separate database to install: MariaDB comes up as a container beside the app,
|
||||
and the schema is created on first boot.
|
||||
|
||||
<Aside type="note" title="Windows and macOS are fine for trying it">
|
||||
The images are Linux containers, so Docker Desktop runs them. For a deployment other people
|
||||
depend on, a Linux host is the shape everything else assumes — the log paths, the bind
|
||||
mounts and the reverse-proxy notes all read that way.
|
||||
</Aside>
|
||||
|
||||
## The game server host
|
||||
|
||||
Only if you are connecting a game server. Today that means a ServUO shard, which is what
|
||||
the [`uo` module](/docs/getting-started/install-a-game-module/) and the installer support.
|
||||
|
||||
| Requirement | Detail |
|
||||
|---|---|
|
||||
| **A working ServUO install** | It must currently boot and compile scripts cleanly. The installer deploys onto a healthy shard; it does not repair a broken one. |
|
||||
| **ServUO {platform.bundle.servuoMin}** *(patch tier only)* | The base install works on any reasonably current ServUO. The optional patch tier is written and tested against stock {platform.bundle.servuoMin}; on any other version it is unsupported, and skipping it still leaves you with a working bridge. |
|
||||
| **The shard stopped** | `ServUO.exe` locks `Scripts.dll` and rewrites `Saves/` on exit. The installer refuses to deploy under a running shard. |
|
||||
| **Administrator / root** | It writes into system directories and registers a service. |
|
||||
| **Outbound HTTPS** | To fetch the bundle and its two artifacts. No Gitea account and no git client are needed. |
|
||||
| **The sidecar on the same host as the shard** | The shard connects to `127.0.0.1:7788`. Splitting them is not supported — that loopback socket *is* the trust boundary for inbound commands. |
|
||||
|
||||
<Aside type="caution" title="Back up before the shard install">
|
||||
The overlay overwrites `Scripts/Scripts.csproj`, a stock file, and the optional patch tier
|
||||
edits stock sources. A copy of `Scripts/` and `Config/` costs nothing and is the difference
|
||||
between an experiment and a gamble. The installer keeps its own backups too — see
|
||||
[Connect a game server](/docs/getting-started/connect-a-game-server/).
|
||||
</Aside>
|
||||
|
||||
## What you do not need
|
||||
|
||||
- **A Gitea account.** Everything the installers fetch is a public release asset.
|
||||
- **A build toolchain.** Not on either host. The site pulls images; the module arrives as a
|
||||
verified tarball; the shard plugin is C# source that ServUO itself compiles at boot.
|
||||
- **An inbound port on the game host** — for the *game*. The shard never listens for the
|
||||
website. If the website runs on a different machine from the shard, the **sidecar** needs
|
||||
to be reachable by the website, and that is the one hole you will open deliberately.
|
||||
|
||||
Next: [Install the site](/docs/getting-started/install-the-site/).
|
||||
103
src/content/docs/docs/getting-started/verify-the-whole-stack.mdx
Normal file
103
src/content/docs/docs/getting-started/verify-the-whole-stack.mdx
Normal file
@@ -0,0 +1,103 @@
|
||||
---
|
||||
title: Verify the whole stack
|
||||
description: Four checks, one per link in the chain, that distinguish "files copied" from "the bridge works".
|
||||
---
|
||||
|
||||
import { Aside, Steps } from '@astrojs/starlight/components';
|
||||
|
||||
A successful install is not a working bridge, and the failure is quiet in a specific way:
|
||||
**ServUO shells out to `dotnet build`, prints the output, ignores the exit code, and
|
||||
reloads the existing `Scripts.dll`.** A broken script build looks exactly like a clean boot.
|
||||
|
||||
So verify each link in the chain, in order. Each check tells you which one to fix.
|
||||
|
||||
<Steps>
|
||||
|
||||
1. **The plugin compiled — watch the boot output.**
|
||||
|
||||
Start your shard the way you always do. You want the build to succeed *and* the bridge
|
||||
to announce itself:
|
||||
|
||||
```
|
||||
Core: Compiling scripts...
|
||||
Build succeeded.
|
||||
[Bridge] enabled=True endpoint=127.0.0.1:7788 queueCap=10000 …
|
||||
```
|
||||
|
||||
If you scrolled past it, force the question:
|
||||
|
||||
```bash
|
||||
dotnet build Scripts/Scripts.csproj -c Release -p:Platform=x64 # must be 0 errors
|
||||
```
|
||||
|
||||
2. **The shard is connected — ask it in game.**
|
||||
|
||||
As an administrator:
|
||||
|
||||
```
|
||||
[bridge status
|
||||
```
|
||||
|
||||
It reports `connected=True depth=0 sent=… dropped=0`. `connected=False` means the shard
|
||||
cannot reach the sidecar. `dropped` climbing means the sidecar is wedged and the shard
|
||||
is shedding events rather than stalling — which is what it is designed to do, and why a
|
||||
broken bridge never freezes your game.
|
||||
|
||||
`[bridge reload` re-reads `Bridge.cfg` without a restart; `[bridge sweepnow` forces one
|
||||
pass of every stream.
|
||||
|
||||
3. **The sidecar is healthy — ask it over HTTP.**
|
||||
|
||||
`/health` needs no auth, so it is safe to curl on the shard host:
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8080/health
|
||||
```
|
||||
|
||||
```json
|
||||
{"status":"ok","protocol":4,"plugin_connected":true,"database":"ok","uptime":"2m"}
|
||||
```
|
||||
|
||||
**`plugin_connected: true` is the one that matters.** It is the only value in this whole
|
||||
sequence that distinguishes "files copied" from "the bridge works".
|
||||
|
||||
4. **The website is ingesting — look at the shard screen.**
|
||||
|
||||
On the site, open **Shard (uo-link)** (`/admin/uo/link`). The header should read
|
||||
**Connected**, with *Shard link: up* and *WS ingest: online*, and the live feed at the
|
||||
bottom should start showing events within seconds rather than sitting on
|
||||
*Waiting for shard events…*.
|
||||
|
||||
Then check the public side: the shard status page should stop reporting the game as
|
||||
offline.
|
||||
|
||||
</Steps>
|
||||
|
||||
## When one of them fails
|
||||
|
||||
| What you see | What it means |
|
||||
|---|---|
|
||||
| Shard boots clean, nothing reaches the site | The classic silent failure — a stale `Scripts.dll`. Run the `dotnet build` line above and read the errors. |
|
||||
| `[bridge` is not a command | The plugin did not compile, or the bridge is disabled in `Bridge.cfg`. |
|
||||
| `connected=False` | The sidecar is not listening on `127.0.0.1:7788`. Check the service, and that `[shard] bind` matches `Host`/`Port` in `Bridge.cfg`. |
|
||||
| `/health` is fine locally, the site says offline | The website cannot reach port 8080 — bind address or firewall. The site is *designed* to render normally with the shard down, so this fails quietly. |
|
||||
| The site logs `409` from the sidecar | Protocol mismatch. Set the Protocol field to what `/health` reports rather than guessing; the sidecar rejects rather than mis-parsing. |
|
||||
| `401` from the sidecar | Wrong or missing token. Read the live one back with `uo-link-sidecar --print-config`; do not retype it from a screenshot. |
|
||||
|
||||
<Aside type="note" title="`runicgateway doctor` answers most of this in one command">
|
||||
Run on the shard host, it checks the install record, the ServUO tree, every overlay file
|
||||
hash, the patch tier, the sidecar, its service, `/health`, and that the sidecar and overlay
|
||||
agree on a protocol. Its output is the first thing anyone helping you will ask for. It
|
||||
exits non-zero when a check failed, so a monitoring system can run it too.
|
||||
</Aside>
|
||||
|
||||
## What "working" looks like a week later
|
||||
|
||||
- The public shard page shows live status, and the admin dashboard shows events arriving.
|
||||
- `dropped` in `[bridge status` stays at zero. A climbing number means the sidecar is
|
||||
wedged, not that the shard is unhealthy.
|
||||
- `doctor` is still green after a shard update — that is what catches an overlay file
|
||||
reverted by hand or by a ServUO upgrade.
|
||||
|
||||
You have finished the installation path. From here,
|
||||
[Administration](/docs/administration/configuration/) covers running the site day to day.
|
||||
@@ -13,13 +13,6 @@ network-facing component, and only the website's backend is allowed to talk to i
|
||||
website degrades gracefully when the game is down, and sensitive events never reach the
|
||||
public event stream.
|
||||
|
||||
:::note[This documentation is being written in phases]
|
||||
The scaffold, theme and sidebar are in place. The pages themselves land in phases 7 and 8,
|
||||
starting with the installation path — which is the priority of the whole project, because
|
||||
the repositories treat the site and the shard as separate deployments and nothing today
|
||||
presents them as one sequence.
|
||||
:::
|
||||
|
||||
## What the platform is on today
|
||||
|
||||
<table>
|
||||
@@ -46,10 +39,30 @@ two independent deployments.
|
||||
2. **The shard side** is the installer binary, run on the game server's host. It sets up
|
||||
the plugin overlay and the sidecar, and it never contacts the website.
|
||||
|
||||
They meet at four values pasted into **Admin → Shard**, and at protocol {platform.protocol},
|
||||
which both sides check before they will pair.
|
||||
They meet at four values pasted into the module's shard screen, and at protocol
|
||||
{platform.protocol}, which both sides check before they will pair.
|
||||
|
||||
## Where to go next
|
||||
You can stop after the first one. A site with no game server attached is a complete
|
||||
community website — news, wiki, pages, Teams, forums, accounts and moderation are all core,
|
||||
and none of them knows a game exists. The second install is what fills the game screens.
|
||||
|
||||
## Start here
|
||||
|
||||
The seven pages of **Getting started** are that sequence, in order, and each one says what
|
||||
you should expect to see before you move on:
|
||||
|
||||
1. [Requirements](/docs/getting-started/requirements/) — what you need on both hosts
|
||||
2. [Install the site](/docs/getting-started/install-the-site/) — Docker Compose, pull-only
|
||||
3. [First run](/docs/getting-started/first-run/) — the first admin, and maintenance → live
|
||||
4. [Install a game module](/docs/getting-started/install-a-game-module/) — what makes the game screens exist
|
||||
5. [Connect a game server](/docs/getting-started/connect-a-game-server/) — the installer, on the shard host
|
||||
6. [Verify the whole stack](/docs/getting-started/verify-the-whole-stack/) — proving it works, rather than assuming
|
||||
|
||||
Then **Administration** covers running it: configuration, branding, content, users,
|
||||
authentication, Teams, moderation, notifications, modules, the shard connection, upgrades,
|
||||
and what to do when something is wrong.
|
||||
|
||||
## Where the truth lives
|
||||
|
||||
The canonical, normative documents live in the
|
||||
[`docs` repository](https://gitea.whitlocktech.com/RunicGateway/docs) and always win over
|
||||
|
||||
160
src/data/quickstart.mjs
Normal file
160
src/data/quickstart.mjs
Normal file
@@ -0,0 +1,160 @@
|
||||
/**
|
||||
* quickstart.mjs — the self-contained site deployment (D35, PLAN.md §10).
|
||||
*
|
||||
* The org lead chose a quickstart an operator can copy without leaving the page: the
|
||||
* Compose file and the environment file below are complete enough to boot a site, and
|
||||
* `/docs/getting-started/install-the-site/` renders them verbatim.
|
||||
*
|
||||
* That decision creates the artifact §1 spends its whole length warning about — a second
|
||||
* copy of somebody else's file, free to drift. `scripts/checkQuickstart.mjs` is the price
|
||||
* of it: every service, image, port, mount and variable below is re-read from `website`'s
|
||||
* own `docker-compose.yml` and `.env.example` on `main`, over the Gitea API, and any
|
||||
* disagreement fails the build. Same mechanism and same intent as `checkFacts.mjs`.
|
||||
*
|
||||
* WHAT THIS FILE IS NOT. It is not a smaller compose file that the project supports as an
|
||||
* alternative. It is the shipped one with the parts an operator does not need on day one
|
||||
* left out, and the page says so: `bot` and `ntfy` are real services, documented where
|
||||
* they are configured, and the reader is pointed at the full file for them.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Services the quickstart ships, and — for the check — what each one must still agree with
|
||||
* upstream about. `omitted` records the services deliberately left out, because a NEW
|
||||
* service appearing upstream should make someone decide, rather than pass silently.
|
||||
*/
|
||||
export const services = ['db', 'app'];
|
||||
export const omittedServices = {
|
||||
ntfy: 'Push notifications for the Android app. Nothing needs it to boot, and it wants a public URL a first install does not have yet.',
|
||||
bot: 'The Discord bot. It is configured from the admin panel once the site is up, so it is introduced on the integrations page rather than here.',
|
||||
};
|
||||
|
||||
/**
|
||||
* The Compose file, exactly as the page prints it.
|
||||
*
|
||||
* Three differences from upstream's, all deliberate and all asserted by the check:
|
||||
* - `bot` and `ntfy` are absent (above).
|
||||
* - `db` does not bind-mount `./server/db/schema.sql`. That mount is a checkout-relative
|
||||
* path, and this quickstart has no checkout; the server ensures its own schema on boot,
|
||||
* which is what actually creates the tables in every deployment.
|
||||
* - the `MODULES` comment block is reduced to one line pointing at the module page.
|
||||
*/
|
||||
export const compose = `services:
|
||||
db:
|
||||
image: mariadb:11
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
MARIADB_DATABASE: \${DB_NAME}
|
||||
MARIADB_USER: \${DB_USER}
|
||||
MARIADB_PASSWORD: \${DB_PASSWORD}
|
||||
MARIADB_ROOT_PASSWORD: \${DB_ROOT_PASSWORD}
|
||||
volumes:
|
||||
- dbdata:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
app:
|
||||
image: gitea.whitlocktech.com/runicgateway/website-app:\${IMAGE_TAG:-latest}
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
DB_HOST: db
|
||||
UPLOAD_DIR: /app/uploads
|
||||
LOG_DIR: /app/logs
|
||||
MODULES_DIR: /app/modules
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- uploads:/app/uploads
|
||||
- ./logs:/app/logs
|
||||
- ./brand:/app/brand:ro
|
||||
- ./modules:/app/modules
|
||||
ports:
|
||||
- "3000:3000"
|
||||
|
||||
volumes:
|
||||
dbdata:
|
||||
uploads:
|
||||
`;
|
||||
|
||||
/**
|
||||
* The environment file, as the page prints it. `fill` marks the lines an operator must
|
||||
* change before this is a real deployment — the page highlights exactly these.
|
||||
*/
|
||||
export const env = [
|
||||
{ key: 'IMAGE_TAG', value: 'latest' },
|
||||
|
||||
{ key: 'NODE_ENV', value: 'production' },
|
||||
{ key: 'PORT', value: '3000' },
|
||||
{ key: 'INTERNAL_PORT', value: '3001' },
|
||||
|
||||
{ key: 'DB_HOST', value: 'db' },
|
||||
{ key: 'DB_PORT', value: '3306' },
|
||||
{ key: 'DB_NAME', value: 'runic_gateway' },
|
||||
{ key: 'DB_USER', value: 'runic' },
|
||||
{ key: 'DB_PASSWORD', value: 'change-me-db-password', fill: true },
|
||||
{ key: 'DB_ROOT_PASSWORD', value: 'change-me-root-password', fill: true },
|
||||
|
||||
{ key: 'JWT_SECRET', value: 'change-me-to-a-long-random-string', fill: true },
|
||||
{ key: 'SECRET_ENC_KEY', value: 'change-me-to-another-long-random-string', fill: true },
|
||||
{ key: 'COOKIE_SECURE', value: 'auto' },
|
||||
{ key: 'TRUST_PROXY', value: '1' },
|
||||
|
||||
{ key: 'ADMIN_USERNAME', value: 'admin', fill: true },
|
||||
{ key: 'ADMIN_PASSWORD', value: 'change-me-before-first-boot', fill: true },
|
||||
|
||||
{ key: 'BOT_INTERNAL_KEY', value: 'change-me-to-a-third-long-random-string', fill: true },
|
||||
];
|
||||
|
||||
/**
|
||||
* `SECRET_ENC_KEY` is in this quickstart and NOT in upstream's `.env.example`, which is why
|
||||
* it needs a declaration rather than passing quietly.
|
||||
*
|
||||
* Found by booting this exact file against the published image (phase 7): the server calls
|
||||
* `resolveKey()` in `utils/secretBox.js` at require time and throws
|
||||
* `SECRET_ENC_KEY must be set in production`, so the container crash-loops before it ever
|
||||
* listens. It is documented in `server/.env.example` — the file local development copies —
|
||||
* and missing from the root `.env.example` that Compose actually reads.
|
||||
*
|
||||
* The check treats the omission as upstream's bug, not as licence: it fails the moment the
|
||||
* variable appears in `.env.example`, so this note cannot outlive the defect it describes.
|
||||
*/
|
||||
export const notInUpstreamEnvExample = {
|
||||
SECRET_ENC_KEY:
|
||||
"the app refuses to start in production without it (utils/secretBox.js), but website's root .env.example does not list it",
|
||||
};
|
||||
|
||||
/**
|
||||
* Variables upstream's `.env.example` carries that the quickstart leaves out, each with the
|
||||
* reason. The check requires this list plus the keys above to account for EVERY key in
|
||||
* `.env.example`: when website adds a variable, this repo goes red and someone decides
|
||||
* whether a first install needs it. That failure is the feature.
|
||||
*/
|
||||
export const envOmitted = {
|
||||
UPLOAD_DIR: 'set in the Compose file, where the volume that makes it meaningful is',
|
||||
LOG_LEVEL: 'logging defaults are fine until there is something to debug',
|
||||
FILE_LOG_LEVEL: 'as above',
|
||||
LOG_TO_FILE: 'as above',
|
||||
LOG_DIR: 'set in the Compose file, beside its bind mount',
|
||||
LOG_FILE: 'as above',
|
||||
BRAND_NAME: 'branding is its own admin screen and its own page',
|
||||
BRAND_SHORT_NAME: 'as above',
|
||||
BRAND_TAGLINE: 'as above',
|
||||
BRAND_DESCRIPTION: 'as above',
|
||||
BRAND_CONTACT_EMAIL: 'as above',
|
||||
BRAND_URL: 'as above',
|
||||
BRAND_ACCENT_COLOR: 'as above',
|
||||
BRAND_LOGO: 'as above',
|
||||
BRAND_HERO: 'as above',
|
||||
BRAND_FAVICON: 'as above',
|
||||
JWT_EXPIRES_IN: 'the default session length is a decision for later, not for boot',
|
||||
COOKIE_NAME: 'changing it logs everyone out; not a first-install decision',
|
||||
DEBUG_TRUST_PROXY: 'a diagnostic, and a noisy one',
|
||||
TOTP_CHALLENGE_TTL: 'the default is right',
|
||||
CLIENT_ORIGIN: 'only needed when the client is served from a different origin, which a Compose deployment does not do',
|
||||
BOT_INTERNAL_URL: 'points at the bot service, which this quickstart does not run',
|
||||
NTFY_BASE_URL: 'push notifications need the ntfy service, which this quickstart does not run',
|
||||
};
|
||||
Reference in New Issue
Block a user