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:
@@ -19,6 +19,9 @@
|
||||
-- it knows this module registered, because it is the side that knows which
|
||||
-- registrant owned what.
|
||||
|
||||
-- Phase 7b.
|
||||
DROP TABLE IF EXISTS rust_config_writes;
|
||||
|
||||
-- Phase 7. Children before parents: every one of these carries a foreign key
|
||||
-- into `rust_servers`, `users` or `rust_perm_groups`.
|
||||
DROP TABLE IF EXISTS rust_perm_catalogue;
|
||||
|
||||
@@ -618,3 +618,53 @@ ALTER TABLE rust_server_state ADD COLUMN IF NOT EXISTS wipe_id VARCHAR(48) NULL;
|
||||
-- a server is up), and `last_seen_at` is when a `server.hello` last arrived. Only
|
||||
-- a successful refresh moves it.
|
||||
ALTER TABLE rust_server_state ADD COLUMN IF NOT EXISTS last_seen_at DATETIME NULL;
|
||||
|
||||
|
||||
-- ── Configuration written from the site (phase 7b, R18) ───────────────────
|
||||
--
|
||||
-- The audit trail for the most powerful thing this website can do to somebody's
|
||||
-- game host: write a file on it. One row per save attempt, including the ones
|
||||
-- that were refused and the ones the plugin rolled back — a write that did not
|
||||
-- land is exactly the row an operator asking "why is ZoneManager down" needs to
|
||||
-- find.
|
||||
--
|
||||
-- **No file bodies.** `changes` holds the fields that changed and their before
|
||||
-- and after LITERALS, which is what a person reading this wants, and secrets are
|
||||
-- redacted on the way in (`configEdit.redactChange`). D37 lets an admin read a
|
||||
-- credential on the page they opened deliberately; this table is read by more
|
||||
-- people, for longer, and usually by somebody who was not there.
|
||||
--
|
||||
-- The versions bracket the write: `version_before` is what the plugin said the
|
||||
-- file was when it was read, `version_after` what it is now. They are the
|
||||
-- plugin's own hashes, echoed — this module never computes one.
|
||||
CREATE TABLE IF NOT EXISTS rust_config_writes (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
server_id VARCHAR(64) NOT NULL,
|
||||
path VARCHAR(255) NOT NULL,
|
||||
plugin VARCHAR(128) NULL,
|
||||
-- What the admin asked us to reload. NULL is an honest value: a file whose
|
||||
-- plugin is not loaded is written and not reloaded, and saying so is the
|
||||
-- difference between "saved" and "in effect".
|
||||
reload_target VARCHAR(128) NULL,
|
||||
-- `form` or `raw`. Which tier an edit came through changes how it should be
|
||||
-- read: a form edit is type-preserving and narrow, a raw edit replaced the
|
||||
-- whole document.
|
||||
tier VARCHAR(16) NOT NULL DEFAULT 'form',
|
||||
user_id INT NULL,
|
||||
-- `applied` | `rolled-back` | `refused` | `unreachable`
|
||||
outcome VARCHAR(24) NOT NULL,
|
||||
reloaded TINYINT(1) NOT NULL DEFAULT 0,
|
||||
changes LONGTEXT NULL,
|
||||
version_before VARCHAR(64) NULL,
|
||||
version_after VARCHAR(64) NULL,
|
||||
detail VARCHAR(500) NULL,
|
||||
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT fk_rust_config_writes_server
|
||||
FOREIGN KEY (server_id) REFERENCES rust_servers (id) ON DELETE CASCADE,
|
||||
-- A deleted account must not delete the record that they changed a setting.
|
||||
-- The row stays and the name goes; the alternative is an audit trail that a
|
||||
-- person can erase by closing their account.
|
||||
CONSTRAINT fk_rust_config_writes_user
|
||||
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE SET NULL,
|
||||
KEY idx_rust_config_writes_server (server_id, created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
Reference in New Issue
Block a user