feat(rust): mod configuration from the site, and an editor that will not rewrite a float
R18's two tiers: a form generated from a config file's own values, and raw JSON for what a form cannot express. Admin → Rust mod config, one live round trip per action, nothing cached between a browser and a game host's disk. `configEdit.js` is the part that could not be done naively. JavaScript cannot tell `1` from `1.0`, and both mod frameworks deserialize a config into typed C# classes — so a read-modify-write silently rewrites every whole-numbered float as an integer on fields nobody touched, and a plugin that then throws at load does not come back. It never parses, mutates and re-serialises: it records the SOURCE SPAN of every value and splices literals into them, so an untouched `1.0` is still `1.0` and a number an admin types travels as text the whole way (D35/D36). The bridge's own config is editable with `Host`, `Port` and `ServerId` locked, in the form and in the raw tier, because either would cut the link carrying the edit or strand every row this site holds (D38). Credentials render masked with a reveal; the raw tier shows them (D37) and the audit trail never does. `rust_config_writes` records every save including the refused and the rolled back — an operator asking why a setting is not what they set needs to see that somebody tried. Three defects a browser walk found that 179 green tests did not: * every save of the bridge's own config was refused while the page said the opposite — a `<select>` whose value matches no `<option>` shows the first one, so the reload guess `RunicGateway` was on the wire and "nothing" was on the screen; * `btn ghost` is not a class this platform defines (`.btn-ghost` is), so every secondary button in this module has rendered as a primary one since phase 7 — here it made the open file and the active tier indistinguishable; * a save's refusal rendered at the top of a long form, far from the button. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PMH6bw1jXMgbyF3ZWGEzSM
This commit is contained in:
@@ -1,5 +1,163 @@
|
||||
{
|
||||
"paths": {
|
||||
"/api/v1/admin/rust/config/{serverId}/file": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Admin · Rust"
|
||||
],
|
||||
"summary": "One configuration file",
|
||||
"description": "The file’s text, the version a save must present back, and the field list the generated form is drawn from — types, keys, and which values are credentials. A file that is already broken on disk still opens, with the parse error, because the raw tier is the only thing that can fix it.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serverId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "path",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The file and the reading of it"
|
||||
},
|
||||
"400": {
|
||||
"description": "Not a configuration path"
|
||||
},
|
||||
"503": {
|
||||
"description": "The sidecar or the game is unreachable"
|
||||
}
|
||||
}
|
||||
},
|
||||
"post": {
|
||||
"tags": [
|
||||
"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.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serverId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "What happened: applied, or rolled back with the reason"
|
||||
},
|
||||
"400": {
|
||||
"description": "Invalid body, an edit the form may not make, or a locked key"
|
||||
},
|
||||
"409": {
|
||||
"description": "The file changed on the host since it was read"
|
||||
},
|
||||
"503": {
|
||||
"description": "The sidecar or the game is unreachable"
|
||||
}
|
||||
},
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"path": {
|
||||
"example": "any"
|
||||
},
|
||||
"edits": {
|
||||
"example": "any"
|
||||
},
|
||||
"reload": {
|
||||
"example": "any"
|
||||
},
|
||||
"version": {
|
||||
"example": "any"
|
||||
},
|
||||
"text": {
|
||||
"example": "any"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/rust/config/{serverId}/files": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Admin · Rust"
|
||||
],
|
||||
"summary": "Every plugin configuration file on one server",
|
||||
"description": "A live recursive walk of the game host’s configuration directory, grouped by the plugin each file probably belongs to, plus every plugin currently loaded. The root comes from the mod framework, so it is `oxide/config` on Oxide and `carbon/configs` on Carbon. Files past the size limit are listed and marked un-editable rather than hidden. The game’s data directory is never walked.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serverId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The configuration tree and the loaded plugins"
|
||||
},
|
||||
"502": {
|
||||
"description": "Bad Gateway"
|
||||
},
|
||||
"503": {
|
||||
"description": "The sidecar or the game is unreachable"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/rust/config/{serverId}/writes": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Admin · Rust"
|
||||
],
|
||||
"summary": "Recent configuration writes",
|
||||
"description": "The audit trail for one server: who changed which field, from what to what, whether the plugin reloaded, and whether the change was rolled back. Refused and unreachable attempts are recorded too — an operator asking why a setting is not what they set needs to see that somebody tried. Values of credential-shaped fields are never stored.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "serverId",
|
||||
"in": "path",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "limit",
|
||||
"in": "query",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The recent writes, newest first"
|
||||
},
|
||||
"500": {
|
||||
"description": "Internal Server Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/admin/rust/permissions": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
Reference in New Issue
Block a user