7 Commits

Author SHA1 Message Date
9cc109910c Merge pull request 'fix(handoff): print the shard screen's real path' (#22) from fix/admin-shard-path into main
Some checks failed
sync-project-tree / sync (push) Successful in 6s
Release installer / release (push) Failing after 4m3s
Reviewed-on: #22
2026-08-24 16:30:49 +00:00
6da385425e fix(handoff): print the shard screen's real path
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m39s
The end-of-run block told operators to paste the four values at
`<site>/admin/shard`. That page moved when the shard screens became part of the
`uo` module: a module owns one path segment wherever it appears (website
`MODULE_SYSTEM.md` §2.8), so it is `/admin/uo/link`, labelled "Shard (uo-link)".

The old path is worse than a 404. The SPA has no route for it, so it sends the
operator to the dashboard — the link looks like it worked, and the values they
were told to paste have nowhere to go.

- The path is now a named constant, `ADMIN_SHARD_PATH`, carrying why it is not
  the obvious string and the fact that API routes are NOT affected by the module
  namespacing rule (they keep `/api/v1/admin/shard/*`).
- Both handoff tests assert the new path, so this cannot regress quietly.
- The two user-facing labels that name the screen — the `--site-url` help text
  and `update`'s protocol-change instruction — say "Admin → Shard (uo-link)",
  matching what the sidebar actually reads.

