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.
This commit is contained in:
Lemon-miaow committed 2026-09-23 22:08:19 +08:00
1 parent 9c3a1c5be1
commit 0790f8dfd3
4 files changed
+33 -11

No files matched your search

+5 -4
View File
@@ -12,6 +12,7 @@ import {
MC_NAME, MC_NAME,
PagerFooter, PagerFooter,
PlayerField, PlayerField,
rconReply,
SearchBox, SearchBox,
usePagedNames, usePagedNames,
type Feedback, type Feedback,
@@ -68,8 +69,8 @@ export function BansSection({ name }: { name: string }) {
setFb(null); setFb(null);
setBanning(true); setBanning(true);
try { try {
await api.accessBan(name, "ban", player); const res = await api.accessBan(name, "ban", player);
setFb({ kind: "ok", msg: t("access_banned", { player }) }); setFb({ kind: "ok", msg: rconReply(res.output, t("access_banned", { player })) });
setValue(""); setValue("");
setTouched(false); setTouched(false);
setArmed(false); setArmed(false);
@@ -86,8 +87,8 @@ export function BansSection({ name }: { name: string }) {
setFb(null); setFb(null);
setPardoning(p); setPardoning(p);
try { try {
await api.accessBan(name, "pardon", p); const res = await api.accessBan(name, "pardon", p);
setFb({ kind: "ok", msg: t("access_pardoned", { player: p }) }); setFb({ kind: "ok", msg: rconReply(res.output, t("access_pardoned", { player: p })) });
reload(); reload();
} catch (e) { } catch (e) {
setFb({ kind: "err", msg: humanizeError(e) }); setFb({ kind: "err", msg: humanizeError(e) });
@@ -9,6 +9,7 @@ import {
CollapsibleSection, CollapsibleSection,
FeedbackLine, FeedbackLine,
PagerFooter, PagerFooter,
rconReply,
SearchBox, SearchBox,
usePagedNames, usePagedNames,
type Feedback, type Feedback,
@@ -52,11 +53,16 @@ export function OnlineSection({ name, defaultOpen = true }: { name: string; defa
setFb(null); setFb(null);
setPending({ player: playerName, action }); setPending({ player: playerName, action });
try { try {
if (action === "kick") await api.accessKick(name, playerName); const res =
else await api.accessBan(name, "ban", playerName); action === "kick"
? await api.accessKick(name, playerName)
: await api.accessBan(name, "ban", playerName);
setFb({ setFb({
kind: "ok", 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 reload(); // the player just left — refresh so the roster reflects it
} catch (e) { } catch (e) {
@@ -12,6 +12,7 @@ import {
MC_NAME, MC_NAME,
PagerFooter, PagerFooter,
PlayerField, PlayerField,
rconReply,
SearchBox, SearchBox,
usePagedNames, usePagedNames,
type Feedback, type Feedback,
@@ -56,8 +57,11 @@ export function WhitelistSection({ name, defaultOpen = false }: { name: string;
setFb(null); setFb(null);
setAdding(true); setAdding(true);
try { try {
await api.accessWhitelist(name, "add", player); const res = await api.accessWhitelist(name, "add", player);
setFb({ kind: "ok", msg: t("access_whitelist_added", { player }) }); setFb({
kind: "ok",
msg: rconReply(res.output, t("access_whitelist_added", { player })),
});
setValue(""); setValue("");
setTouched(false); setTouched(false);
reload(); reload();
@@ -73,8 +77,11 @@ export function WhitelistSection({ name, defaultOpen = false }: { name: string;
setFb(null); setFb(null);
setRemoving(p); setRemoving(p);
try { try {
await api.accessWhitelist(name, "remove", p); const res = await api.accessWhitelist(name, "remove", p);
setFb({ kind: "ok", msg: t("access_whitelist_removed", { player: p }) }); setFb({
kind: "ok",
msg: rconReply(res.output, t("access_whitelist_removed", { player: p })),
});
reload(); reload();
} catch (e) { } catch (e) {
setFb({ kind: "err", msg: humanizeError(e) }); setFb({ kind: "err", msg: humanizeError(e) });
+8
View File
@@ -22,6 +22,14 @@ export const MC_NAME = /^[A-Za-z0-9_]{1,16}$/;
export type Feedback = { kind: "ok" | "err"; msg: string } | null; 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 /** FeedbackLine is the shared inline result line for a player action: emerald on
* success, destructive on failure. There is no toast library — every section * success, destructive on failure. There is no toast library — every section
* reports here, in place, right under the control that fired. */ * reports here, in place, right under the control that fired. */