docs(plan): record the org lead's decisions and design against them #2
548
PLAN.md
548
PLAN.md
@@ -1,17 +1,17 @@
|
|||||||
# runicgateway.com — design of record
|
# runicgateway.com — design of record
|
||||||
|
|
||||||
**Status:** proposal, awaiting org-lead approval. Nothing is built.
|
**Status:** decisions taken by the org lead on 2026-08-19 and recorded in §5. Nothing is built yet.
|
||||||
**Date:** 2026-08-19
|
**Date:** 2026-08-19 (revision 2)
|
||||||
**Repo:** `RunicGateway/runicgateway.com`
|
**Repo:** `RunicGateway/runicgateway.com`
|
||||||
**AI-assisted:** yes — drafted by Claude, per the org's AI-usage disclosure policy.
|
**AI-assisted:** yes — drafted by Claude, per the org's AI-usage disclosure policy.
|
||||||
|
|
||||||
The public website and documentation site for Runic Gateway. Two audiences: **server
|
The public website and documentation site for Runic Gateway. Two audiences: **server
|
||||||
administrators** who want to understand and install the platform, and **developers** who want to
|
administrators** who want to understand and install the platform, and **developers** who want to
|
||||||
build modules and integrations for it.
|
build modules and integrations for it. A third arrives with the Android closed beta: **players**,
|
||||||
|
who want the app.
|
||||||
|
|
||||||
This document is the plan. It is deliberately written to be argued with — §9 lists the questions
|
Revision 1 asked five open questions. Revision 2 records the answers as decisions and designs
|
||||||
that are still open, and §2 lists the things the existing repos disagree about rather than picking
|
against them. §14 lists the three facts still outstanding — none of them block starting.
|
||||||
a winner quietly.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -21,11 +21,17 @@ a winner quietly.
|
|||||||
2. [Verified platform state](#2-verified-platform-state)
|
2. [Verified platform state](#2-verified-platform-state)
|
||||||
3. [What the repositories disagree about](#3-what-the-repositories-disagree-about)
|
3. [What the repositories disagree about](#3-what-the-repositories-disagree-about)
|
||||||
4. [Phase 0 — fix the documentation the site will quote](#4-phase-0--fix-the-documentation-the-site-will-quote)
|
4. [Phase 0 — fix the documentation the site will quote](#4-phase-0--fix-the-documentation-the-site-will-quote)
|
||||||
5. [Information architecture](#5-information-architecture)
|
5. [Decisions of record](#5-decisions-of-record)
|
||||||
6. [Visual direction](#6-visual-direction)
|
6. [Runtime shape](#6-runtime-shape)
|
||||||
7. [Accuracy machinery](#7-accuracy-machinery)
|
7. [Branding is bind-mounted data](#7-branding-is-bind-mounted-data)
|
||||||
8. [Build phases](#8-build-phases)
|
8. [The Android closed-beta signup](#8-the-android-closed-beta-signup)
|
||||||
9. [Open questions](#9-open-questions)
|
9. [Legal pages](#9-legal-pages)
|
||||||
|
10. [Information architecture](#10-information-architecture)
|
||||||
|
11. [Visual direction](#11-visual-direction)
|
||||||
|
12. [Accuracy machinery](#12-accuracy-machinery)
|
||||||
|
13. [Build phases](#13-build-phases)
|
||||||
|
14. [Still needed from the org lead](#14-still-needed-from-the-org-lead)
|
||||||
|
15. [Planned, not in scope: the demo instance](#15-planned-not-in-scope-the-demo-instance)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -59,28 +65,39 @@ The cause: all nine checkouts in the workspace sat on feature branches whose loc
|
|||||||
|
|
||||||
**Rule for this repo:** every version, protocol number or capability claim that reaches the website
|
**Rule for this repo:** every version, protocol number or capability claim that reaches the website
|
||||||
is verified against `origin/<default-branch>` or the Gitea API *at the moment it is written*, never
|
is verified against `origin/<default-branch>` or the Gitea API *at the moment it is written*, never
|
||||||
against a local working tree. §7 makes that a build-time check rather than a promise, because a
|
against a local working tree. §12 makes that a build-time check rather than a promise, because a
|
||||||
promise is exactly what failed.
|
promise is exactly what failed.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 2. Verified platform state
|
## 2. Verified platform state
|
||||||
|
|
||||||
All values read from `origin/main` or the Gitea API on 2026-08-19.
|
All values re-read from the Gitea API on **2026-08-19**, after revision 1.
|
||||||
|
|
||||||
### Versions
|
### Versions
|
||||||
|
|
||||||
| Component | Value | Authority |
|
| Component | Value | Authority |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| **Wire protocol** | **4** | `link/sidecar/src/main.rs` `PROTOCOL_VERSION`; `servuo-plugins/overlay.toml` `protocol = 4` |
|
| **Wire protocol** | **4** | `link` `main:sidecar/src/main.rs:55` `PROTOCOL_VERSION` |
|
||||||
| **Module API** | **1.6.0** | `website/server/src/modules/version.js` |
|
| **Module API** | **1.6.0** | `website` `main:server/src/modules/version.js` |
|
||||||
| **Current bundle** | **2026.08.19** (protocol 4) | `installer` branch `bundles`, `current.json` |
|
| **Current bundle** | **2026.08.19** (protocol 4, generated 09:05:52Z) | `installer` branch `bundles` → `current.json` |
|
||||||
| uo-link sidecar | **v2.0.0** | release; in bundle 2026.08.19 |
|
| uo-link sidecar | **v2.0.0** (2026-08-19) | release; in bundle 2026.08.19 |
|
||||||
| Plugin overlay | **v1.0.0** | release; in bundle 2026.08.19 |
|
| Plugin overlay | **v1.0.0** (2026-08-19) | release; in bundle 2026.08.19 |
|
||||||
| Installer | **v0.1.0** | release |
|
| Installer | **v0.1.0** (2026-08-07) | release |
|
||||||
| `module-uo` | **v1.0.1** | release (`module.json` on `main` reads `0.3.0`; the release version is tag-derived) |
|
| `module-uo` | **v1.0.1** (2026-08-19) | release |
|
||||||
| Android app | **v0.5.0** | release |
|
| Android app | **v0.5.0** (2026-08-08), id `com.runicgateway.app` | release; `app/build.gradle.kts` |
|
||||||
| ServUO | **57.4** — the only version the patch tier is verified against | `overlay.toml` |
|
| ServUO | **57.4** — min version, and the only version the patch tier is verified against | bundle `overlay.servuo` |
|
||||||
|
| `website` | **no releases** — ships as container images, never tagged | Gitea releases API (empty) |
|
||||||
|
|
||||||
|
Three corrections to revision 1, found while re-verifying:
|
||||||
|
|
||||||
|
- **The bundle manifests live at the root of the `bundles` branch**, not under `bundles/`:
|
||||||
|
`current.json`, `bundle-2026.08.19.json`. §12's fact-checker must fetch the root path.
|
||||||
|
- **`website` publishes no releases at all.** The site must never print a "website version"; it
|
||||||
|
refers to the platform by bundle and Module API version instead.
|
||||||
|
- **The Android app targets no server of ours.** `core/net/BaseUrlHolder.kt`,
|
||||||
|
`core/net/HostSelectionInterceptor.kt` and `core/prefs/ServerPreferences.kt` mean the user enters
|
||||||
|
the address of the deployment they belong to. This is load-bearing for §9.
|
||||||
|
|
||||||
### What exists
|
### What exists
|
||||||
|
|
||||||
@@ -121,19 +138,21 @@ unprivileged account.
|
|||||||
active module per deployment. `module_id` columns exist to keep it later-friendly; nothing
|
active module per deployment. `module_id` columns exist to keep it later-friendly; nothing
|
||||||
exercises them.
|
exercises them.
|
||||||
- **A second game module.** `module-rust` is a paper dry-run (`docs/modules/rust-dryrun.md`),
|
- **A second game module.** `module-rust` is a paper dry-run (`docs/modules/rust-dryrun.md`),
|
||||||
deliberately unimplemented — it exists to test that the contract generalises.
|
deliberately unimplemented — it exists to test that the contract generalises. Its absence is also
|
||||||
|
the exit criterion for the Integration Kit's draft status (§5, D8).
|
||||||
- **A finished Integration Kit.** It describes itself as a draft: "finished when someone outside
|
- **A finished Integration Kit.** It describes itself as a draft: "finished when someone outside
|
||||||
this project builds a working module for a new game by following it alone. That has not happened
|
this project builds a working module for a new game by following it alone. That has not happened
|
||||||
yet."
|
yet."
|
||||||
- **macOS and Windows-on-ARM installer builds.** Deliberately absent — the shard and sidecar must
|
- **macOS and Windows-on-ARM installer builds.** Deliberately absent — the shard and sidecar must
|
||||||
share a host, and no ServUO host is either.
|
share a host, and no ServUO host is either.
|
||||||
|
- **A public demo instance.** Planned (§15), not built, and not linked until it is.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. What the repositories disagree about
|
## 3. What the repositories disagree about
|
||||||
|
|
||||||
Verified against `origin/main`. None of these are caused by this project; all of them would be
|
Verified against `origin/main`. None of these are caused by this project; all of them would be
|
||||||
**inherited and amplified** by a website that quotes them.
|
**inherited and amplified** by a website that quotes them. All ten are now in scope — see §5, D3.
|
||||||
|
|
||||||
| # | Conflict | Location | Severity |
|
| # | Conflict | Location | Severity |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
@@ -149,13 +168,20 @@ Verified against `origin/main`. None of these are caused by this project; all of
|
|||||||
| 10 | **`Module-uo/README.md` shows Phase 4 as `⬜`** though the module system shipped 2026-08-12 and the admin Modules screen exists. | `Module-uo/README.md` line 41 | Low |
|
| 10 | **`Module-uo/README.md` shows Phase 4 as `⬜`** though the module system shipped 2026-08-12 and the admin Modules screen exists. | `Module-uo/README.md` line 41 | Low |
|
||||||
| 11 | **Product name spelling.** All prose is "Runic Gateway"; identifiers are `RunicGateway`. **Settled by the org lead: keep "Runic Gateway" in prose.** Recorded because the site's original brief mandated the opposite. | everywhere | Settled |
|
| 11 | **Product name spelling.** All prose is "Runic Gateway"; identifiers are `RunicGateway`. **Settled by the org lead: keep "Runic Gateway" in prose.** Recorded because the site's original brief mandated the opposite. | everywhere | Settled |
|
||||||
|
|
||||||
|
Also found while re-verifying, and not a documentation conflict but a real constraint on §10:
|
||||||
|
|
||||||
|
> **`gitea.whitlocktech.com` has registration disabled.** Repositories are publicly readable and
|
||||||
|
> `/explore` answers anonymously, but the sign-up page returns "Registration is disabled". Nobody
|
||||||
|
> outside the org can open an issue. See §14, N3.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Phase 0 — fix the documentation the site will quote
|
## 4. Phase 0 — fix the documentation the site will quote
|
||||||
|
|
||||||
Conflicts 1–5 are protocol-accuracy bugs that exist independently of this website, and the site
|
Conflicts 1–5 are protocol-accuracy bugs that exist independently of this website, and the site
|
||||||
cannot be written honestly on top of them: "Connect a game server" has to tell an operator which
|
cannot be written honestly on top of them: "Connect a game server" has to tell an operator which
|
||||||
number to type into Admin → Shard, and today the canonical guide says the wrong one.
|
number to type into Admin → Shard, and today the canonical guide says the wrong one. Conflicts
|
||||||
|
6–10 are hygiene; the org lead elected to fix them in the same pass.
|
||||||
|
|
||||||
**Phase 0 lands before the site quotes anything.** Each is a separate PR in its own repo, using the
|
**Phase 0 lands before the site quotes anything.** Each is a separate PR in its own repo, using the
|
||||||
verified values from §2.
|
verified values from §2.
|
||||||
@@ -164,18 +190,267 @@ verified values from §2.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 0.1 | `docs` | `link/INTEGRATION.md` — protocol 3 → 4 in §2 and every normative example (`X-UOLink-Version`, `/health`, `ws.hello`, the JS client). Resolves the self-contradiction with its own protocol-4 event catalog. |
|
| 0.1 | `docs` | `link/INTEGRATION.md` — protocol 3 → 4 in §2 and every normative example (`X-UOLink-Version`, `/health`, `ws.hello`, the JS client). Resolves the self-contradiction with its own protocol-4 event catalog. |
|
||||||
| 0.2 | `docs` | `link/v4.md` — status line: the `edge` → `main` cutover is done; released as link v2.0.0 + overlay v1.0.0 in bundle 2026.08.19. |
|
| 0.2 | `docs` | `link/v4.md` — status line: the `edge` → `main` cutover is done; released as link v2.0.0 + overlay v1.0.0 in bundle 2026.08.19. |
|
||||||
| 0.3 | `docs` | `installer/INSTALL.md` — protocol 3 → 4 and the stale component versions → the current bundle, including Appendix A's `curl` commands. |
|
| 0.3 | `docs` | `installer/INSTALL.md` — protocol 3 → 4 and the stale component versions → bundle 2026.08.19 / sidecar 2.0.0 / overlay 1.0.0, including Appendix A's `curl` commands. |
|
||||||
| 0.4 | `.profile` | `README.md` — protocol 3 → 4 in the handoff block. |
|
| 0.4 | `.profile` | `README.md` — protocol 3 → 4 in the handoff block; the `module-uo` release link moves off `v0.3.0`. |
|
||||||
|
| 0.5 | `docs` | `website/ARCHITECTURE.md` — the diagram moves `shardIngest.js` and `uoLinkClient.js` into the module, matching `website/README.md` and the module system as shipped. |
|
||||||
|
| 0.6 | `docs` | `website/website-README.md` — re-sync the snapshot with the live README, including "Three ways in, and none of them is a build". |
|
||||||
|
| 0.7 | `docs` | `README.md` — index entries for `TEAMS.md`, `ARCHITECTURE.md`, `TRUSTED_DEVICES_MFA.md`, `MODERATION_APPEALS.md`. |
|
||||||
|
| 0.8 | `docs` | `website/BACKEND_DESIGN.md` — retitle for the game-agnostic core. |
|
||||||
|
| 0.9 | `Module-uo` | `README.md` — Phase 4 marked complete. |
|
||||||
|
| 0.10 | `docs` | `SECURITY.md` — the public vulnerability contact moves from a personal Gmail to `security@runicgateway.com` (§14, N2). Conditional on that address existing. |
|
||||||
|
|
||||||
Conflicts 6–10 are documentation hygiene rather than operator-facing errors. They are **not**
|
0.5–0.9 are grouped where they touch one repo, so the real PR count is smaller than the row count;
|
||||||
blockers and are proposed as an optional follow-up batch — see §9, Q2.
|
`docs` PRs 0.5–0.8 land as one hygiene PR.
|
||||||
|
|
||||||
**Nothing in Phase 0 changes code or a contract.** Each PR corrects documentation to match what the
|
**Nothing in Phase 0 changes code or a contract.** Each PR corrects documentation to match what the
|
||||||
code already does, and each cites the source of truth in its description.
|
code already does, and each cites the source of truth in its description.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Information architecture
|
## 5. Decisions of record
|
||||||
|
|
||||||
|
Taken by the org lead (Colby Whitlock) on 2026-08-19. Recorded so they are not re-litigated.
|
||||||
|
|
||||||
|
| # | Decision | Consequence |
|
||||||
|
|---|---|---|
|
||||||
|
| **D1** | **Runtime: Astro + Node adapter, hybrid.** Pages prerendered; a small number of real server endpoints exist. One Node container. | §6. Makes both the bind-mounted branding and the signup form possible without a second service. |
|
||||||
|
| **D2** | **Beta signup: our own form → our own store → CSV into Play.** Beta purpose only, no announce list. | §8. Granular consent is unnecessary; the policy states one purpose. |
|
||||||
|
| **D3** | **All ten documentation conflicts are fixed**, not just the operator-facing five. | §4. |
|
||||||
|
| **D4** | **Real web screenshots**, captured from the local review stack, not placeholders. | §13 phase 6. Needs seeded, presentable demo content. |
|
||||||
|
| **D5** | **Claude drafts `/privacy` and `/terms`** from what the code actually collects; the org lead reviews before ship. | §9. |
|
||||||
|
| **D6** | **Ship the image and compose file; the org lead deploys.** DNS and TLS terminate at their existing reverse proxy. | §13 phase 9. This repo never touches the production host. |
|
||||||
|
| **D7** | **The site sends no email at all.** No SMTP, no notifications, no mailbox behind the domain yet. | §8 designs the signup so it works anyway — see "The opt-in link removes the need for email". A contact address is still required (§14, N2). |
|
||||||
|
| **D8** | **Understated honesty.** The site reads as finished; factual badges appear only where they save a reader wasted effort. **The Integration Kit stays marked draft until a second module is successfully built against it.** | §11, §10. A status with an exit criterion, not a mood. |
|
||||||
|
| **D9** | **No analytics.** No tracking scripts, no third-party requests, no cookie banner. | Reverse-proxy access logs are the only traffic data. |
|
||||||
|
| **D10** | **Support routes to Discord and Gitea issues.** Invite: `https://discord.gg/t2Jav8yT4g`. | §10. Discord is the front door; Gitea takes issues from anyone with an account, which today is nobody outside the org — §14, N3. |
|
||||||
|
| **D11** | **Keep the existing emblem.** `runic-emblem.png` is the mark on both the website and the Android launcher icon; the site adopts it rather than drawing a new one. | §11. Revision 1's "draw a new geometric mark" recommendation is withdrawn. |
|
||||||
|
| **D12** | **A public demo instance is planned but out of scope today** — a Proxmox VM running the full stack including ServUO, with restricted settings and an hourly automatic reset. | §15. The IA reserves the slot so it lands later without a restructure. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Runtime shape
|
||||||
|
|
||||||
|
**Astro with the Node adapter, `output: 'server'` with per-page `prerender = true`.** Every
|
||||||
|
marketing and documentation page is prerendered HTML at build time; a handful of endpoints under
|
||||||
|
`/api/` and the `/brand/*` asset route are the only things that execute per request.
|
||||||
|
|
||||||
|
Why not fully static: two requirements need a process on the box. The beta signup must accept a
|
||||||
|
POST and write it somewhere (§8), and branding must be overridable by dropping a file into a bind
|
||||||
|
mount **without rebuilding the image** (§7) — which means the bytes cannot be fingerprinted into
|
||||||
|
the build output.
|
||||||
|
|
||||||
|
Why not a separate API service: one container is one thing to deploy, one thing to patch, and one
|
||||||
|
log to read. The dynamic surface is three endpoints.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────── runicgateway.com container ────────────────────────────┐
|
||||||
|
│ │
|
||||||
|
│ Astro (Node adapter) │
|
||||||
|
│ ├── prerendered pages ......... marketing, docs, legal — plain HTML │
|
||||||
|
│ ├── GET /brand/* ............ reads the bind mount, falls back to defaults │
|
||||||
|
│ └── POST /api/beta-signup ..... writes SQLite on the bind mount │
|
||||||
|
│ │
|
||||||
|
│ /app/brand-default ..... baked into the image (stock logo, tokens, brand.json) │
|
||||||
|
└─────────┬──────────────────────────────────────────────┬───────────────────────────┘
|
||||||
|
│ bind mount │ bind mount
|
||||||
|
./brand → /app/brand ./data → /app/data
|
||||||
|
logo, favicon, theme.css, brand.json beta.sqlite, exports/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Stack:** Astro for the marketing pages, Starlight for `/docs` (sidebar, breadcrumbs,
|
||||||
|
previous/next, automatic table of contents, offline full-text search). Vite underneath, so it stays
|
||||||
|
inside the org's existing tooling family. Node 22 LTS.
|
||||||
|
|
||||||
|
**Security posture**, matching the rest of the org:
|
||||||
|
|
||||||
|
- A strict CSP with no external origins. Self-hosted fonts, no CDN, no analytics (D9), so
|
||||||
|
`default-src 'self'` holds with no exceptions to argue about.
|
||||||
|
- The only writing endpoint is the signup, and it is rate-limited (§8).
|
||||||
|
- **No authenticated surface exists on the site at all.** The CSV export is a CLI run against the
|
||||||
|
bind mount, not an HTTP route — see §8.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Branding is bind-mounted data
|
||||||
|
|
||||||
|
The requirement: swapping a logo or recolouring the site is a file copy and a container restart —
|
||||||
|
never a rebuild, never an image push. This mirrors the product's own posture (`BRAND_*` env,
|
||||||
|
appearance/theming in the admin panel), so someone who has themed a Runic Gateway deployment
|
||||||
|
already knows how to theme this site.
|
||||||
|
|
||||||
|
### The mechanism
|
||||||
|
|
||||||
|
Two directories. `/app/brand-default` is baked into the image and always complete.
|
||||||
|
`/app/brand` is the bind mount and may be empty, partial, or full. **Every asset resolves against
|
||||||
|
the mount first and the defaults second**, per file — so overriding only `theme.css` leaves every
|
||||||
|
logo stock, and an empty mount produces the stock site exactly.
|
||||||
|
|
||||||
|
`GET /brand/*` serves them at stable, unhashed URLs with an ETag and a short cache TTL. These files
|
||||||
|
are deliberately **not** imported through Vite, because Vite would fingerprint the filename into
|
||||||
|
the build and the mount could never replace them.
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `logo.png` / `logo.svg` | The emblem — header mark, hero |
|
||||||
|
| `wordmark.svg` | Optional horizontal lockup (emblem + "Runic Gateway"); falls back to emblem + type |
|
||||||
|
| `favicon.ico`, `icon-192.png`, `icon-512.png` | Browser and install icons |
|
||||||
|
| `og-image.png` | Link preview card |
|
||||||
|
| `theme.css` | **Custom-property overrides only** — appended last, so it wins |
|
||||||
|
| `brand.json` | Text and links: site name, tagline, Discord invite, Gitea org URL, contact address |
|
||||||
|
|
||||||
|
`brand.json` exists so that renaming the product, changing the Discord invite or adding a contact
|
||||||
|
address does not require a rebuild either — the same class of change as swapping a logo.
|
||||||
|
|
||||||
|
### The rule that keeps the promise true
|
||||||
|
|
||||||
|
**Every colour, radius, shadow and font in the site's stylesheet is a CSS custom property defined in
|
||||||
|
one file.** `theme.css` in the mount only ever redefines those properties. A `scripts/checkTokens.mjs`
|
||||||
|
fails the build if a colour literal appears anywhere outside the token definition file.
|
||||||
|
|
||||||
|
Without that check, "one CSS file changes the appearance" decays into "one CSS file changes most of
|
||||||
|
the appearance, and then there is a hardcoded `#0e1318` in the footer". The check is the mechanism;
|
||||||
|
diligence is not.
|
||||||
|
|
||||||
|
Token names deliberately match `website/client/src/styles/theme.css` where the concepts line up
|
||||||
|
(`--bg`, `--panel-a`, `--accent`, `--ink`, `--line`, `--radius-card`, …), so a theme written for one
|
||||||
|
is legible in the other.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. The Android closed-beta signup
|
||||||
|
|
||||||
|
### What Google Play actually requires
|
||||||
|
|
||||||
|
Established before designing, because it constrains everything:
|
||||||
|
|
||||||
|
- Closed testing accepts testers as **pasted email lists** (up to 200 lists, 2,000 addresses each,
|
||||||
|
50 lists per track) **or as a Google Group**. There is **no API to add an individual tester** —
|
||||||
|
any form we build ends in a human pasting a CSV.
|
||||||
|
- Every tester must **opt in themselves** through a web opt-in link, whichever method is used.
|
||||||
|
Being on the list is necessary and not sufficient.
|
||||||
|
- An individual (non-organisation) developer account needs **12 testers opted in continuously for
|
||||||
|
14 days** before production access. That number is a design target, not a footnote — the page
|
||||||
|
should be written to convert.
|
||||||
|
- The store listing requires a **privacy policy URL** and a **contact email address** (§9, §14).
|
||||||
|
|
||||||
|
*These are current as of 2026-08-19 and should be re-checked in the Play Console before launch;
|
||||||
|
Play's testing requirements have changed more than once.*
|
||||||
|
|
||||||
|
### The opt-in link removes the need for email
|
||||||
|
|
||||||
|
D7 says the site sends no email. That looks fatal — with the email-list method, Google does not
|
||||||
|
notify testers; the developer distributes the opt-in link. But **the closed-testing opt-in URL is
|
||||||
|
safe to publish**, because it only works for addresses already on the tester list. Anyone else who
|
||||||
|
opens it is refused.
|
||||||
|
|
||||||
|
So: the confirmation screen after a successful signup shows the opt-in link and says plainly *"open
|
||||||
|
this with the same Google account once you've been added — we'll announce each batch in Discord."*
|
||||||
|
Discord (D10) is the notification channel that email would otherwise be. No SMTP, no mailbox, no
|
||||||
|
deliverability problem, and no unsubscribe machinery for a list nobody is mailed from.
|
||||||
|
|
||||||
|
This is worth stating in the plan because it is the decision that makes D7 workable rather than
|
||||||
|
merely accepted.
|
||||||
|
|
||||||
|
### The page
|
||||||
|
|
||||||
|
`/beta` — what the beta is, what it needs (an Android device, a Google account, and the willingness
|
||||||
|
to stay opted in), current status, the form, and what happens next. Honest about the wait: batches
|
||||||
|
are added by hand.
|
||||||
|
|
||||||
|
### The store
|
||||||
|
|
||||||
|
SQLite at `/app/data/beta.sqlite` (bind-mounted), via `better-sqlite3`.
|
||||||
|
|
||||||
|
```
|
||||||
|
signups
|
||||||
|
id INTEGER PRIMARY KEY
|
||||||
|
email TEXT NOT NULL UNIQUE COLLATE NOCASE
|
||||||
|
created_at TEXT NOT NULL -- ISO 8601 UTC
|
||||||
|
ip_hash TEXT NOT NULL -- salted SHA-256, never the raw address
|
||||||
|
user_agent TEXT
|
||||||
|
consent_text TEXT NOT NULL -- the exact wording they agreed to, versioned
|
||||||
|
status TEXT NOT NULL -- new | exported | removed
|
||||||
|
note TEXT
|
||||||
|
```
|
||||||
|
|
||||||
|
**The raw IP is never stored** — only a salted hash, which is enough to rate-limit and not enough to
|
||||||
|
identify. The salt lives in the container environment, so rotating it destroys the linkage
|
||||||
|
deliberately. `consent_text` stores the wording itself rather than a version number, so a record can
|
||||||
|
always answer "what exactly did this person agree to" without archaeology.
|
||||||
|
|
||||||
|
### Abuse resistance without a third party
|
||||||
|
|
||||||
|
D9 and §6's CSP forbid external requests, so no captcha service. Instead:
|
||||||
|
|
||||||
|
- A honeypot field, hidden from real users, that bots fill.
|
||||||
|
- A minimum time-to-submit — a submission under ~2 seconds after page render is a script.
|
||||||
|
- A per-`ip_hash` token bucket persisted in SQLite: a few signups per hour, a couple of dozen per day.
|
||||||
|
- A global cap on total rows, above which the form closes and says so, so the box cannot be filled.
|
||||||
|
- Strict validation, and a duplicate submission answered idempotently ("you're already on the list")
|
||||||
|
rather than with an error that leaks whether an address is enrolled.
|
||||||
|
|
||||||
|
### Getting the list into Play
|
||||||
|
|
||||||
|
A CLI, not an HTTP route:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker compose exec site node scripts/beta.mjs export # → /app/data/exports/<date>.csv, marks rows exported
|
||||||
|
docker compose exec site node scripts/beta.mjs export --all # everything, including already-exported
|
||||||
|
docker compose exec site node scripts/beta.mjs remove <email> # deletion request
|
||||||
|
docker compose exec site node scripts/beta.mjs stats
|
||||||
|
```
|
||||||
|
|
||||||
|
Deliberately not an admin page. An authenticated HTTP surface on a marketing site is a login form,
|
||||||
|
a session, a password to rotate and a thing to patch — for an operation performed by the one person
|
||||||
|
who already has shell on the host, against a file already on their disk. The CSV lands in the bind
|
||||||
|
mount and is opened locally.
|
||||||
|
|
||||||
|
`remove` exists because §9 promises deletion on request and a promise needs a mechanism.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Legal pages
|
||||||
|
|
||||||
|
Both drafted from what the code actually collects (D5), reviewed by the org lead before ship.
|
||||||
|
**Not legal advice** — accurate and specific beats generated boilerplate, and the org lead decides
|
||||||
|
whether it is sufficient.
|
||||||
|
|
||||||
|
### `/privacy`
|
||||||
|
|
||||||
|
Three separately-scoped sections, because Runic Gateway is self-hosted software and conflating them
|
||||||
|
would be wrong in both directions:
|
||||||
|
|
||||||
|
1. **This website.** No cookies. No analytics. No third-party requests of any kind. The one thing
|
||||||
|
collected is a beta signup: email address, timestamp, salted IP hash, user agent, and the consent
|
||||||
|
wording — used solely to add the address to the Google Play tester list, never sold, never mailed
|
||||||
|
to. How to have it removed. Reverse-proxy access logs and their retention.
|
||||||
|
2. **The Android app.** The point that makes this unusual and must be stated precisely: **we operate
|
||||||
|
no server the app talks to.** The app connects to an address the user enters
|
||||||
|
(`ServerPreferences` / `BaseUrlHolder`), which is run by whoever runs that community. Data the
|
||||||
|
app holds on the device — session and refresh tokens, the trusted-device token, the selected
|
||||||
|
server, the ntfy push registration — and what leaves it, and to whom. Google Play itself collects
|
||||||
|
its own data as the distributor; that is Google's policy, not ours.
|
||||||
|
3. **Self-hosted deployments.** A Runic Gateway deployment collects account data, IP addresses for
|
||||||
|
bot scoring and IP bans, session records and audit logs. **The operator of that deployment is the
|
||||||
|
data controller for it, not us.** This section exists so an operator understands the
|
||||||
|
responsibility they take on, and so no player mistakes this policy for the one governing their
|
||||||
|
community's site.
|
||||||
|
|
||||||
|
The Play Data Safety declaration is filled from section 2, and section 2 is written knowing that is
|
||||||
|
what it is for.
|
||||||
|
|
||||||
|
### `/terms`
|
||||||
|
|
||||||
|
Short and honest: the software is GPL-3.0-or-later and the licence governs its use; this site is
|
||||||
|
informational and warranty-free; the beta is a beta and may break or end; acceptable use of the
|
||||||
|
signup form; and how to get in touch. It does not attempt to govern anyone's self-hosted deployment,
|
||||||
|
because it cannot.
|
||||||
|
|
||||||
|
Both pages are linked from the footer on every page, and `/privacy` is the URL given to Play.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Information architecture
|
||||||
|
|
||||||
Organised by what a reader is trying to do. A reader should never need to know that `link`,
|
Organised by what a reader is trying to do. A reader should never need to know that `link`,
|
||||||
`servuo-plugins` and `installer` are three repositories in order to connect a game server.
|
`servuo-plugins` and `installer` are three repositories in order to connect a game server.
|
||||||
@@ -184,11 +459,15 @@ Organised by what a reader is trying to do. A reader should never need to know t
|
|||||||
|
|
||||||
| Route | Purpose |
|
| Route | Purpose |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `/` | Hero, the data path, grouped capabilities, the self-hosted argument, get-started CTA |
|
| `/` | Hero, the data path, grouped capabilities, the self-hosted argument, get-started CTA. **Reserves a "See it running" slot** for the demo (§15) — laid out now, hidden until it exists |
|
||||||
| `/features/` | Grouped capability presentation, explicit about what is core and what a module supplies |
|
| `/features/` | Grouped capability presentation, explicit about what is core and what a module supplies |
|
||||||
| `/architecture/` | The system explained visually, for a technical evaluator deciding whether to run it |
|
| `/architecture/` | The system explained visually, for a technical evaluator deciding whether to run it |
|
||||||
| `/modules/` | What a module is, `module-uo` as the worked example, writing your own, the Integration Kit |
|
| `/modules/` | What a module is, `module-uo` as the worked example, writing your own, the Integration Kit (draft-badged per D8) |
|
||||||
| `/integrations/` | Discord, mobile + ntfy push, SSO — with an explicit "not built" list |
|
| `/integrations/` | Discord, mobile + ntfy push, SSO — with an explicit "not built" list |
|
||||||
|
| `/app/` | The Android app: what it does, the 14 existing screenshots, and the beta CTA |
|
||||||
|
| `/beta/` | The closed-beta signup (§8) |
|
||||||
|
| `/community/` | Discord (`discord.gg/t2Jav8yT4g`) as the front door, the Gitea org for code and contributions, `security@` for vulnerabilities — the split in §14 N3 |
|
||||||
|
| `/privacy/`, `/terms/` | §9 |
|
||||||
|
|
||||||
**Feature grouping**, using project terminology:
|
**Feature grouping**, using project terminology:
|
||||||
|
|
||||||
@@ -239,7 +518,7 @@ website; the website is a separate Docker deployment.
|
|||||||
3. First run — first admin, maintenance → live
|
3. First run — first admin, maintenance → live
|
||||||
4. Install a game module — admin panel, `MODULES` env, or by hand
|
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)
|
5. Connect a game server — the installer binary on the shard host (ServUO-specific today)
|
||||||
6. Paste the four values into Admin → Shard
|
6. Paste the four values into Admin → Shard — **protocol 4**, per §2
|
||||||
7. Verify — `[bridge status` in game, `/health` reporting `plugin_connected: true`, then `doctor`
|
7. Verify — `[bridge status` in game, `/health` reporting `plugin_connected: true`, then `doctor`
|
||||||
8. Configure authentication and integrations
|
8. Configure authentication and integrations
|
||||||
|
|
||||||
@@ -248,32 +527,53 @@ Troubleshooting.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 6. Visual direction
|
## 11. Visual direction
|
||||||
|
|
||||||
**"Modern infrastructure software with an arcane identity."** Dark-first. Marketing pages are
|
**"Modern infrastructure software with an arcane identity."** Dark-first. Marketing pages are
|
||||||
single-theme by design; the docs honour the reader's light/dark preference.
|
single-theme by design; the docs honour the reader's light/dark preference.
|
||||||
|
|
||||||
- **The palette descends from the product's own tokens** (`website/client/src/styles/theme.css`), so
|
**The mark is the existing emblem** (D11). `runic-emblem.png` — a gold-and-ruby ring around a
|
||||||
the site and the thing it describes read as one family: near-black ground, a steel-blue accent
|
glowing cyan portal — is already the site logo and the Android launcher icon, so adopting it makes
|
||||||
derived from the product's `#7f99bd`, and the product's existing live/maintenance signal colours.
|
the three surfaces one product. Revision 1 argued for replacing it; the org lead likes it, and a
|
||||||
One secondary — a muted violet — reserved **exclusively** for the runic motif (hairlines,
|
mark already carrying recognition beats a better-drawn one that carries none.
|
||||||
gradients, diagram glow) and never used for text.
|
|
||||||
- **Type:** Cinzel — already the project's display face — for the wordmark and hero only; a modern
|
|
||||||
variable sans for everything else. Self-hosted, so there are no external font requests.
|
|
||||||
- **Motif:** hand-drawn SVG geometry — a gateway glyph, thin luminous topology lines, layered
|
|
||||||
panels. Subtle, structural, used where it explains something. **No AI-generated rune artwork
|
|
||||||
anywhere.**
|
|
||||||
|
|
||||||
### On the existing emblem
|
Work needed on it, none of which is a redesign:
|
||||||
|
|
||||||
`website/client/public/assets/img/runic-emblem.png` is an ornate gold-and-gems medallion. It reads
|
- Web derivatives from the 1024px source: it is a **1.4 MB PNG**, far too heavy for a header. WebP
|
||||||
as instance branding for UOMysticmoon, and as exactly the fantasy-decoration register the site is
|
and AVIF at header, hero and OG sizes, plus a real multi-resolution `favicon.ico` and the 192/512
|
||||||
meant to avoid. **Recommendation: draw a new stroke-only geometric gateway mark** that scales down
|
PWA icons.
|
||||||
to a favicon. See §9, Q3.
|
- A horizontal lockup — emblem beside "Runic Gateway" set in Cinzel — for the header and OG card.
|
||||||
|
- All of it lands in `/app/brand-default` (§7), so the org lead can replace any of it with an
|
||||||
|
updated logo by copying a file.
|
||||||
|
|
||||||
|
**Palette, revised.** Revision 1 proposed a muted violet secondary. That was invented; the emblem
|
||||||
|
already has a palette and it is better. The site descends from the product's tokens
|
||||||
|
(`website/client/src/styles/theme.css`) and takes its accents from the mark:
|
||||||
|
|
||||||
|
- Near-black ground and panels — the product's `--bg` family, unchanged.
|
||||||
|
- The product's steel-blue `#7f99bd` for interface and links, unchanged.
|
||||||
|
- **Gold and cyan from the emblem** as the accent pair — gold for emphasis and rules, the portal's
|
||||||
|
cyan for the glow behind the diagrams and the live-state signal. Both derived from the artwork by
|
||||||
|
sampling, not guessed, and both held to WCAG AA against the ground.
|
||||||
|
- The product's existing live/maintenance signal colours (`--mode-live`, `--mode-maint`) reused
|
||||||
|
verbatim, so a status pill means the same thing on both sites.
|
||||||
|
|
||||||
|
**Type:** Cinzel — already the project's display face, and already in the Android app's `res/font` —
|
||||||
|
for the wordmark and hero only; a modern variable sans for everything else. Self-hosted, so there
|
||||||
|
are no external font requests and §6's CSP needs no exception.
|
||||||
|
|
||||||
|
**Motif:** hand-drawn SVG geometry — a gateway glyph derived from the emblem's concentric rings,
|
||||||
|
thin luminous topology lines, layered panels. Subtle, structural, used where it explains something.
|
||||||
|
**No AI-generated rune artwork anywhere.**
|
||||||
|
|
||||||
|
**Maturity signals** (D8) are typographically quiet: a small "draft" chip on Integration Kit pages,
|
||||||
|
version chips wherever a component is named. No apologetic tone anywhere. The Integration Kit chip
|
||||||
|
has a defined removal condition — a second module built successfully against the kit — recorded here
|
||||||
|
so a future reader knows when to take it down.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 7. Accuracy machinery
|
## 12. Accuracy machinery
|
||||||
|
|
||||||
The site quotes versions, protocol numbers and capability lists. Given §1's process rule, that needs
|
The site quotes versions, protocol numbers and capability lists. Given §1's process rule, that needs
|
||||||
a mechanism rather than diligence:
|
a mechanism rather than diligence:
|
||||||
@@ -281,62 +581,138 @@ a mechanism rather than diligence:
|
|||||||
- **`src/data/platform.json`** — one file holding every externally-sourced fact: protocol version,
|
- **`src/data/platform.json`** — one file holding every externally-sourced fact: protocol version,
|
||||||
Module API version, current bundle tag, component versions, `module-uo`'s capability list. Every
|
Module API version, current bundle tag, component versions, `module-uo`'s capability list. Every
|
||||||
page reads from it. **No version number is ever hardcoded in prose.**
|
page reads from it. **No version number is ever hardcoded in prose.**
|
||||||
- **`scripts/checkFacts.mjs`** — fetches the authority for each fact (Gitea raw `overlay.toml`,
|
- **`scripts/checkFacts.mjs`** — fetches the authority for each fact and **fails the build on any
|
||||||
`version.js`, `bundles/current.json`, release tags) and **fails the build on any disagreement.**
|
disagreement**:
|
||||||
|
|
||||||
|
| Fact | Authority |
|
||||||
|
|---|---|
|
||||||
|
| Protocol version | `link` `main:sidecar/src/main.rs` → `PROTOCOL_VERSION` |
|
||||||
|
| Overlay protocol | `servuo-plugins` `main:overlay.toml` → `protocol` |
|
||||||
|
| Module API | `website` `main:server/src/modules/version.js` → `MODULE_API_VERSION` |
|
||||||
|
| Bundle + component pins | `installer` branch `bundles`, **root** `current.json` |
|
||||||
|
| Release versions | Gitea releases API per repo |
|
||||||
|
|
||||||
Same mechanism and the same intent as the Integration Kit's `checkCoreApi.js`: when the platform
|
Same mechanism and the same intent as the Integration Kit's `checkCoreApi.js`: when the platform
|
||||||
moves, this repo goes red so someone updates the site. That failure is the feature.
|
moves, this repo goes red so someone updates the site. That failure is the feature.
|
||||||
- **`scripts/checkLinks.mjs`** — every internal link resolves; every outbound link into a
|
- **`scripts/checkLinks.mjs`** — every internal link resolves; every outbound link into a
|
||||||
`RunicGateway` repo points at a branch path, not a commit permalink.
|
`RunicGateway` repo points at a branch path, not a commit permalink.
|
||||||
|
- **`scripts/checkTokens.mjs`** — no colour literal outside the token file (§7).
|
||||||
- `astro check` plus a production build, in CI on every PR.
|
- `astro check` plus a production build, in CI on every PR.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 8. Build phases
|
## 13. Build phases
|
||||||
|
|
||||||
| Phase | Deliverable |
|
| Phase | Deliverable |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **0** | The documentation fixes in §4 — four PRs across `docs` and `.profile` |
|
| **0** | The documentation fixes in §4 — all ten conflicts, across `docs`, `.profile` and `Module-uo` |
|
||||||
| **1** | Foundation: scaffold, design tokens, typography, layout shell, shared header/footer, the SVG mark, docs theming, sidebar, `platform.json` + `checkFacts.mjs` |
|
| **1** | Foundation: Astro + Node adapter scaffold, the token file, typography, layout shell, header/footer, docs theming and sidebar, `platform.json` + `checkFacts.mjs` + `checkTokens.mjs` |
|
||||||
| **2** | Homepage: hero, the data-path diagram as inline SVG, grouped capability sections, CTA |
|
| **2** | Branding pipeline (§7): `/brand/*` resolution, `brand-default` contents, the emblem's web derivatives and lockup, `brand.json` wiring |
|
||||||
| **3** | Marketing: `/features/`, `/architecture/`, `/modules/`, `/integrations/` |
|
| **3** | Homepage: hero, the data-path diagram as inline SVG, grouped capability sections, CTA, the reserved demo slot |
|
||||||
| **4** | Docs — the journey: Getting started (7) + Administration (12). The installation path is the priority of the whole project |
|
| **4** | Marketing: `/features/`, `/architecture/`, `/modules/`, `/integrations/` |
|
||||||
| **5** | Docs — builder and reference: Modules (8) + Architecture (5) + Reference (7) |
|
| **5** | The app and the beta: `/app/`, `/beta/`, the signup endpoint, the SQLite store, rate limiting, the export CLI (§8) |
|
||||||
| **6** | Polish: responsive, accessibility, SEO/OpenGraph/sitemap/robots, full-text search, screenshot components |
|
| **6** | Legal: `/privacy/`, `/terms/`, footer links, and the Play Data Safety notes (§9) |
|
||||||
| **7** | Validation: `astro check`, production build, link check, fact check, mobile layout verified in a real browser |
|
| **7** | Docs — the journey: Getting started (7) + Administration (12). **The installation path is the priority of the whole project** |
|
||||||
| **8** | Delivery: Dockerfile (static build → static server), `docker-compose.yml`, Gitea Actions workflow, README, CONTRIBUTING with the AI-disclosure requirement |
|
| **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 |
|
||||||
|
| **10** | Polish: responsive, accessibility, SEO/OpenGraph/sitemap/robots, full-text search, CSP headers |
|
||||||
|
| **11** | Validation: `astro check`, production build, all four check scripts, 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) |
|
||||||
|
|
||||||
**Stack:** Astro + Starlight — Astro for the marketing pages, Starlight for `/docs` (sidebar,
|
Phases 5 and 6 are deliberately adjacent and early: the beta cannot start without `/privacy`, and
|
||||||
breadcrumbs, previous/next, automatic table of contents, offline full-text search). Static output,
|
the closed test is the nearest real deadline.
|
||||||
Vite underneath, so it stays inside the org's existing tooling family.
|
|
||||||
|
|
||||||
**Deploy:** a Docker image behind the org lead's reverse proxy. This repo ships the image and the
|
|
||||||
compose file; the deployment endpoint is wired on the host.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9. Open questions
|
## 14. Still needed from the org lead
|
||||||
|
|
||||||
**Q1 — Does Phase 0 land as PRs from here, or as issues?**
|
None of these block starting Phase 0 or Phase 1.
|
||||||
§4 proposes four PRs correcting protocol and version references in `docs` and `.profile`. The
|
|
||||||
alternative is filing issues and letting the site launch alongside documentation that contradicts
|
|
||||||
it. *Recommendation: PRs — the docs are wrong regardless of whether this site is ever built.*
|
|
||||||
|
|
||||||
**Q2 — Do conflicts 6–10 get fixed too?**
|
**N1 — Resolved.** `runicgateway.com` is registered through **Cloudflare**, with DNS on Cloudflare.
|
||||||
Documentation hygiene, not operator-facing errors: the stale architecture diagram, the drifted
|
The domain does not resolve to anything yet; the record is pointed at the host in phase 12.
|
||||||
README snapshot, the stale docs index, the UOMysticmoon title, the `⬜` phase table. Cheap to fix,
|
|
||||||
outside the site's scope, and each is one small PR. Fix now, later, or never?
|
|
||||||
|
|
||||||
**Q3 — Screenshots.** There are **zero web-UI captures** anywhere in the workspace. The only real
|
**N2 — A contact address at the domain. Recommendation: Cloudflare Email Routing**, now that N1
|
||||||
product images are 14 Android screenshots in `docs/android/screenshots/`. Options: (a) stand up the
|
confirms the domain is on Cloudflare. It is free, unlimited, receive-only, and adds its own MX and
|
||||||
local review stack and capture real web screenshots, (b) ship clearly-labelled placeholder
|
SPF records automatically. Three addresses, all forwarding to the existing inbox:
|
||||||
components to fill later, (c) launch with the Android captures and diagrams only.
|
|
||||||
|
|
||||||
**Q4 — The product mark.** Draw a new geometric SVG gateway mark, or keep `runic-emblem.png`?
|
| Address | Used by |
|
||||||
|
|---|---|
|
||||||
|
| `hello@runicgateway.com` | The Play store listing's contact email; `/community` |
|
||||||
|
| `privacy@runicgateway.com` | `/privacy` — deletion and data requests (§9) |
|
||||||
|
| `security@runicgateway.com` | `docs/SECURITY.md`, which currently publishes a personal Gmail on a public repo |
|
||||||
|
|
||||||
**Q5 — How direct should the docs be about maturity?** The Integration Kit calls itself a draft,
|
Pair it with Gmail's "Send mail as" to reply from the addresses rather than the underlying inbox.
|
||||||
protocol 4 is days old, the installer is v0.1.0. The brief asks the site to look like an established
|
Routing is receive-only, which is sufficient — the site sends nothing (D7). If double opt-in is ever
|
||||||
product. A "draft" badge on the Integration Kit pages is honest and costs little credibility; saying
|
wanted later, Resend or Brevo's free tier plus DKIM/DMARC would cover sending.
|
||||||
nothing is more polished and less true. Preference?
|
|
||||||
|
Retiring the personal address from `SECURITY.md` is a one-line `docs` PR and rides along with
|
||||||
|
Phase 0.
|
||||||
|
|
||||||
|
**N3 — Gitea registration.** Open registration previously drew a flood of spam accounts, so it is
|
||||||
|
disabled, and "file an issue" is therefore a wall for anyone outside the org. Reopening it safely on
|
||||||
|
**Gitea 1.24.7** is a config change, not a gamble:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[service]
|
||||||
|
DISABLE_REGISTRATION = false
|
||||||
|
REGISTER_MANUAL_CONFIRM = true ; an admin approves each account before it can sign in
|
||||||
|
REGISTER_EMAIL_CONFIRM = false ; manual confirm is ignored unless this is off
|
||||||
|
ENABLE_CAPTCHA = true
|
||||||
|
CAPTCHA_TYPE = cfturnstile
|
||||||
|
CF_TURNSTILE_SITEKEY = <from Cloudflare — free, and the domain is already there>
|
||||||
|
CF_TURNSTILE_SECRET = <ditto>
|
||||||
|
DEFAULT_ALLOW_CREATE_ORGANIZATION = false
|
||||||
|
DEFAULT_KEEP_EMAIL_PRIVATE = true
|
||||||
|
|
||||||
|
[repository]
|
||||||
|
MAX_CREATION_LIMIT = 0 ; -1 is unlimited; 0 means no user may create a repository
|
||||||
|
```
|
||||||
|
|
||||||
|
- **`REGISTER_MANUAL_CONFIRM` is the control that actually stops the flood.** Registrations queue in
|
||||||
|
Admin → User Accounts and cannot sign in, comment or open issues until approved, so a spam run
|
||||||
|
produces a list to delete rather than content to clean up.
|
||||||
|
- **`MAX_CREATION_LIMIT = 0` is defence in depth**, and is what "users cannot create repos by
|
||||||
|
default" means concretely. Raise it per-account in Admin → User Accounts for anyone who should be
|
||||||
|
able to. **Set the org lead's own account to `-1` explicitly before flipping the global**, rather
|
||||||
|
than assuming admin accounts bypass it.
|
||||||
|
- **Turnstile** is free and the domain is already on Cloudflare, so it costs one form.
|
||||||
|
- **Caveat worth weighing:** with no mailer configured on Gitea, nobody is notified when an account
|
||||||
|
is pending, and the applicant is not told when they are approved. That makes Gitea a slow channel
|
||||||
|
for someone who just wants to report a bug.
|
||||||
|
|
||||||
|
**Consequently, `/community` should describe an honest split rather than one channel:** Discord
|
||||||
|
(`discord.gg/t2Jav8yT4g`) is the front door for questions, bug reports and the beta announcements —
|
||||||
|
instant and unauthenticated. Gitea is where the code lives, is publicly readable without an account,
|
||||||
|
and takes issues and pull requests from contributors willing to request one. `security@` is the
|
||||||
|
private channel for vulnerabilities. That page is written the same way whether or not N3 is
|
||||||
|
actioned; only one sentence changes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Planned, not in scope: the demo instance
|
||||||
|
|
||||||
|
D12. Recorded now so the IA reserves room for it and nothing has to be restructured later.
|
||||||
|
|
||||||
|
**Shape:** a VM on the org lead's Proxmox host running the full stack — website, `module-uo`, the
|
||||||
|
uo-link sidecar and a real ServUO shard — so an evaluator clicks one link and sees the actual
|
||||||
|
product with live game data flowing through the bridge, which no screenshot can convey.
|
||||||
|
|
||||||
|
**Constraints that make it safe to run:**
|
||||||
|
|
||||||
|
- **Automatic reset roughly hourly** from a golden snapshot, so nothing an anonymous visitor does
|
||||||
|
outlasts the hour. Reverting the VM is simpler and more complete than any application-level reset.
|
||||||
|
- **Restricted settings** — the demo account reaches most of the admin panel, but not the surfaces
|
||||||
|
that would let a visitor break out or reach the network: SSO client secrets, the uo-link token and
|
||||||
|
base URL, mail, the module installer, and anything shelling out. The specific allow-list is
|
||||||
|
designed when it is built, against the admin route inventory.
|
||||||
|
- Seeded content shared with phase 9's screenshots, so the demo and the site's imagery agree.
|
||||||
|
- Isolated network segment; no path from the demo VM to anything else on the host.
|
||||||
|
- Its own subdomain, its own TLS, a `noindex` header, and a banner stating it resets hourly.
|
||||||
|
|
||||||
|
**Site-side preparation done now, for free:** the homepage lays out a "See it running" slot and
|
||||||
|
`/features/` a per-capability "try it" affordance, both rendered only when
|
||||||
|
`brand.json` carries a demo URL. When the VM exists, the site gains a working demo by way of one
|
||||||
|
line in a bind-mounted file — no rebuild, consistent with §7.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user