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:
@@ -52,11 +52,11 @@ const TIMEOUT_MS = 12000
|
||||
* here, `PROTOCOL_VERSION` in the sidecar, `ProtocolVersion` in the bridge
|
||||
* plugin, and `protocol` in its `overlay.toml`.
|
||||
*
|
||||
* **4 — the permission mirror.** Protocol 2 was the read path, 3 the first
|
||||
* message the WEBSITE originates (`link.confirm`); 4 is the first that WRITES
|
||||
* to the game — the whole permission set the site authors for one server, and
|
||||
* the report the plugin sends back. The bump lands here in the same change as
|
||||
* the emitters,
|
||||
* **5 — configuration from the site.** Protocol 2 was the read path, 3 the
|
||||
* first message the WEBSITE originates (`link.confirm`), 4 the first that
|
||||
* writes to the game's permission store; 5 is the first that writes to the game
|
||||
* HOST'S FILESYSTEM — a plugin's settings, and a reload watched closely enough
|
||||
* to be undone. The bump lands here in the same change as the emitters,
|
||||
* because the sidecar refuses a client declaring a different version with a
|
||||
* `409`: a module left on 2 would stop being able to read the server board it
|
||||
* has been reading all along. A constant that lags the deployment is not a safe
|
||||
@@ -66,7 +66,7 @@ const TIMEOUT_MS = 12000
|
||||
* deployment into a `409` naming both numbers instead of a parse failure three
|
||||
* layers further in.
|
||||
*/
|
||||
const PROTOCOL_VERSION = 4
|
||||
const PROTOCOL_VERSION = 5
|
||||
|
||||
/** What a caller gets back. Shaped once so every call site reads the same. */
|
||||
function reply(ok, status, data = null) {
|
||||
@@ -239,6 +239,41 @@ const permCatalogue = (server) => request(server, '/permissions/catalogue')
|
||||
*/
|
||||
const permSync = (server, set) => request(server, '/permissions/sync', { method: 'POST', body: set })
|
||||
|
||||
/**
|
||||
* Every settings file on one game host, and every plugin loaded to reload one
|
||||
* (protocol 5, R18).
|
||||
*
|
||||
* A description of the tree, never its contents: paths, sizes, which files are
|
||||
* too large to edit, and the plugin each one probably belongs to. **Probably**
|
||||
* is the operative word and it survives all the way to the form — a folder name
|
||||
* is convention, not contract, and reloading the wrong plugin would report
|
||||
* success while the edited one never re-read anything.
|
||||
*
|
||||
* Live, like `/status`: what is on a host's disk has no stale answer worth
|
||||
* giving, and a cached one would be an edit an operator made over SSH that the
|
||||
* website then overwrote.
|
||||
*/
|
||||
const configFiles = (server) => request(server, '/config/files')
|
||||
|
||||
/** One settings file as text, with the version a write has to present back. */
|
||||
const configFile = (server, path) =>
|
||||
request(server, `/config/file?path=${encodeURIComponent(path)}`)
|
||||
|
||||
/**
|
||||
* Replace a set of settings files and reload what owns them (protocol 5).
|
||||
*
|
||||
* **The only call in this module that writes to a filesystem**, and the only one
|
||||
* whose reply routinely takes seconds: the plugin holds it open across the
|
||||
* reload it is watching, and across the rollback if that reload never arrives.
|
||||
*
|
||||
* Like every other write on this bridge, a refusal comes back `{ ok: true }`
|
||||
* with the answer in `data.kind` — `config.report` or `config.error`. The
|
||||
* transport keeps its own codes, and a `504` here is the one case worth reading
|
||||
* carefully: the plugin writes a whole set or restores a whole set, never half
|
||||
* of either, so the state is knowable by re-reading rather than by guessing.
|
||||
*/
|
||||
const configWrite = (server, body) => request(server, '/config/write', { method: 'POST', body })
|
||||
|
||||
module.exports = {
|
||||
TIMEOUT_MS,
|
||||
PROTOCOL_VERSION,
|
||||
@@ -252,5 +287,8 @@ module.exports = {
|
||||
confirmLink,
|
||||
permCatalogue,
|
||||
permSync,
|
||||
configFiles,
|
||||
configFile,
|
||||
configWrite,
|
||||
joinUrl,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user