From 0790f8dfd3a2437e2e81158588e0caef435c0713 Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Wed, 23 Sep 2026 22:08:19 +0800 Subject: [PATCH] fix(panel): surface the RCON reply for player-access mutations (#56) The whitelist/ban/kick mutations reported a canned success message and threw away the server's reply, so a refused command still read as done: live, the vanilla server answers "That player does not exist" for a name it has never seen (any player who has not joined yet), while the panel said the player had been whitelisted/banned. api.ts documents these replies as "surfaced verbatim as confirmation"; now they are. The localized string stays as the fallback for a silent server. --- panel/src/components/players/BansSection.tsx | 9 +++++---- panel/src/components/players/OnlineSection.tsx | 12 +++++++++--- panel/src/components/players/WhitelistSection.tsx | 15 +++++++++++---- panel/src/components/players/shared.tsx | 8 ++++++++ 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/panel/src/components/players/BansSection.tsx b/panel/src/components/players/BansSection.tsx index 4f6dce9..8e0948c 100644 --- a/panel/src/components/players/BansSection.tsx +++ b/panel/src/components/players/BansSection.tsx @@ -12,6 +12,7 @@ import { MC_NAME, PagerFooter, PlayerField, + rconReply, SearchBox, usePagedNames, type Feedback, @@ -68,8 +69,8 @@ export function BansSection({ name }: { name: string }) { setFb(null); setBanning(true); try { - await api.accessBan(name, "ban", player); - setFb({ kind: "ok", msg: t("access_banned", { player }) }); + const res = await api.accessBan(name, "ban", player); + setFb({ kind: "ok", msg: rconReply(res.output, t("access_banned", { player })) }); setValue(""); setTouched(false); setArmed(false); @@ -86,8 +87,8 @@ export function BansSection({ name }: { name: string }) { setFb(null); setPardoning(p); try { - await api.accessBan(name, "pardon", p); - setFb({ kind: "ok", msg: t("access_pardoned", { player: p }) }); + const res = await api.accessBan(name, "pardon", p); + setFb({ kind: "ok", msg: rconReply(res.output, t("access_pardoned", { player: p })) }); reload(); } catch (e) { setFb({ kind: "err", msg: humanizeError(e) }); diff --git a/panel/src/components/players/OnlineSection.tsx b/panel/src/components/players/OnlineSection.tsx index 3b07668..12e7e55 100644 --- a/panel/src/components/players/OnlineSection.tsx +++ b/panel/src/components/players/OnlineSection.tsx @@ -9,6 +9,7 @@ import { CollapsibleSection, FeedbackLine, PagerFooter, + rconReply, SearchBox, usePagedNames, type Feedback, @@ -52,11 +53,16 @@ export function OnlineSection({ name, defaultOpen = true }: { name: string; defa setFb(null); setPending({ player: playerName, action }); try { - if (action === "kick") await api.accessKick(name, playerName); - else await api.accessBan(name, "ban", playerName); + const res = + action === "kick" + ? await api.accessKick(name, playerName) + : await api.accessBan(name, "ban", playerName); setFb({ kind: "ok", - msg: t(action === "kick" ? "access_kicked" : "access_banned", { player: playerName }), + msg: rconReply( + res.output, + t(action === "kick" ? "access_kicked" : "access_banned", { player: playerName }), + ), }); reload(); // the player just left — refresh so the roster reflects it } catch (e) { diff --git a/panel/src/components/players/WhitelistSection.tsx b/panel/src/components/players/WhitelistSection.tsx index b1a998d..a4c3f6f 100644 --- a/panel/src/components/players/WhitelistSection.tsx +++ b/panel/src/components/players/WhitelistSection.tsx @@ -12,6 +12,7 @@ import { MC_NAME, PagerFooter, PlayerField, + rconReply, SearchBox, usePagedNames, type Feedback, @@ -56,8 +57,11 @@ export function WhitelistSection({ name, defaultOpen = false }: { name: string; setFb(null); setAdding(true); try { - await api.accessWhitelist(name, "add", player); - setFb({ kind: "ok", msg: t("access_whitelist_added", { player }) }); + const res = await api.accessWhitelist(name, "add", player); + setFb({ + kind: "ok", + msg: rconReply(res.output, t("access_whitelist_added", { player })), + }); setValue(""); setTouched(false); reload(); @@ -73,8 +77,11 @@ export function WhitelistSection({ name, defaultOpen = false }: { name: string; setFb(null); setRemoving(p); try { - await api.accessWhitelist(name, "remove", p); - setFb({ kind: "ok", msg: t("access_whitelist_removed", { player: p }) }); + const res = await api.accessWhitelist(name, "remove", p); + setFb({ + kind: "ok", + msg: rconReply(res.output, t("access_whitelist_removed", { player: p })), + }); reload(); } catch (e) { setFb({ kind: "err", msg: humanizeError(e) }); diff --git a/panel/src/components/players/shared.tsx b/panel/src/components/players/shared.tsx index ee2c618..d03f111 100644 --- a/panel/src/components/players/shared.tsx +++ b/panel/src/components/players/shared.tsx @@ -22,6 +22,14 @@ export const MC_NAME = /^[A-Za-z0-9_]{1,16}$/; export type Feedback = { kind: "ok" | "err"; msg: string } | null; +/** rconReply decides what a mutation reports: the server's own reply, verbatim, + * whenever it says anything — the reply is ground truth and names refusals too + * ("That player does not exist"), so a canned confirmation must never speak over + * it. The localized fallback only covers a silent server. */ +export function rconReply(output: string | undefined, fallback: string): string { + return (output ?? "").trim() || fallback; +} + /** FeedbackLine is the shared inline result line for a player action: emerald on * success, destructive on failure. There is no toast library — every section * reports here, in place, right under the control that fired. */