13 Commits

Author SHA1 Message Date
152ffef86e docs(installer): add release orchestration and the bundle manifest
The installer needs CI that reacts when a component publishes a release. Adds
that as section 7, folded into version tracking because the bundle IS the compat
matrix -- which closes the "where does the compat matrix live" gap section 7
previously left open.

- 7.1 Bundle manifest: CI publishes an exact, protocol-checked combination of
  component versions; the installer resolves against it at run time and
  --bundle <tag> pins one. A link release regenerates JSON and leaves the
  installer binary untouched, so operators don't re-download the installer for a
  sidecar patch and the repo doesn't accumulate releases with identical code.
  Two compose-time gates: sidecar PROTOCOL_VERSION must equal the overlay
  manifest's declared version, and every asset's SHA256 must match.
- 7.2 Triggers: each component's release job POSTs to the installer's
  workflow-dispatch endpoint (link's release.yml already declares
  workflow_dispatch and already holds a write:repository token), plus a nightly
  cron so a missed dispatch self-heals. repository_dispatch avoided -- support
  is uncertain on this Gitea version.
- 7.3 Stale overlay: dispatch, don't wait. Components self-release on merge to
  their own main, so the release normally already exists. If main is ahead with
  *releasable* commits (docs:/chore: correctly cut nothing), fire that repo's
  workflow, compose from what exists now, warn loudly, and let the nightly fold
  in the result. Dispatching another repo's workflow is fine -- it still runs
  its own gates -- but polling it is not, since Gitea's dispatch endpoint
  returns no run handle.

Bundle CI becomes a Phase 0 deliverable, since Phase 1 resolves what to install
from the bundle. `update` now moves between checked combinations rather than two
independently-latest artifacts.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 06:09:58 -05:00
29056ba996 docs(installer): plan the Runic Gateway installer
Design of record for a deployment tool that takes a stock ServUO install and
configures it for Runic Gateway. Supersedes the informal overview it grew from,
which described a ServUO integration that does not match how servuo-plugins
actually ships.

Corrections that change the design:

- There is no RunicGateway.dll and no Plugins/ dir. The plugin ships as C#
  source compiled by ServUO at boot, so the step is a hash-compare sync of
  overlay/ -- but a successful copy does not mean a working bridge, because
  ScriptCompiler.Compile() ignores dotnet build's exit code and reloads the
  stale Scripts.dll.
- Stock ServUO files ARE modified, by three diffs in patches/. Made an opt-in,
  skippable tier: git apply against a hand-modified shard will fail, and the
  EventSink.cs patch needs a full core solution rebuild.
- Config paths collided with what the sidecar actually reads. Split ownership:
  sidecar.toml stays the sidecar's schema, install.json is the installer's.
  Service definitions pin UOLINK_CONFIG and UOLINK_DB_PATH, since the sidecar
  writes relative to CWD and would land in VirtualStore under Program Files.
- The token handoff was missing entirely -- the largest "installed it and
  nothing happened" failure mode.
- deploy.ps1 cannot be the cross-platform deployer; it stays the developer tool.

Decisions: public audience, unsigned binaries anchored on SHA256SUMS, Rust,
release-tarball plugin distribution, printed token handoff, new installer repo,
warn-and-skip on non-57.4 ServUO, and an uninstall that never edits the shard
tree -- it prints the files to delete and the hunks to revert.