Found while writing the runicgateway.com installation journey, by pasting the
printed link into a real deployment and landing on the dashboard.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-24 11:22:45 -05:00
5d4c68eaf5 Merge pull request 'docs(backup): correct why the sidecar database is not backed up (Teams cutover 3/6)' (#21) from edge into main
All checks were successful
Release installer / release (push) Successful in -51s
sync-project-tree / sync (push) Successful in 9s
Compose bundle / compose (push) Successful in 23s
Reviewed-on: #21
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-19 08:56:06 +00:00
c3771d22f2 Merge pull request 'docs(backup): correct why the sidecar database is not backed up' (#20) from feat/teams-phase1-guild-roster into edge
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m46s
Reviewed-on: #20
2026-08-17 19:28:25 +00:00
6c49217e9c docs(backup): correct why the sidecar database is not backed up
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m49s
backup.rs justified skipping the sidecar's database on two claims. Protocol 4
falsifies one and reveals the other was already wrong.

It said the database is safe because store.rs creates every table IF NOT EXISTS.
That held only while every schema change added a whole table — which, up to and
including Protocol 3.0, every one of them did. Protocol 4 adds a COLUMN to a table
that already exists, which IF NOT EXISTS cannot do, so link now carries a real
migration. A run can change the database's structure, not only its contents.

It also said every table holds state the sweeps repopulate. `events` does not: it
is never pruned, and the website backfills what it missed from GET /history on
every reconnect. So a lost database costs the gap-recovery window for whatever
happened while the site was down. That claim was untrue before this workstream
existed.

The behaviour does not change — the database is still not copied — because the
argument against backing up unbounded bulk survives both corrections: `events`
grows without limit, the migration is transactional and additive, and the website
holds its own durable copy of everything already ingested. Only the reasoning was
wrong, and a wrong reason left in place is what lets the next person extend it to
a case it never covered.

Whether that unbounded table should be pruned or protected belongs to link, on its
own merits, rather than being settled inside a backup policy.

No logic change; docs only.

Refs: docs/website/TEAMS.md Part 12 Phase 1

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-17 12:58:24 -05:00
7758724eb5 Merge pull request 'docs(readme): describe a released installer, not an unreleased one' (#19) from docs/installer-first-setup into main
All checks were successful
sync-project-tree / sync (push) Successful in 7s
Release installer / release (push) Successful in -35s
Compose bundle / compose (push) Successful in 20s
Reviewed-on: #19
Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
2026-08-07 21:30:39 +00:00
484f00ddee docs(readme): describe a released installer, not an unreleased one
All checks were successful
PR Checks / rust-gates (pull_request) Successful in 1m17s
The status section still said "Phases 1 to 4 are built, on edge. Nothing
is released yet" and told visitors the way to install was by hand — both
untrue as of v0.1.0.

- Add an "Install a shard with it" section up top: verify, run, paste the
  four values. That is what a visitor to this repo is here for.
- Status: released, all five phases, both cutover gates recorded.
- Fix a broken link: bundles/current.json is no longer on main, it lives
  on the bundles branch.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-07 16:05:56 -05:00
5 changed files with 95 additions and 44 deletions

View File

@@ -25,9 +25,48 @@ It also **does not replace ServUO startup behavior.** ServUO keeps running throu
its existing release/start scripts; the installer never writes a launcher and
never restarts the shard.
## Install a shard with it
Grab a binary and `SHA256SUMS` from the
[releases page](https://gitea.whitlocktech.com/RunicGateway/installer/releases),
verify the checksum, and run it as Administrator/root against a **stopped** shard:
```bash
sha256sum -c SHA256SUMS --ignore-missing
chmod +x runicgateway-installer-linux-x86_64
sudo ./runicgateway-installer-linux-x86_64 install
```
```powershell
# Windows, from an elevated PowerShell
.\runicgateway-installer-windows-x86_64.exe install
```
It ends by printing the four values to paste into **Admin → Shard** on your site.
The full operator guide — what it asks, where it writes, the patch tier, day-two
commands and troubleshooting — is
[`installer/INSTALL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md).
Prefer to place everything yourself, or on a host that cannot run the binary?
[INSTALL.md Appendix A](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md#appendix-a--installing-by-hand)
is the same deployment done with `curl`, `tar` and `systemctl`, and stays
supported.
## Status
**Phases 1 to 4 are built, on the `edge` branch. Nothing is released yet.**
**Released.** All five phases are built and the `edge → main` cutover (#17) cut
the first release, [`v0.1.0`](https://gitea.whitlocktech.com/RunicGateway/installer/releases),
publishing `linux-x86_64`, `linux-aarch64` and `windows-x86_64.exe` with
`SHA256SUMS`.
| Phase | State |
|---|---|
| 0 — prerequisites in the other repos | ✅ merged |
| 1 — installer core: bundle resolution, ServUO detection, overlay sync, `install.json` | ✅ released |
| 2 — uo-link install + service registration | ✅ released |
| 3 — the opt-in stock-file patch tier | ✅ released |
| 4 — `doctor`, `update`, `uninstall` | ✅ released |
| 5 — packaging polish: Linux `aarch64`, backup before overwrite | ✅ released |
The binary does everything
[`installer/INSTALL.md`](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md)
@@ -40,39 +79,18 @@ The design of record is
in the docs repo: phases, locked decisions, and the Phase 0 prerequisites in other
repos (a `servuo-plugins` release workflow, a non-interactive config read-back in
`link`, and the bundle-manifest CI here), all of which have landed —
[`bundles/current.json`](bundles/current.json) names the current protocol-checked
sidecar + overlay combination, recomposed on every component release and nightly
(see [`bundles/README.md`](bundles/README.md)).
[`bundles/current.json`](https://gitea.whitlocktech.com/RunicGateway/installer/src/branch/bundles/current.json)
names the current protocol-checked sidecar + overlay combination, recomposed on
every component release and nightly (see [`bundles/README.md`](bundles/README.md)).
| Phase | State |
|---|---|
| 0 — prerequisites in the other repos | ✅ merged |
| 1 — installer core: bundle resolution, ServUO detection, overlay sync, `install.json` | ✅ on `edge` |
| 2 — uo-link install + service registration | ✅ on `edge` |
| 3 — the opt-in stock-file patch tier | ✅ on `edge` |
| 4 — `doctor`, `update`, `uninstall` | ✅ on `edge` |
| 5 — packaging polish: Linux `aarch64`, backup before overwrite | in progress |
**Why `edge`:** `release.yml` publishes an installer binary on every push to
`main`, so nothing lands there until the whole tool is worth handing to an
operator. The `edge → main` cutover cuts the first release. PRs into `edge` run
the same gates as PRs into `main`.
**What the cutover is waiting on**, per PLAN.md §5:
1. **Phase 5**, packaging polish — deliberately *before* the first release rather
than after it, because it changes the release layout, and shipping first would
mean a first release immediately superseded by the next. There is no `.deb`
and no MSI: both would give the sidecar binary, its service unit and its
service account a second owner beside this tool.
2. **The Windows SCM half verified on a real host.** `sc create`, the virtual
service account, the failure actions and the token-file ACL have never been
executed anywhere. Running the *systemd* half for real is what turned up a bug
no unit test had, so this is not a formality.
Until the cutover, the way to install is by hand —
[INSTALL.md Appendix A](https://gitea.whitlocktech.com/RunicGateway/docs/src/branch/main/installer/INSTALL.md#appendix-a--installing-by-hand)
is the same deployment done with `curl`, `tar` and `systemctl`.
**`main` publishes.** `release.yml` cuts a release from every push to `main`, which
is why the crate was integrated on `edge` until it was worth handing to an
operator. Both cutover gates were met first: Phase 5 (its scope settled as **no
`.deb` and no MSI** — either would give the sidecar binary, its service unit and
its service account a second owner beside this tool), and the **Windows SCM half
verified on a real host**. That second one earned its place: `sc start` failed
with 1053 on its first real run and needed a sidecar fix (link#29) before it
passed 13/13.
## Related repos

View File

@@ -3,10 +3,27 @@
//! ## Scoped by what cannot be fetched again
//!
//! Most of what this installer writes is replaceable: the sidecar binary and every overlay file are
//! re-downloadable and hash-named in the bundle, and the sidecar's database is a cache with a schema
//! — `link`'s `store.rs` creates every table `IF NOT EXISTS` and every one of them holds shard state
//! the sweeps repopulate. Backing those up would be bulk with no recovery value, and the bulk is not
//! free: it would bury the two things that matter.
//! re-downloadable and hash-named in the bundle, and the sidecar's database is overwhelmingly a
//! projection of shard state that the sweeps repopulate. Backing it up would be bulk with little
//! recovery value, and the bulk is not free: it would bury the two things that matter.
//!
//! That reasoning used to be stated two ways that are no longer true, and the correction is worth
//! keeping rather than quietly deleting:
//!
//! - It said the database is safe because `store.rs` creates every table `IF NOT EXISTS`. That held
//! only while every schema change added a whole *table*. Protocol 4 adds a *column* to a table
//! that already exists, which `IF NOT EXISTS` cannot do, so `link` now carries a real migration
//! (`PRAGMA user_version` steps). A run can therefore change the database's structure, not just
//! its contents.
//! - It said every table holds state the sweeps repopulate. `events` does not: it is never pruned,
//! and the website backfills the events it missed from `GET /history` on every reconnect. So a
//! lost database costs the gap-recovery window for anything that happened while the site was down.
//!
//! The decision is unchanged — this still does not copy the database — because the argument against
//! backing up unbounded bulk survives both corrections: `events` grows without limit, the migration
//! is transactional and additive, and the website holds its own durable copy of everything it has
//! already ingested. Only the *reason* was wrong. Whether that table should be pruned or protected
//! is a question for `link`, on its own merits, not something to settle inside a backup policy.
//!
//! What a run can destroy irrecoverably is short:
//!

View File

@@ -77,7 +77,7 @@ pub struct Cli {
pub patches_unsupported_servuo: bool,
/// `--host <name>`: the hostname to print in the website URLs.
pub host: Option<String>,
/// `--site-url <url>`: the site's base URL, for the Admin → Shard link.
/// `--site-url <url>`: the site's base URL, for the Admin → Shard (uo-link) link.
pub site_url: Option<String>,
/// `--yes`: assume the default answer to every prompt.
pub assume_yes: bool,
@@ -137,7 +137,7 @@ Options:
--host <NAME> install. The hostname to print in the
website URLs.
--site-url <URL> install. Your site's base URL, for the
Admin → Shard link.
Admin → Shard (uo-link) link.
--yes Assume the default answer to every prompt.
On uninstall it means yes: that prompt
defaults to no, and typing `uninstall

View File

@@ -294,6 +294,18 @@ fn port_of(bind: &str) -> &str {
}
}
/// Where the shard settings live in the website's admin panel.
///
/// NOT `/admin/shard`, which is what this printed until 2026-08-24 and what an operator who ran
/// an older build still has in their scrollback. Those screens belong to the `uo` MODULE now, and
/// a module owns one path segment wherever it appears (website `MODULE_SYSTEM.md` §2.8), so the
/// page moved. The old path does not 404 — the SPA sends it to the dashboard, which is the worst
/// way for a link in a handoff to be wrong, because it looks like it worked.
///
/// API routes are NOT affected by that rule and keep `/api/v1/admin/shard/*`. This is the SPA URL
/// a person types.
const ADMIN_SHARD_PATH: &str = "/admin/uo/link";
/// The end-of-run block from PLAN.md §6 — the one manual step the installer cannot do.
///
/// Returned as a string rather than printed so it can be tested, and so the caller decides where it
@@ -312,12 +324,13 @@ pub fn handoff(doc: &ConfigDoc, host: &str, site_url: Option<&str>) -> String {
Protocol version {protocol}\n \
Auth token {token}\n \
(also in {config})\n\n\
Paste these into Admin → Shard on your Runic Gateway site:\n \
{site}/admin/shard\n\n\
Paste these into Admin → Shard (uo-link) on your Runic Gateway site:\n \
{site}{admin_path}\n\n\
The token is write-only once saved — the site will never show it back to you.\n",
protocol = doc.protocol,
token = doc.web.auth_token,
config = doc.config_path,
admin_path = ADMIN_SHARD_PATH,
)
}
@@ -401,7 +414,7 @@ mod tests {
assert!(block.contains(&doc.web.auth_token), "{block}");
// The trailing slash on the site URL must not produce a double slash in the link.
assert!(
block.contains("https://my-site.example/admin/shard"),
block.contains("https://my-site.example/admin/uo/link"),
"{block}"
);
assert!(block.contains("/etc/runicgateway/sidecar.toml"), "{block}");
@@ -412,7 +425,10 @@ mod tests {
// An unattended run has nobody to ask, and the token is far too useful to withhold over a
// link the operator does not need.
let block = handoff(&doc(), "shard", None);
assert!(block.contains("https://<your-site>/admin/shard"), "{block}");
assert!(
block.contains("https://<your-site>/admin/uo/link"),
"{block}"
);
assert!(block.contains("4f9c"), "{block}");
}

View File

@@ -81,7 +81,7 @@ pub fn closing(prior: Option<&InstallRecord>, bundle: &Bundle, now: &InstallReco
println!();
ui::warn(&format!(
"The protocol version changed: {}{}.\n \
Update the Protocol version field in Admin → Shard on your website. Nothing else \
Update the Protocol version field in Admin → Shard (uo-link) on your website. Nothing else \
changed —\n the URLs and the auth token are the same, and the sidecar answers a \
website still set to\n {} with 409 rather than mis-parsing it.",
previous_protocol.unwrap_or(bundle.protocol),