feat(rcon): unify terminal workspace
CI / validate (push) Successful in 6m45s
Release / release (push) Successful in 8m38s

This commit is contained in:
dmg
2026-08-08 09:41:37 -04:00
parent 3564d24a45
commit 19486150c3
6 changed files with 245 additions and 135 deletions
+15 -87
View File
@@ -1,14 +1,7 @@
import { rconServers } from "@minecraft-account-manager/database";
import { asc } from "drizzle-orm";
import { RconConsole } from "@/components/rcon-console";
import { RconConsole, type RconTerminalNotice } from "@/components/rcon-console";
import { db } from "@/lib/database";
import {
createRconServer,
deleteRconServer,
setRconServerEnabled,
testSavedRconServer,
updateRconServer,
} from "./actions";
export const dynamic = "force-dynamic";
@@ -46,13 +39,17 @@ export default async function RconPage({
const query = await searchParams;
const saved = queryValue(query.saved);
const error = queryValue(query.error);
const notice: RconTerminalNotice | undefined = error
? { status: "error", message: errorMessages[error] ?? "The RCON operation failed." }
: saved
? { status: "success", message: savedMessages[saved] ?? "RCON settings saved." }
: undefined;
const servers = await db.select({
id: rconServers.id,
name: rconServers.name,
host: rconServers.host,
port: rconServers.port,
enabled: rconServers.enabled,
updatedAt: rconServers.updatedAt,
}).from(rconServers).orderBy(asc(rconServers.name));
return (
@@ -60,88 +57,19 @@ 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 to internal or external server addresses. Credentials are never sent to the browser.</p>
<p className="mt-5 max-w-2xl text-sm leading-6 text-muted">Select and manage a connection, then run commands through the portal backend. 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>}
{error && <p className="mt-7 border-l-2 border-accent bg-panel px-5 py-4 text-sm text-accent" role="alert">{errorMessages[error] ?? "The RCON operation failed."}</p>}
<div className="mt-10 grid gap-10 lg:grid-cols-[1.1fr_0.9fr]">
<section className="border border-line bg-panel p-6 shadow-[6px_6px_0_var(--color-shadow)]">
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Command proxy</p>
<h2 className="mt-2 font-display text-3xl font-black uppercase">Console</h2>
<p className="mt-3 text-xs leading-5 text-muted">Only the latest bounded response is shown. Commands and responses are not saved as console history.</p>
<RconConsole servers={servers.filter((server) => server.enabled).map(({ id, name }) => ({ id, name }))} />
</section>
<section>
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Configuration</p>
<h2 className="mt-2 font-display text-3xl font-black uppercase">Add connection</h2>
<form action={createRconServer} className="mt-5 space-y-4 border border-line bg-panel p-6">
<ConnectionFields prefix="new" />
<label className="flex items-center gap-3 font-mono text-[10px] font-bold uppercase"><input className="size-4" name="enabled" type="checkbox" value="yes" />Enable immediately</label>
<button className="border border-ink bg-ink px-5 py-3 font-mono text-[10px] font-bold uppercase tracking-wider text-canvas" type="submit">Add connection</button>
</form>
</section>
</div>
<section className="mt-12">
<div className="flex items-end justify-between border-b border-line pb-4">
<div><p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Saved endpoints</p><h2 className="mt-2 font-display text-3xl font-black uppercase">Connections</h2></div>
<span className="font-mono text-xs text-muted">{servers.length} configured</span>
</div>
<div className="divide-y divide-line">
{servers.map((server) => (
<article className="grid gap-5 py-6 lg:grid-cols-[1fr_auto] lg:items-start" key={server.id}>
<div>
<div className="flex flex-wrap items-center gap-3"><h3 className="font-mono text-lg font-bold">{server.name}</h3><span className={`px-2 py-1 font-mono text-[9px] font-bold uppercase ${server.enabled ? "bg-signal text-ink" : "border border-line text-muted"}`}>{server.enabled ? "Enabled" : "Disabled"}</span></div>
<p className="mt-2 font-mono text-[10px] text-muted">{server.host}:{server.port}</p>
<p className="mt-1 font-mono text-[9px] text-muted">Updated {server.updatedAt.toISOString()}</p>
</div>
<div className="flex flex-wrap items-start gap-4">
<form action={testSavedRconServer}><input name="serverId" type="hidden" value={server.id} /><button className="font-mono text-[9px] font-bold uppercase underline underline-offset-4" type="submit">Test</button></form>
<form action={setRconServerEnabled}><input name="serverId" type="hidden" value={server.id} /><input name="enabled" type="hidden" value={server.enabled ? "no" : "yes"} /><button className="font-mono text-[9px] font-bold uppercase underline underline-offset-4" type="submit">{server.enabled ? "Disable" : "Enable"}</button></form>
<details className="relative">
<summary className="cursor-pointer list-none font-mono text-[9px] font-bold uppercase underline underline-offset-4">Edit</summary>
<form action={updateRconServer} className="relative z-10 mt-3 w-[min(28rem,80vw)] space-y-4 border border-line bg-panel p-5 shadow-[5px_5px_0_var(--color-shadow)] lg:absolute lg:right-0">
<input name="serverId" type="hidden" value={server.id} />
<ConnectionFields defaults={server} prefix={server.id} />
<label className="flex items-center gap-3 font-mono text-[10px] font-bold uppercase"><input className="size-4" defaultChecked={server.enabled} name="enabled" type="checkbox" value="yes" />Enabled</label>
<button className="border border-ink px-4 py-2 font-mono text-[9px] font-bold uppercase" type="submit">Save connection</button>
</form>
</details>
<details className="relative">
<summary className="cursor-pointer list-none font-mono text-[9px] font-bold uppercase text-accent underline underline-offset-4">Delete</summary>
<form action={deleteRconServer} className="relative z-10 mt-3 w-64 border border-accent bg-panel p-5 shadow-[5px_5px_0_var(--color-accent)] lg:absolute lg:right-0">
<input name="serverId" type="hidden" value={server.id} /><input name="confirmation" type="hidden" value={server.id} />
<p className="text-xs leading-5">Delete {server.name}? Its encrypted credential will be removed.</p>
<button className="mt-4 bg-accent px-4 py-2 font-mono text-[9px] font-bold uppercase text-canvas" type="submit">Confirm deletion</button>
</form>
</details>
</div>
</article>
))}
{!servers.length && <p className="py-8 text-sm text-muted">No RCON connections configured.</p>}
<section className="mt-10">
<div className="flex flex-col gap-3 sm:flex-row sm:items-end sm:justify-between">
<div>
<p className="font-mono text-[9px] font-bold uppercase tracking-widest text-muted">Command proxy</p>
<h2 className="mt-2 font-display text-3xl font-black uppercase">Terminal</h2>
</div>
<p className="max-w-xl text-xs leading-5 text-muted">Only the latest bounded response is shown. Commands and responses are not saved as console history.</p>
</div>
<RconConsole notice={notice} servers={servers} />
</section>
</main>
);
}
function ConnectionFields({
prefix,
defaults,
}: {
prefix: string;
defaults?: { name: string; host: string; port: number };
}) {
const fieldClass = "mt-2 block w-full border border-line bg-canvas px-4 py-3 font-mono text-sm outline-none focus:border-accent";
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`}>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>
</>
);
}