fix(rust): protocol 13 — a configuration save that settles after its reload (F9, D179)
Some checks failed
PR Checks / server-tests (pull_request) Failing after 16s
PR Checks / client-build (pull_request) Successful in 17s
PR Checks / frozen-manifest (pull_request) Failing after 37s

The plugin now answers a save once the files are written, with pending and
a writeId, and reports the reload later as a config.outcome event. The save
is recorded as reloading with a settle_by of two plugin ceilings plus slack
on the database's clock; ingest settles the row by (server, writeId), only
while it is still reloading, so a replay moves nothing and a late outcome
still lands. A row past settle_by reads as lost.

GET /admin/rust/config/:serverId/writes/:writeId serves the poll; the page
polls it every two seconds, holds the Save button while it waits, and says
whether a rolled-back plugin came back on its old file. config.outcome is
a staff kind: it carries the server's log tail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E14m6SuuY6i1vASFeGDBeY
This commit is contained in:
2026-09-26 17:16:45 -05:00
parent 36eae7ffa8
commit 654a24585d
13 changed files with 515 additions and 37 deletions

View File

@@ -41,7 +41,7 @@
"Admin · Rust"
],
"summary": "Save a configuration file and reload its plugin",
"description": "Send `edits` (the generated form: pointers and literals, type-preserving) or `text` (the raw tier: the whole document). `version` must match what the host holds or the save is refused 409 with the current file. The game backs the file up, writes it, reloads the named plugin, and **restores the old file automatically** if the plugin does not come back — which is answered 200 with `report.rolledBack`, because a rollback is a round trip that worked and an edit that did not.",
"description": "Send `edits` (the generated form: pointers and literals, type-preserving) or `text` (the raw tier: the whole document). `version` must match what the host holds or the save is refused 409 with the current file. The game backs the file up, writes it, reloads the named plugin, and **restores the old file automatically** if the plugin fails to load. With a plugin to reload, the answer comes as soon as the file is written — `pending: true` and `write.id` — and the outcome is read from `GET …/writes/{writeId}` once the reload settles. Without one, the answer is final.",
"parameters": [
{
"name": "serverId",
@@ -54,7 +54,7 @@
],
"responses": {
"200": {
"description": "What happened: applied, or rolled back with the reason"
"description": "Written and reloading (`pending`, with the write’s id), or the final outcome when nothing was reloaded"
},
"400": {
"description": "Invalid body, an edit the form may not make, or a locked key"
@@ -158,6 +158,44 @@
}
}
},
"/api/v1/admin/rust/config/{serverId}/writes/{writeId}": {
"get": {
"tags": [
"Admin · Rust"
],
"summary": "One configuration write, while its reload settles",
"description": "A save that reloads a plugin answers as soon as the file is written, with `pending: true` and this write’s id; the reload can take longer than a request should, behind a cold compile. Poll this until `outcome` leaves `reloading`: `applied`, or `rolled-back` with the reason, the server’s log, and `restored` (whether the plugin came back on its old file). `lost` means the plugin never reported — it was reloaded, or the link dropped — so re-read the file.",
"parameters": [
{
"name": "serverId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "writeId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "The write and how far it has got"
},
"404": {
"description": "No such write on that server"
},
"500": {
"description": "Internal Server Error"
}
}
}
},
"/api/v1/admin/rust/permissions": {
"get": {
"tags": [