feat(rcon): refine server console
CI / validate (push) Successful in 6m11s
Release / release (push) Successful in 7m59s

This commit is contained in:
dmg
2026-08-08 08:06:11 -04:00
parent 7f6d69e0a7
commit 3564d24a45
10 changed files with 58 additions and 37 deletions
@@ -60,7 +60,7 @@ export default async function RconPage({
<header className="border-b border-line pb-8">
<p className="font-mono text-xs font-bold uppercase tracking-[0.25em] text-accent">Server operations</p>
<h1 className="mt-4 font-display text-5xl font-black uppercase sm:text-7xl">RCON</h1>
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Run commands through the portal backend. RCON endpoints remain internal and credentials are never sent to the browser.</p>
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Run commands through the portal backend to internal or external server addresses. Credentials are never sent to the browser.</p>
</header>
{saved && <p className="mt-7 border-l-2 border-signal bg-panel px-5 py-4 font-mono text-xs font-bold uppercase tracking-wider" role="status">{savedMessages[saved] ?? "RCON settings saved."}</p>}
@@ -139,7 +139,7 @@ function ConnectionFields({
return (
<>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-name`}>Name<input className={fieldClass} defaultValue={defaults?.name} id={`${prefix}-rcon-name`} maxLength={100} name="name" required /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-host`}>Internal hostname<input autoCapitalize="none" autoCorrect="off" className={fieldClass} defaultValue={defaults?.host} id={`${prefix}-rcon-host`} maxLength={253} name="host" placeholder="season4.somc.svc.cluster.local" required spellCheck={false} /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-host`}>Server address<input autoCapitalize="none" autoCorrect="off" className={fieldClass} defaultValue={defaults?.host} id={`${prefix}-rcon-host`} maxLength={253} name="host" placeholder="minecraft.example.com" required spellCheck={false} /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-port`}>Port<input className={fieldClass} defaultValue={defaults?.port ?? 25575} id={`${prefix}-rcon-port`} max={65535} min={1} name="port" required type="number" /></label>
<label className="block font-mono text-[10px] font-bold uppercase" htmlFor={`${prefix}-rcon-password`}>{defaults ? "Replacement password" : "Password"}<input autoComplete="new-password" className={fieldClass} id={`${prefix}-rcon-password`} maxLength={512} name="password" required={!defaults} type="password" />{defaults && <span className="mt-2 block font-sans text-[10px] font-normal normal-case text-muted">Leave blank to preserve the current password.</span>}</label>
</>
@@ -8,12 +8,15 @@ vi.mock("@/app/admin/(console)/rcon/actions", () => ({
import { RconConsole } from "./rcon-console";
describe("RconConsole", () => {
it("renders labelled keyboard-operable controls without history", () => {
it("renders a labelled keyboard-operable terminal without persisted history", () => {
const markup = renderToStaticMarkup(<RconConsole servers={[{ id: "one", name: "Season 4" }]} />);
expect(markup).toContain('aria-label="RCON terminal"');
expect(markup).toContain('for="rcon-console-server"');
expect(markup).toContain('for="rcon-command"');
expect(markup).toContain("Season 4");
expect(markup).toContain("Run command");
expect(markup).toContain("server://");
expect(markup).toContain("Awaiting command");
expect(markup).toContain("Enter ↵");
expect(markup).not.toContain("Latest response");
});
+34 -17
View File
@@ -16,24 +16,41 @@ export function RconConsole({ servers }: { servers: ServerOption[] }) {
}
return (
<form action={action} className="mt-6 space-y-4">
<label className="block font-mono text-[10px] font-bold uppercase tracking-wider" htmlFor="rcon-console-server">
Server
<select className="mt-2 block w-full border border-line bg-canvas px-4 py-3 font-sans text-sm font-normal normal-case" defaultValue={state.serverId || servers[0]?.id} id="rcon-console-server" name="serverId" required>
{servers.map((server) => <option key={server.id} value={server.id}>{server.name}</option>)}
</select>
</label>
<label className="block font-mono text-[10px] font-bold uppercase tracking-wider" htmlFor="rcon-command">
Command
<input autoComplete="off" className="mt-2 block w-full border border-line bg-canvas px-4 py-3 font-mono text-sm outline-none focus:border-accent" id="rcon-command" maxLength={1024} name="command" placeholder="list" required spellCheck={false} />
</label>
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Running…" : "Run command"}</button>
{state.status !== "idle" && (
<div aria-live="polite" className={`border-l-2 bg-canvas p-4 ${state.status === "error" ? "border-accent" : "border-signal"}`} role={state.status === "error" ? "alert" : "status"}>
<p className="font-mono text-[9px] font-bold uppercase tracking-wider text-muted">Latest response{responseServer ? `${responseServer.name}` : ""}</p>
<pre className="mt-2 max-h-80 overflow-auto whitespace-pre-wrap break-words font-mono text-xs leading-5">{state.message}</pre>
<form action={action} aria-label="RCON terminal" className="mt-6 overflow-hidden border-2 border-ink bg-panel shadow-[6px_6px_0_var(--color-shadow)]">
<div className="flex flex-wrap items-center justify-between gap-3 border-b border-line bg-canvas px-4 py-3">
<div className="flex items-center gap-2 font-mono text-[10px] font-bold uppercase tracking-wider text-muted">
<span aria-hidden="true" className="size-2 rounded-full bg-signal shadow-[0_0_0_1px_var(--color-ink)]" />
<span>server://</span>
</div>
)}
<label className="flex items-center gap-2 font-mono text-[9px] font-bold uppercase tracking-wider" htmlFor="rcon-console-server">
Target
<select className="border border-line bg-panel px-3 py-2 font-mono text-xs font-bold normal-case outline-none focus:border-accent" defaultValue={state.serverId || servers[0]?.id} id="rcon-console-server" name="serverId" required>
{servers.map((server) => <option key={server.id} value={server.id}>{server.name}</option>)}
</select>
</label>
</div>
<div aria-live="polite" aria-relevant="additions text" className="min-h-52 max-h-80 overflow-auto p-5 font-mono text-xs leading-5" role={state.status === "error" ? "alert" : "status"}>
{pending ? (
<p className="text-muted"><span aria-hidden="true" className="mr-2 text-accent"></span>Executing command</p>
) : state.status === "idle" ? (
<p className="text-muted"><span aria-hidden="true" className="mr-2 text-accent">#</span>Awaiting command</p>
) : (
<>
<p className={`text-[9px] font-bold uppercase tracking-wider ${state.status === "error" ? "text-accent" : "text-muted"}`}>
{state.status === "error" ? "Error" : "Response"}{responseServer ? `${responseServer.name}` : ""}
</p>
<pre className="mt-3 whitespace-pre-wrap break-words font-mono text-xs leading-5">{state.message}</pre>
</>
)}
</div>
<div className="flex items-center gap-3 border-t-2 border-ink bg-canvas p-3">
<span aria-hidden="true" className="font-mono text-lg font-black text-accent">$</span>
<label className="sr-only" htmlFor="rcon-command">Command</label>
<input autoComplete="off" autoFocus className="min-w-0 flex-1 bg-transparent px-1 py-2 font-mono text-sm outline-none placeholder:text-muted focus-visible:outline-none" id="rcon-command" maxLength={1024} name="command" placeholder="list" required spellCheck={false} />
<button className="border border-ink bg-ink px-4 py-2 font-mono text-[9px] font-bold uppercase tracking-wider text-canvas disabled:cursor-wait disabled:opacity-60" disabled={pending} type="submit">{pending ? "Running…" : "Enter ↵"}</button>
</div>
</form>
);
}