Phases 0-5, with Phase 0 (a release workflow for servuo-plugins, which has
none today) gating everything else. Two open questions remain in section 8.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-01 05:44:44 -05:00
186f057bc0 Merge pull request 'docs(tree): sync android/PROJECT_TREE.md' (#79) from chore/sync-android-tree into main
Reviewed-on: #79
2026-08-01 07:50:46 +00:00
runic-docs-bot
33a013ca98 docs(tree): sync android/PROJECT_TREE.md from RunicGateway/Android-app@5eaf5d2 [skip ci] 2026-08-01 07:22:23 +00:00
5e2bc22a94 Merge pull request 'docs(tree): sync link/PROJECT_TREE.md' (#77) from chore/sync-link-tree into main
Reviewed-on: #77
2026-08-01 07:21:29 +00:00
04cd64b838 Merge branch 'main' into chore/sync-link-tree 2026-08-01 07:21:09 +00:00
916c11ee92 Merge pull request 'docs(tree): sync website/PROJECT_TREE.md' (#78) from chore/sync-website-tree into main
Reviewed-on: #78
2026-08-01 07:20:49 +00:00
runic-docs-bot
ea3755760d docs(tree): sync website/PROJECT_TREE.md from RunicGateway/website@5103b74 [skip ci] 2026-08-01 07:19:52 +00:00
runic-docs-bot
54b3002701 docs(tree): sync link/PROJECT_TREE.md from RunicGateway/link@295defb [skip ci] 2026-08-01 06:50:30 +00:00
9d98109628 Merge pull request 'docs!: Protocol 3.0 cutover — the 3.0 documentation set' (#73) from edge into main
Reviewed-on: #73
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-01 06:29:36 +00:00
d0808b766b Merge branch 'main' into edge 2026-08-01 06:28:21 +00:00
06b4a06baa Merge pull request 'docs(tree): sync website/PROJECT_TREE.md' (#59) from chore/sync-website-tree into main
Reviewed-on: #59
2026-07-28 15:02:41 +00:00
runic-docs-bot
f2fa6abff7 docs(tree): sync website/PROJECT_TREE.md from RunicGateway/website@a3407ae [skip ci] 2026-07-28 06:12:43 +00:00
4 changed files with 541 additions and 8 deletions

View File

@@ -85,6 +85,7 @@ android-app/
│ │ │ │ │ │ │ ├── PlayerShardDto.kt
│ │ │ │ │ │ │ ├── PostDto.kt
│ │ │ │ │ │ │ ├── PublicDto.kt
│ │ │ │ │ │ │ ├── ShardContentDto.kt
│ │ │ │ │ │ │ ├── ShardDto.kt
│ │ │ │ │ │ │ ├── SsoDto.kt
│ │ │ │ │ │ │ └── WikiDto.kt
@@ -106,6 +107,7 @@ android-app/
│ │ │ │ │ ├── NotificationsRepository.kt
│ │ │ │ │ ├── PlayerShardRepository.kt
│ │ │ │ │ ├── SettingsRepository.kt
│ │ │ │ │ ├── ShardFeaturesRepository.kt
│ │ │ │ │ ├── ShardRepository.kt
│ │ │ │ │ └── WikiRepository.kt
│ │ │ │ ├── di/
@@ -171,6 +173,8 @@ android-app/
│ │ │ │ │ ├── session/
│ │ │ │ │ │ └── SessionViewModel.kt
│ │ │ │ │ ├── shard/
│ │ │ │ │ │ ├── AtlasScreen.kt
│ │ │ │ │ │ ├── AtlasViewModel.kt
│ │ │ │ │ │ ├── ChampsScreen.kt
│ │ │ │ │ │ ├── ChampsViewModel.kt
│ │ │ │ │ │ ├── FrameFields.kt
@@ -180,7 +184,13 @@ android-app/
│ │ │ │ │ │ ├── GuildsViewModel.kt
│ │ │ │ │ │ ├── HousesScreen.kt
│ │ │ │ │ │ ├── HousesViewModel.kt
│ │ │ │ │ │ ├── LeaderboardsScreen.kt
│ │ │ │ │ │ ├── LeaderboardsViewModel.kt
│ │ │ │ │ │ ├── LiveBoard.kt
│ │ │ │ │ │ ├── MarketScreen.kt
│ │ │ │ │ │ ├── MarketViewModel.kt
│ │ │ │ │ │ ├── RulesScreen.kt
│ │ │ │ │ │ ├── RulesViewModel.kt
│ │ │ │ │ │ ├── ShardComponents.kt
│ │ │ │ │ │ ├── ShardEventText.kt
│ │ │ │ │ │ ├── ShardScreen.kt
@@ -284,6 +294,7 @@ android-app/
│ │ │ │ │ ├── PlayerShardDtoTest.kt
│ │ │ │ │ ├── PublicDtoTest.kt
│ │ │ │ │ ├── ShardBoardDtoTest.kt
│ │ │ │ │ ├── ShardContentDtoTest.kt
│ │ │ │ │ ├── ShardDtoTest.kt
│ │ │ │ │ ├── SsoDtoTest.kt
│ │ │ │ │ └── WikiDtoTest.kt
@@ -294,7 +305,8 @@ android-app/
│ │ │ │ └── FakeShardStream.kt
│ │ │ └── repository/
│ │ │ ├── AccountTrustedDevicesTest.kt
│ │ │ ── ConnectionVersionGuardTest.kt
│ │ │ ── ConnectionVersionGuardTest.kt
│ │ │ └── ShardFeaturesRepositoryTest.kt
│ │ ├── ui/
│ │ │ ├── admin/
│ │ │ │ ├── AdminContentViewModelTest.kt
@@ -304,7 +316,8 @@ android-app/
│ │ │ ├── contact/
│ │ │ │ └── ContactViewModelTest.kt
│ │ │ ├── navigation/
│ │ │ │ ── MenuAccessTest.kt
│ │ │ │ ── MenuAccessTest.kt
│ │ │ │ └── MenuFeatureGatingTest.kt
│ │ │ ├── notifications/
│ │ │ │ └── NotificationRoutingTest.kt
│ │ │ ├── player/
@@ -315,6 +328,8 @@ android-app/
│ │ │ │ ├── FrameFieldsTest.kt
│ │ │ │ ├── LiveBoardTest.kt
│ │ │ │ ├── ShardBoardViewModelTest.kt
│ │ │ │ ├── ShardContentHelpersTest.kt
│ │ │ │ ├── ShardContentViewModelTest.kt
│ │ │ │ └── ShardEventTextTest.kt
│ │ │ ├── theme/
│ │ │ │ └── BrandColorTest.kt

446
installer/PLAN.md Normal file
View File

@@ -0,0 +1,446 @@
# Runic Gateway Installer — plan
Status: **planning**. No installer code exists yet. This document is the design of record; it
supersedes the informal overview it grew out of, which described a ServUO integration that does not
match how `servuo-plugins` actually ships (see [Corrections](#corrections-to-the-original-overview)).
---
## 1. Purpose
Take a stock ServUO installation and configure it for Runic Gateway with minimal manual steps, while
keeping the components separated and independently maintainable.
The installer handles environment detection, ServUO overlay deployment, the optional stock-file
patch tier, uo-link installation and service registration, version tracking, diagnostics, and
updates from Gitea releases.
**It is a deployment tool, not a hosted bootstrapper.** There is no `curl | bash`, no installer
service, and no hosted bootstrap script. Artifacts are downloaded from a Gitea release page and run.
**It does not replace ServUO startup behavior.** ServUO keeps running through its existing
release/start scripts. The installer never writes a launcher.
### Decisions locked
| Question | Decision |
|---|---|
| Audience | **Public** — any ServUO operator, not just shards we run |
| Code signing | **Unsigned.** `SHA256SUMS` is the trust anchor; SmartScreen/Gatekeeper warnings are expected and documented, as with most self-hosted tooling |
| Language | **Rust** — single static binary per OS, reuses the cross-compile pattern already proven in `link/.gitea/workflows/release.yml` |
| Plugin source | **Release tarball artifact** — no git and no Gitea credentials on the shard host |
| Composition | **Published bundle manifest** (§7.1). CI names an exact, protocol-checked combination of component versions; the installer fetches it at run time and `--bundle <tag>` pins one. Component releases regenerate JSON, not the installer binary |
| Token handoff | **Print token + prefilled admin URL** at the end of the run |
| Repo | **New repo**, `RunicGateway/installer`. It deploys *both* other components, so living inside `link/` would invert the dependency |
| ServUO version | **Warn and skip.** Patches are verified against stock 57.4 only; on anything else the base install proceeds and the patch tier is skipped with a warning. Forks are the norm in a public audience — refusing outright would block most operators |
| Uninstall | **Never touches the ServUO tree.** Removes uo-link and its service entry, then *prints* the overlay files to delete and the patch hunks to revert. Reverting is the operator's call |
---
## 2. Corrections to the original overview
These are not wording nits — each one changes what the installer has to do.
### 2.1 There is no `RunicGateway.dll` and no `Plugins/` directory
The plugin ships as **C# source** and ServUO compiles it at boot. The real deployable is
`servuo-plugins/overlay/`, which mirrors the server root:
```
overlay/
├── Config/Bridge.cfg
└── Scripts/
├── Scripts.csproj # Phase 0 — whole-file overwrite of a stock file
└── Custom/Bridge/*.cs # 22 files
```
So the plugin step is a hash-compare file sync, not a DLL drop — mechanically easier than the
overview assumed. The sting is that **a successful copy does not mean a working bridge.** Per
`link/SHARD_PREREQS.md`, `ScriptCompiler.Compile()` shells out to `dotnet build`, prints the output,
**ignores the exit code**, and reloads the existing `Scripts.dll`. A broken script build is
invisible: the shard boots clean on stale code. Diagnostics must therefore verify *post-boot* state,
never treat "files copied" as success.
### 2.2 Stock ServUO files *are* modified — by an optional tier
`servuo-plugins/patches/` holds unified diffs against stock ServUO 57.4, plus two `.cs` files that
can only be copied *after* their patch lands (they reference symbols the patch introduces):
| Patch | Target | Companion file | Rebuild required |
|---|---|---|---|
| `playervendor-sale-eventsink.patch` | `Server/EventSink.cs` | `BridgeVendorSale.cs` | **Core**`dotnet build ServUO.sln`; the dynamic script build is not enough |
| `playervendor-sale-gump.patch` | `Scripts/Gumps/PlayerVendorGumps.cs` | (same unit as above) | script build |
| `commandlogging-event.patch` | `Scripts/Commands/Logging.cs` | `BridgeModerationAudit.cs` | script build |
Plus `overlay/Scripts/Scripts.csproj`, which overwrites a stock file (Phase 0 — it fixes the silent
ServUO build bug above).
This is the hardest part of the installer. `git apply` against a hand-modified shard will fail, and
most real shards are hand-modified. Therefore:
- The patch tier is **opt-in and skippable**. The base install must complete without it.
- Always dry-run (`git apply --check`) before applying, and report per-patch.
- When skipped or failed, say plainly what is lost: **no `vendor.sale` events, no in-game moderation
audit forwarding**.
- The `EventSink.cs` patch must warn loudly that a **core solution rebuild** is required, not just a
shard restart.
- On any ServUO version other than stock **57.4**, skip the whole tier with a warning and continue
with the base install. Do not attempt to apply unverified diffs to an unknown tree.
- Record applied patches in `install.json`, **and cache the applied `.patch` files** next to it
(`/etc/runicgateway/patches/`, `%ProgramData%\RunicGateway\patches\`). Re-runs stay idempotent,
and uninstall can print the exact hunks offline long after the release tarball is gone (§5,
Phase 4).
### 2.3 Config paths collide with what the sidecar actually reads
The sidecar reads `$UOLINK_CONFIG`, else `sidecar.toml` in the **working directory**
(`link/sidecar/src/config.rs`), with keys `[shard].bind`, `[web].bind`, `[web].auth_token`,
`[store].path`. The overview proposed a `config.toml` with `[updates]`, `[link]`, `[servuo]` — keys
the sidecar cannot read.
Two files, two owners:
| File | Owner | Contents |
|---|---|---|
| `/etc/runicgateway/sidecar.toml` | uo-link | The sidecar's own schema, unchanged. Service sets `UOLINK_CONFIG` to this path |
| `/etc/runicgateway/install.json` | installer | Deployed versions, file hashes, applied patches, ServUO path, timestamps |
**Working-directory trap:** the sidecar writes both `sidecar.toml` and `uo-link.db` relative to CWD.
Under `C:\Program Files\` that fails or silently lands in VirtualStore. The service definitions must
pin `UOLINK_CONFIG` and `UOLINK_DB_PATH` explicitly:
- Linux: config `/etc/runicgateway/sidecar.toml`, db `/var/lib/runicgateway/uo-link.db`, dedicated
service user
- Windows: binary under `%ProgramFiles%\RunicGateway\`, **data under `%ProgramData%\RunicGateway\`**
### 2.4 The token handoff was missing entirely
The whole point is the website reaching the sidecar, and today that is manual and undocumented in
the install flow: the sidecar generates a token on first run and logs it, then a human pastes base
URL, WS URL, token, and protocol version into Admin → Shard, where it is AES-GCM encrypted and
becomes write-only. This is the largest "I installed it and nothing happened" failure mode.
The installer closes it by printing a copy-paste block at the end of a successful run — see §6.
### 2.5 `deploy.ps1` cannot be the cross-platform deployer
It is PowerShell-only; a Linux ServUO host running .NET typically has no `pwsh`. It also hard-throws
when the ServUO process is running — correct behavior, and the installer must inherit it (detect and
refuse, rather than corrupt a live `Scripts.dll`). The installer reimplements the sync natively; it
is a short hash-compare-and-copy that never deletes.
`deploy.ps1` **stays** in `servuo-plugins` as the developer-facing tool. The installer is for
operators.
### 2.6 Prerequisites the overview assumed away
- **`servuo-plugins` has no release workflow.** Only `link` does. "Pull latest repository" is
replaced by a release tarball, which has to be built first (Phase 0).
- **arm64 is not buildable today.** `link/release.yml` cross-compiles only
`x86_64-unknown-linux-gnu` and `x86_64-pc-windows-gnu`. An arm64 `.deb` needs another cross
toolchain.
- **The compat matrix has no home.** `PROTOCOL_VERSION` currently lives only in
`link/sidecar/src/main.rs`. The sidecar publishes it via `X-UOLink-Version` and `/health`, and the
website stores an expected value — but the *plugin's* protocol version is not queryable before
boot. See §7.
---
## 3. Distribution model
Components are published as Gitea release artifacts. Operators download from the release page
(browser, `curl`/`wget`, or `scp` to the server) and run the binary.
```
Runic Gateway Installer v1.0.0
├── runicgateway-installer-windows-x86_64.exe
├── runicgateway-installer-linux-x86_64
└── SHA256SUMS
uo-link v3.x.y (existing release, extended)
├── uo-link-sidecar-windows-x86_64.exe
├── uo-link-sidecar-linux-x86_64
├── runicgateway-link_<ver>_amd64.deb (Phase 5)
└── SHA256SUMS
servuo-plugins v<ver> (new release, Phase 0)
├── runicgateway-overlay-<ver>.tar.gz # overlay/ + patches/ + manifest.json
└── SHA256SUMS
```
Binding those together is the **bundle manifest** (§7.1) — published by the installer repo's CI, not
by any component, and the thing the installer actually resolves against.
```bash
scp runicgateway-installer-linux-x86_64 user@server:/tmp/
chmod +x runicgateway-installer-linux-x86_64
sudo ./runicgateway-installer-linux-x86_64
```
### Unsigned-binary posture
Because releases are unsigned, trust is anchored on checksums and the operator's own verification.
The docs must state this up front rather than let users discover it as a scary dialog:
- Every release publishes `SHA256SUMS`; the install docs lead with the verification command for both
OSes.
- Windows will show a SmartScreen "unrecognized app" prompt. Documented, with the exact click path.
- The installer verifies the SHA256 of everything **it** downloads (overlay tarball, sidecar binary)
against the release's `SHA256SUMS` and refuses on mismatch. Self-verification is not optional just
because the installer itself is unsigned.
- Revisit signing if it ever becomes affordable; the release layout should not have to change.
---
## 4. Component architecture
```
Runic Gateway Installer (Rust, one binary per OS)
┌───────────────┴────────────────┐
▼ ▼
ServUO integration uo-link
│ │
┌────────┴────────┐ ┌────────┴────────┐
▼ ▼ ▼ ▼
overlay sync patch tier (opt-in) binary install service registration
(never deletes) (git apply + guard) + config + data (systemd / Windows SCM)
```
Each component keeps its own lifecycle. ServUO's existing startup process is untouched.
---
## 5. Phases
### Phase 0 — prerequisites (no installer code)
Repo work that must land before an installer can exist.
1. **`servuo-plugins`: add `.gitea/workflows/release.yml`.** Retarget the release *engine* half of
`link/release.yml` (its header comment explicitly anticipates this — the plan/release steps
consume only `{version, changelog, artifacts}`). The adapter half produces
`runicgateway-overlay-<ver>.tar.gz` containing `overlay/`, `patches/`, and a `manifest.json`
(version, commit, per-file SHA256, declared protocol version, minimum ServUO version).
2. **`link`: make the sidecar installable.** Confirm/settle default data paths, and add a way to
read back config non-interactively (e.g. `--print-config` emitting JSON: bind addresses, token,
protocol version, db path) so the installer does not have to scrape logs for the token.
3. **Bundle CI in the installer repo** (§7). Compose job (read both repos' latest releases → run the
two gates → publish `bundle.json`), the nightly cron, and the dispatch step appended to each
component's release workflow. This must exist before Phase 1 is useful, since the installer
resolves what to install *from* the bundle.
4. **`docs`: this file, plus `docs/installer/INSTALL.md`** (the operator-facing guide) once the
shape is settled.
### Phase 1 — installer core
- ServUO root detection and validation (`ServUO.exe`, `Scripts/`, `Config/`), with version detection
and an explicit refusal when the ServUO process is running.
- Overlay sync: fetch tarball → verify SHA256 → hash-compare against the server tree → add/change,
**never delete**. Port of `deploy.ps1` semantics including its `-Verify` dry run (`--verify`).
- Write `install.json`: component, version, source commit, per-file hashes, applied patches,
timestamp.
- Idempotent re-runs; a second run with no upstream change reports "unchanged" and writes nothing.
### Phase 2 — uo-link install and service
- Linux: binary → `/usr/bin/runicgateway-link`, config → `/etc/runicgateway/sidecar.toml`, db →
`/var/lib/runicgateway/`, systemd unit with a dedicated user, `enable` + `start`.
- Windows: `%ProgramFiles%\RunicGateway\`, data in `%ProgramData%\RunicGateway\`, service
registration with automatic start and restart-on-failure.
- Both: `UOLINK_CONFIG` and `UOLINK_DB_PATH` pinned in the service definition (§2.3).
- Token surfacing (§6).
### Phase 3 — patch tier (opt-in)
Everything in §2.2. Detect applicability, dry-run, apply, record, warn about the core rebuild, and
degrade loudly rather than silently.
### Phase 4 — diagnostics and updates
`runicgateway doctor` — the command that makes the whole thing supportable:
```
✓ ServUO found /opt/ServUO (57.4)
✓ Overlay in sync 23 files, all hashes match install.json
⚠ Patch tier 1 of 3 applied — vendor.sale unavailable
✓ uo-link installed 3.0.1
✓ Service running, enabled
✓ Sidecar reachable 127.0.0.1:8080 /health ok
✓ Protocol sidecar 3 = overlay manifest 3
✗ Shard connected no shard has dialed in since boot
```
The last check matters most: it is the only thing that distinguishes "files copied" from "the bridge
actually works" (§2.1).
`runicgateway update` — resolves the current bundle (§7.1), then acts asymmetrically by component,
deliberately:
- **uo-link**: compare the bundle's version against what is installed → download → verify checksum →
replace binary → restart service.
- **plugin overlay**: download the bundle's overlay tarball → verify → re-sync → record commit →
tell the operator ServUO must restart (the installer does not restart the shard).
Because both come from one bundle, an update always moves to a combination whose protocol versions
were checked together, rather than to two independently-latest artifacts that may disagree.
`runicgateway uninstall` — **removes only what it exclusively owns, and never edits the ServUO
tree.** The installer cannot know what the operator has changed in those files since deployment, so
a clever automatic revert risks silently eating their work. It removes and it reports:
| Action | Scope |
|---|---|
| Removed | uo-link binary, its service entry (systemd unit / Windows service), `install.json` and the cached patch set |
| Kept | `sidecar.toml` and `uo-link.db` (config and history survive; `--purge` to drop them) |
| **Printed, not done** | Every overlay file deployed into the ServUO tree, listed by path, for the operator to delete |
| **Printed, not done** | The exact hunks each applied patch added to `EventSink.cs`, `PlayerVendorGumps.cs`, `Logging.cs`, rendered from the cached `.patch` files, for the operator to revert by hand |
The printed report is also written to a file, so it survives the terminal scrollback of a long
uninstall.
### Phase 5 — packaging polish
`.deb` packaging, Windows MSI, arm64 cross build, and optional automated backup before upgrade.
Deliberately last: v1 can register services directly (`sc create` / a written systemd unit) and ship
plain binaries. Nothing in Phases 14 should have to change to add these.
---
## 6. Token handoff (the end of a successful run)
```
Runic Gateway is installed.
One manual step remains — connect the website to this sidecar:
Base URL http://<this-host>:8080
WebSocket URL ws://<this-host>:8080/ws
Protocol version 3
Auth token 4f9c... (also in /etc/runicgateway/sidecar.toml)
Paste these into Admin → Shard on your Runic Gateway site:
https://<your-site>/admin/shard
The token is write-only once saved — the site will never show it back to you.
```
The installer prompts for the site URL only to build that link; it never contacts the website. A
future "installer registers itself with the website" flow (claim code + authenticated endpoint) is
explicitly **out of scope** — it is real backend work in a security-sensitive area and can be added
later without changing anything here.
---
## 7. Version tracking, the bundle, and release orchestration
Three components version independently, bound by a protocol contract:
- **sidecar** — `PROTOCOL_VERSION` in `link/sidecar/src/main.rs`, exposed on `/health` and as
`X-UOLink-Version` on every response; a mismatch is rejected `409`.
- **website** — stores an expected protocol version in `uoLinkConfig` (admin-managed).
- **plugin overlay** — has no queryable version before ServUO boots. The overlay release
`manifest.json` declares it, and `install.json` records what was deployed.
### 7.1 The bundle manifest
**The bundle is the compat matrix.** Rather than the installer hardcoding versions or blindly
resolving "latest", CI publishes a small manifest naming an exact, checked combination:
```json
{
"bundle": "2026.08.01",
"protocol": 3,
"link": { "version": "3.0.1", "sha256": "a91f..." },
"overlay": { "version": "2.4.0", "commit": "a81f42c", "sha256": "7c3e..." }
}
```
The installer fetches the current bundle at run time; `--bundle <tag>` pins an older one for a
reproducible install. Because the bundle is data, **a new `link` release regenerates ~20 lines of
JSON and leaves the installer binary untouched** — operators do not re-download the installer to
pick up a sidecar patch, and the installer does not accumulate releases whose code is byte-identical.
Two gates run at compose time, both cheap and both worth it:
1. The sidecar's `PROTOCOL_VERSION` must equal the overlay manifest's declared protocol version.
This is the check that catches an `edge`/`main` protocol mismatch before it reaches an operator.
2. Every referenced asset must exist and its SHA256 must match the publishing repo's `SHA256SUMS`.
### 7.2 What triggers a bundle
| Trigger | Why |
|---|---|
| `link` publishes a release | Its release job `POST`s to the installer repo's workflow-dispatch endpoint as its final step. `link/.gitea/workflows/release.yml` already declares `workflow_dispatch: {}` and already holds a `write:repository` token |
| `servuo-plugins` publishes a release | Same, once Phase 0 gives it a release workflow |
| Nightly cron on the installer repo | Recomputes from whatever the latest releases actually are, so a missed or failed dispatch self-heals instead of silently pinning operators to a stale sidecar |
`repository_dispatch` is deliberately avoided — support for it is uncertain on this Gitea version,
whereas dispatching an existing `workflow_dispatch` workflow via the API works today.
### 7.3 Stale-overlay handling: dispatch, don't wait
Each component **self-releases on merge to its own `main`**, using the same conventional-commit
engine. Note that "updated since the last release" must mean *releasable* commits — the engine sets
`RELEASE=false` when nothing but `docs:`/`chore:` has landed, so a docs typo correctly does **not**
cut an overlay release, and the bundle keeps using the existing one.
So by the time the installer's CI looks, the release normally already exists. If it finds
`servuo-plugins` main ahead of its latest release *with* releasable commits, it:
1. fires that repo's release workflow via workflow-dispatch and **does not wait for it**,
2. composes this bundle from the assets that exist right now,
3. writes a loud warning into the job summary.
The new overlay release lands minutes later on its own and the nightly cron folds it into the next
bundle. This gets the automation without the flaky part: dispatching another repo's workflow is
fine — that workflow still runs its own gates — but *polling* it is not, because Gitea's dispatch
endpoint returns no run handle, so the job would have to guess which run is its own and hold a
runner idle meanwhile. The warning exists so a genuinely broken release workflow surfaces once
rather than being silently retriggered every night forever.
### 7.4 Open risk
The v3 cutover is mid-flight — protocol work landed on `edge` branches with the `edge → main`
cutover still open across four repos. Until that lands, `main` and `edge` disagree about
`PROTOCOL_VERSION`, so the installer must not hardcode a version anywhere; it reads what the
artifacts declare, and §7.1's gate 1 is what stops a mismatched pair from being published as a
bundle. See `docs/link/v3.md`.
---
## 8. Open questions
1. **Windows service mechanism**`sc create` against the plain console binary (simplest, works
today), a bundled WinSW/NSSM shim, or a native `--service` mode in the sidecar using the
`windows-service` crate (cleanest, but changes `link`). Recommendation: `sc create` for v1,
revisit if restart semantics prove inadequate.
2. **Does the installer manage ServUO stop/start?** Currently it refuses while ServUO runs and tells
the operator to restart afterward. Offering to stop/start would be friendlier but means owning
another shard's process lifecycle, and the shard's own start scripts vary.
3. **Co-location assumption** — the shard dials out to the sidecar on loopback `127.0.0.1:7788`, so
sidecar and ServUO must share a host. Should the installer support installing only uo-link on a
different host, or hard-assume co-location?
4. **Branch targeting for the new repo**`link`, `website`, `servuo-plugins` and `docs` are
mid-cutover between `edge` and `main`. The installer repo starts clean on `main`; the Phase 0
`servuo-plugins` release workflow needs a target branch decision.
Resolved and moved into §1 / §2.2 / §5: uninstall scope, and minimum ServUO version.
---
## 9. Administrator experience
Before:
```
find plugins → copy files → edit ServUO → download bridge → start bridge
→ configure startup → find the token → troubleshoot paths
```
After:
```
download artifact → verify checksum → run installer → select ServUO directory
→ install components → paste 4 values into Admin → Shard → start ServUO normally
```

View File

@@ -18,6 +18,7 @@ link/
│ ├── scripts/
│ │ └── gen_tree.py
│ ├── workflows/
│ │ ├── pr-checks.yml
│ │ ├── release.yml
│ │ ├── sonarqube.yml
│ │ └── sync-project-tree.yml

View File

@@ -164,6 +164,7 @@ website/
│ │ │ ├── heroLayout.js
│ │ │ ├── shardEvents.js
│ │ │ ├── useAsync.js
│ │ │ ├── useShardFeatures.js
│ │ │ └── useShardFeed.js
│ │ ├── routes/
│ │ │ ├── admin/
@@ -190,6 +191,8 @@ website/
│ │ │ │ │ ├── SettingsAdmin.jsx
│ │ │ │ │ ├── ShardAdmin.jsx
│ │ │ │ │ ├── ShardOps.jsx
│ │ │ │ │ ├── ShardVisibility.jsx
│ │ │ │ │ ├── SpawnAtlas.jsx
│ │ │ │ │ ├── UserDetail.jsx
│ │ │ │ │ ├── UserEditor.jsx
│ │ │ │ │ ├── UsersAdmin.jsx
@@ -213,17 +216,23 @@ website/
│ │ │ │ └── ResetPassword.jsx
│ │ │ ├── public/
│ │ │ │ ├── About.jsx
│ │ │ │ ├── Atlas.jsx
│ │ │ │ ├── AtlasCreature.jsx
│ │ │ │ ├── ChampSpawns.jsx
│ │ │ │ ├── CmsPage.jsx
│ │ │ │ ├── FiveOnFriday.jsx
│ │ │ │ ├── Governors.jsx
│ │ │ │ ├── Guilds.jsx
│ │ │ │ ├── Houses.jsx
│ │ │ │ ├── Leaderboards.jsx
│ │ │ │ ├── Maintenance.jsx
│ │ │ │ ├── Market.jsx
│ │ │ │ ├── MarketVendor.jsx
│ │ │ │ ├── News.jsx
│ │ │ │ ├── Newsletter.jsx
│ │ │ │ ├── NewsletterIssue.jsx
│ │ │ │ ├── Portal.jsx
│ │ │ │ ├── Rules.jsx
│ │ │ │ ├── Screenshots.jsx
│ │ │ │ ├── Shard.jsx
│ │ │ │ ├── ShardActivity.jsx
@@ -257,9 +266,12 @@ website/
│ └── sonar-test-reporter.mjs
├── server/
│ ├── db/
│ │ ├── data/
│ │ │ └── spawnAtlas.art.example.json
│ │ ├── schema.sql
│ │ └── seed.js
│ ├── scripts/
│ │ ├── importSpawnAtlas.js
│ │ └── routeManifest.js
│ ├── src/
│ │ ├── auth/
@@ -365,15 +377,27 @@ website/
│ │ │ ├── settings/
│ │ │ │ ├── settings.db.js
│ │ │ │ └── settings.model.js
│ │ │ ├── shardAtlas/
│ │ │ │ ├── shardAtlas.db.js
│ │ │ │ └── shardAtlas.model.js
│ │ │ ├── shardClilocs/
│ │ │ │ ├── shardClilocs.db.js
│ │ │ │ └── shardClilocs.model.js
│ │ │ ├── shardEvents/
│ │ │ │ ├── shardEvents.db.js
│ │ │ │ └── shardEvents.model.js
│ │ │ ├── shardLinks/
│ │ │ │ ├── shardLinks.db.js
│ │ │ │ └── shardLinks.model.js
│ │ │ ├── shardMarket/
│ │ │ │ ├── shardMarket.db.js
│ │ │ │ └── shardMarket.model.js
│ │ │ ├── shardState/
│ │ │ │ ├── shardState.db.js
│ │ │ │ └── shardState.model.js
│ │ │ ├── shardVisibility/
│ │ │ │ ├── shardVisibility.db.js
│ │ │ │ └── shardVisibility.model.js
│ │ │ ├── trustedDevices/
│ │ │ │ ├── trustedDevices.db.js
│ │ │ │ └── trustedDevices.model.js
@@ -398,12 +422,14 @@ website/
│ │ │ │ │ ├── account.router.js
│ │ │ │ │ ├── activity.router.js
│ │ │ │ │ ├── admin.controller.js
│ │ │ │ │ ├── admin.routes.js
│ │ │ │ │ ├── authProviders.controller.js
│ │ │ │ │ ├── authProviders.router.js
│ │ │ │ │ ├── botActivity.controller.js
│ │ │ │ │ ├── botActivity.router.js
│ │ │ │ │ ├── dashboard.router.js
│ │ │ │ │ ├── discordBot.controller.js
│ │ │ │ │ ├── discordBot.router.js
│ │ │ │ │ ├── email.router.js
│ │ │ │ │ ├── emailConfig.controller.js
│ │ │ │ │ ├── imageUpload.js
│ │ │ │ │ ├── index.js
@@ -414,16 +440,25 @@ website/
│ │ │ │ │ ├── pages.controller.js
│ │ │ │ │ ├── pages.router.js
│ │ │ │ │ ├── posts.router.js
│ │ │ │ │ ├── settings.router.js
│ │ │ │ │ ├── shard.router.js
│ │ │ │ │ ├── shardAtlas.controller.js
│ │ │ │ │ ├── shardClilocs.controller.js
│ │ │ │ │ ├── shardOps.controller.js
│ │ │ │ │ ├── shardVisibility.controller.js
│ │ │ │ │ ├── uoLink.controller.js
│ │ │ │ │ ├── uoLink.router.js
│ │ │ │ │ ├── uploads.router.js
│ │ │ │ │ ├── users.router.js
│ │ │ │ │ ├── usersShard.controller.js
│ │ │ │ │ └── wiki.router.js
│ │ │ │ ├── auth/
│ │ │ │ │ ├── auth.controller.js
│ │ │ │ │ ├── auth.routes.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── invite.controller.js
│ │ │ │ │ ├── invite.router.js
│ │ │ │ │ ├── login.router.js
│ │ │ │ │ ├── loginGuards.js
│ │ │ │ │ ├── me.routes.js
│ │ │ │ │ ├── mobile.controller.js
│ │ │ │ │ ├── mobile.routes.js
@@ -431,7 +466,10 @@ website/
│ │ │ │ │ ├── mobileSso.routes.js
│ │ │ │ │ ├── notifications.controller.js
│ │ │ │ │ ├── notifications.routes.js
│ │ │ │ │ ├── password.router.js
│ │ │ │ │ ├── passwordReset.controller.js
│ │ │ │ │ ├── register.router.js
│ │ │ │ │ ├── session.router.js
│ │ │ │ │ ├── sso.controller.js
│ │ │ │ │ ├── sso.routes.js
│ │ │ │ │ └── trustDevice.helper.js
@@ -439,13 +477,23 @@ website/
│ │ │ │ │ ├── internal.controller.js
│ │ │ │ │ └── internal.routes.js
│ │ │ │ ├── player/
│ │ │ │ │ ├── account.router.js
│ │ │ │ │ ├── appeals.controller.js
│ │ │ │ │ ├── player.routes.js
│ │ │ │ │ ── shard.controller.js
│ │ │ │ │ ├── appeals.router.js
│ │ │ │ │ ── index.js
│ │ │ │ │ ├── shard.controller.js
│ │ │ │ │ └── shard.router.js
│ │ │ │ ├── public/
│ │ │ │ │ ├── atlas.controller.js
│ │ │ │ │ ├── atlas.router.js
│ │ │ │ │ ├── index.js
│ │ │ │ │ ├── pages.router.js
│ │ │ │ │ ├── posts.router.js
│ │ │ │ │ ├── public.controller.js
│ │ │ │ │ ├── public.routes.js
│ │ │ │ │ ── shard.controller.js
│ │ │ │ │ ├── shard.controller.js
│ │ │ │ │ ── shard.router.js
│ │ │ │ │ ├── site.router.js
│ │ │ │ │ └── wiki.router.js
│ │ │ │ └── v1.router.js
│ │ │ ├── api.router.js
│ │ │ ├── cspReport.controller.js
@@ -455,6 +503,8 @@ website/
│ │ │ ├── auth.js
│ │ │ ├── botInternalClient.js
│ │ │ ├── botInternalKey.js
│ │ │ ├── clilocParse.js
│ │ │ ├── clilocSource.js
│ │ │ ├── db.js
│ │ │ ├── logger.js
│ │ │ ├── mailer.js
@@ -465,6 +515,9 @@ website/
│ │ │ ├── shardBroadcast.js
│ │ │ ├── shardIngest.js
│ │ │ ├── shardSales.js
│ │ │ ├── shardVisibility.js
│ │ │ ├── spawnAtlasParse.js
│ │ │ ├── spawnAtlasSource.js
│ │ │ ├── totp.js
│ │ │ ├── trustProxy.js
│ │ │ ├── uoLinkClient.js
@@ -483,11 +536,14 @@ website/
│ │ ├── appeals.pure.test.js
│ │ ├── appeals.test.js
│ │ ├── appLinks.test.js
│ │ ├── atlasController.test.js
│ │ ├── authController.test.js
│ │ ├── authMe.test.js
│ │ ├── authTrustedDevice.test.js
│ │ ├── botInternalKey.test.js
│ │ ├── botScore.test.js
│ │ ├── clilocParse.test.js
│ │ ├── clilocSource.test.js
│ │ ├── csp.test.js
│ │ ├── emailConfig.model.test.js
│ │ ├── honeypot.test.js
@@ -521,17 +577,32 @@ website/
│ │ ├── secretBox.test.js
│ │ ├── selfTrustedDevices.test.js
│ │ ├── session.test.js
│ │ ├── shardBroadcast.visibility.test.js
│ │ ├── shardControllerPublic.test.js
│ │ ├── shardIngest.champsPages.test.js
│ │ ├── shardIngest.market.test.js
│ │ ├── shardIngest.points.test.js
│ │ ├── shardIngest.protocol2.test.js
│ │ ├── shardIngest.ruleset.test.js
│ │ ├── shardMarket.model.test.js
│ │ ├── shardState.governorTerms.test.js
│ │ ├── shardState.model.test.js
│ │ ├── shardVisibility.test.js
│ │ ├── spawnAtlas.parse.test.js
│ │ ├── spawnAtlas.source.test.js
│ │ ├── ssoCallback.test.js
│ │ ├── ssoState.test.js
│ │ ├── ssoTrustedDevice.test.js
│ │ ├── totp.test.js
│ │ ├── trustedDevices.test.js
│ │ ├── trustProxy.test.js
│ │ ├── uoLinkClient.test.js
│ │ └── usernamePolicy.test.js
│ ├── tools/
│ │ └── cliloc-export/
│ │ ├── clilocexport.csproj
│ │ ├── Program.cs
│ │ └── README.md
│ ├── .env.example
│ ├── package-lock.json
│ ├── package.json