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:
@@ -114,7 +114,7 @@ function ServerState({ row, onSync, busy }) {
|
||||
{row.lastOkAt ? `last pushed ${ago(row.lastOkAt)}` : 'never pushed'}
|
||||
</span>
|
||||
<span style={{ flex: 1 }} />
|
||||
<button type="button" className="btn ghost" onClick={() => onSync(row.serverId)} disabled={busy}>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => onSync(row.serverId)} disabled={busy}>
|
||||
{busy ? 'Syncing…' : 'Sync now'}
|
||||
</button>
|
||||
</div>
|
||||
@@ -158,10 +158,10 @@ function DriftRow({ row, onAdopt, onRevoke, busy }) {
|
||||
{row.serverId} · seen {ago(row.firstSeen)}
|
||||
</span>
|
||||
</span>
|
||||
<button type="button" className="btn ghost" onClick={() => onAdopt(row)} disabled={busy}>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => onAdopt(row)} disabled={busy}>
|
||||
Adopt
|
||||
</button>
|
||||
<button type="button" className="btn ghost" onClick={() => onRevoke(row)} disabled={busy}>
|
||||
<button type="button" className="btn btn-ghost" onClick={() => onRevoke(row)} disabled={busy}>
|
||||
Revoke
|
||||
</button>
|
||||
</Row>
|
||||
@@ -224,7 +224,7 @@ function GroupCard({ group, catalogue, servers, pending, onChanged, setError })
|
||||
actions={
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
className="btn btn-ghost"
|
||||
disabled={busy}
|
||||
onClick={() => act(() => api.adminPermissions.deleteGroup(group.name))}
|
||||
>
|
||||
@@ -248,7 +248,7 @@ function GroupCard({ group, catalogue, servers, pending, onChanged, setError })
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
className="btn btn-ghost"
|
||||
disabled={busy}
|
||||
onClick={() => save(group.permissions.filter((p) => p !== perm))}
|
||||
>
|
||||
@@ -317,7 +317,7 @@ function GroupCard({ group, catalogue, servers, pending, onChanged, setError })
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
className="btn btn-ghost"
|
||||
disabled={busy}
|
||||
onClick={() => act(() => api.adminPermissions.removeMember(group.name, m.userId))}
|
||||
>
|
||||
@@ -415,7 +415,7 @@ export default function Permissions() {
|
||||
title="Servers"
|
||||
subtitle={`${servers.length} configured`}
|
||||
actions={
|
||||
<button type="button" className="btn ghost" disabled={busy} onClick={() => act(() => api.adminPermissions.sync())}>
|
||||
<button type="button" className="btn btn-ghost" disabled={busy} onClick={() => act(() => api.adminPermissions.sync())}>
|
||||
Sync all
|
||||
</button>
|
||||
}
|
||||
@@ -490,7 +490,7 @@ export default function Permissions() {
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn ghost"
|
||||
className="btn btn-ghost"
|
||||
disabled={busy}
|
||||
onClick={() => act(() => api.adminPermissions.revoke(row.id))}
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user