feat(admin): organize command center controls and diagnostics

This commit is contained in:
Simo committed 2026-09-05 20:15:35 +02:00
1 parent 667637f8da
commit 51d1284950
5 files changed
+252 -113

No files matched your search

@@ -28,6 +28,7 @@ export function CommandocentrumControls() {
const t = useTranslations("pages.admin.commandocentrum"); const t = useTranslations("pages.admin.commandocentrum");
const { run, isPending } = useServerAction(); const { run, isPending } = useServerAction();
const [category, setCategory] = useState("hotel");
const [hotelMessage, setHotelMessage] = useState(""); const [hotelMessage, setHotelMessage] = useState("");
const [disconnect, setDisconnect] = useState({ userId: "", username: "" }); const [disconnect, setDisconnect] = useState({ userId: "", username: "" });
const [alert, setAlert] = useState({ userId: "", message: "" }); const [alert, setAlert] = useState({ userId: "", message: "" });
@@ -46,10 +47,30 @@ export function CommandocentrumControls() {
}); });
return ( return (
<> <section id="commands" className="my-6 scroll-mt-4 space-y-4">
<section className="mt-6"> <h2 className="admin-section-title">{t("emulatorControls")}</h2>
<h2 className="admin-section-title">{t("emulatorControls")}</h2> <fieldset
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4"> className="flex flex-wrap gap-2"
aria-label={t("commandCategories")}
>
{["hotel", "players", "rewards", "advanced"].map((group) => (
<Button
key={group}
type="button"
variant={category === group ? "default" : "outline"}
aria-pressed={category === group}
disabled={isPending}
onClick={() => setCategory(group)}
>
{t(`groups.${group}`)}
</Button>
))}
</fieldset>
<p className="text-sm text-[var(--admin-text-muted)]" role="status">
{isPending ? t("executing") : t("chooseCommand")}
</p>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 xl:grid-cols-3">
{category === "hotel" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("updateCatalog")} {t("updateCatalog")}
@@ -71,7 +92,8 @@ export function CommandocentrumControls() {
{t("updateCatalog")} {t("updateCatalog")}
</Button> </Button>
</div> </div>
)}
{category === "hotel" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("updateWordFilter")} {t("updateWordFilter")}
@@ -93,7 +115,8 @@ export function CommandocentrumControls() {
{t("updateWordFilter")} {t("updateWordFilter")}
</Button> </Button>
</div> </div>
)}
{category === "hotel" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("updateNavigator")} {t("updateNavigator")}
@@ -115,50 +138,44 @@ export function CommandocentrumControls() {
{t("updateNavigator")} {t("updateNavigator")}
</Button> </Button>
</div> </div>
</div> )}
{category === "hotel" && (
<div className="admin-card mt-6"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("hotelAlert")} {t("hotelAlert")}
</h3> </h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3"> <p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("hotelAlertDesc")} {t("hotelAlertDesc")}
</p> </p>
<div className="flex gap-2"> <div className="flex flex-wrap gap-2">
<input <input
value={hotelMessage} value={hotelMessage}
onChange={(e) => setHotelMessage(e.target.value)} onChange={(e) => setHotelMessage(e.target.value)}
required required
maxLength={512} maxLength={512}
placeholder={t("messageToBroadcast")} aria-label={t("messageToBroadcast")}
className="flex-1 min-w-[220px]" placeholder={t("messageToBroadcast")}
disabled={isPending} className="flex-1 min-w-0 w-full sm:w-auto"
/> disabled={isPending}
<Button />
type="button" <Button
variant="destructive" type="button"
disabled={isPending || !hotelMessage.trim()} variant="destructive"
onClick={() => disabled={isPending || !hotelMessage.trim()}
run(() => hotelAlert({ message: hotelMessage }), { onClick={() =>
successMessage: "Hotel alert sent.", run(() => hotelAlert({ message: hotelMessage }), {
errorMessage: RCON_ERROR, successMessage: "Hotel alert sent.",
onSuccess: () => setHotelMessage(""), errorMessage: RCON_ERROR,
}) onSuccess: () => setHotelMessage(""),
} })
> }
{t("sendAlert")} >
</Button> {t("sendAlert")}
</Button>
</div>
</div> </div>
</div> )}
</section> {category === "players" && (
<section className="mt-6">
<h2 className="admin-section-title">{t("userActions")}</h2>
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
{t("userActionsDesc")}
</p>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("actions.disconnect")} {t("actions.disconnect")}
@@ -170,6 +187,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={disconnect.userId} value={disconnect.userId}
onChange={(e) => onChange={(e) =>
@@ -180,6 +198,7 @@ export function CommandocentrumControls() {
/> />
<input <input
type="text" type="text"
aria-label={t("usernameRequired")}
placeholder={t("usernameRequired")} placeholder={t("usernameRequired")}
value={disconnect.username} value={disconnect.username}
onChange={(e) => onChange={(e) =>
@@ -215,7 +234,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "players" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("actions.alert")} {t("actions.alert")}
@@ -227,6 +247,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={alert.userId} value={alert.userId}
onChange={(e) => onChange={(e) =>
@@ -237,6 +258,7 @@ export function CommandocentrumControls() {
/> />
<input <input
type="text" type="text"
aria-label={t("alertMessage")}
placeholder={t("alertMessage")} placeholder={t("alertMessage")}
value={alert.message} value={alert.message}
onChange={(e) => onChange={(e) =>
@@ -270,7 +292,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "players" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("forwardToRoom")} {t("forwardToRoom")}
@@ -282,6 +305,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={forward.userId} value={forward.userId}
onChange={(e) => onChange={(e) =>
@@ -293,6 +317,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("roomId")}
placeholder={t("roomId")} placeholder={t("roomId")}
value={forward.roomId} value={forward.roomId}
onChange={(e) => onChange={(e) =>
@@ -325,9 +350,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
</div> )}
{category === "rewards" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveCredits")} {t("giveCredits")}
@@ -339,6 +363,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={credits.userId} value={credits.userId}
onChange={(e) => onChange={(e) =>
@@ -350,6 +375,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("amount")}
placeholder={t("amount")} placeholder={t("amount")}
value={credits.credits} value={credits.credits}
onChange={(e) => onChange={(e) =>
@@ -382,7 +408,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "rewards" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveDuckets")} {t("giveDuckets")}
@@ -394,6 +421,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={duckets.userId} value={duckets.userId}
onChange={(e) => onChange={(e) =>
@@ -405,6 +433,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("amount")}
placeholder={t("amount")} placeholder={t("amount")}
value={duckets.amount} value={duckets.amount}
onChange={(e) => onChange={(e) =>
@@ -437,7 +466,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "rewards" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveDiamonds")} {t("giveDiamonds")}
@@ -449,6 +479,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={diamonds.userId} value={diamonds.userId}
onChange={(e) => onChange={(e) =>
@@ -460,6 +491,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("amount")}
placeholder={t("amount")} placeholder={t("amount")}
value={diamonds.amount} value={diamonds.amount}
onChange={(e) => onChange={(e) =>
@@ -492,9 +524,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
</div> )}
{category === "rewards" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveBadge")} {t("giveBadge")}
@@ -506,6 +537,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={badge.userId} value={badge.userId}
onChange={(e) => onChange={(e) =>
@@ -516,6 +548,7 @@ export function CommandocentrumControls() {
/> />
<input <input
type="text" type="text"
aria-label={t("badgeCode")}
placeholder={t("badgeCode")} placeholder={t("badgeCode")}
value={badge.badge} value={badge.badge}
onChange={(e) => onChange={(e) =>
@@ -549,7 +582,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "players" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("setMotto")} {t("setMotto")}
@@ -561,6 +595,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={motto.userId} value={motto.userId}
onChange={(e) => onChange={(e) =>
@@ -571,6 +606,7 @@ export function CommandocentrumControls() {
/> />
<input <input
type="text" type="text"
aria-label={t("newMotto")}
placeholder={t("newMotto")} placeholder={t("newMotto")}
value={motto.motto} value={motto.motto}
onChange={(e) => onChange={(e) =>
@@ -604,7 +640,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "players" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("setRank")} {t("setRank")}
@@ -616,6 +653,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={rank.userId} value={rank.userId}
onChange={(e) => onChange={(e) =>
@@ -628,6 +666,7 @@ export function CommandocentrumControls() {
type="number" type="number"
min={0} min={0}
max={10} max={10}
aria-label={t("rankPlaceholder")}
placeholder={t("rankPlaceholder")} placeholder={t("rankPlaceholder")}
value={rank.rank} value={rank.rank}
onChange={(e) => onChange={(e) =>
@@ -660,9 +699,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
</div> )}
{category === "advanced" && (
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("executeCommand")} {t("executeCommand")}
@@ -674,6 +712,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={command.userId} value={command.userId}
onChange={(e) => onChange={(e) =>
@@ -684,6 +723,7 @@ export function CommandocentrumControls() {
/> />
<input <input
type="text" type="text"
aria-label={t("commandPlaceholder")}
placeholder={t("commandPlaceholder")} placeholder={t("commandPlaceholder")}
value={command.command} value={command.command}
onChange={(e) => onChange={(e) =>
@@ -719,7 +759,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
)}
{category === "rewards" && (
<div className="admin-card"> <div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2"> <h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("sendGift")} {t("sendGift")}
@@ -731,6 +772,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("userId")}
placeholder={t("userId")} placeholder={t("userId")}
value={gift.userId} value={gift.userId}
onChange={(e) => onChange={(e) =>
@@ -742,6 +784,7 @@ export function CommandocentrumControls() {
<input <input
type="number" type="number"
min={1} min={1}
aria-label={t("furniId")}
placeholder={t("furniId")} placeholder={t("furniId")}
value={gift.itemId} value={gift.itemId}
onChange={(e) => onChange={(e) =>
@@ -752,6 +795,7 @@ export function CommandocentrumControls() {
/> />
<input <input
type="text" type="text"
aria-label={t("giftMessage")}
placeholder={t("giftMessage")} placeholder={t("giftMessage")}
value={gift.message} value={gift.message}
onChange={(e) => onChange={(e) =>
@@ -791,8 +835,8 @@ export function CommandocentrumControls() {
</Button> </Button>
</div> </div>
</div> </div>
</div> )}
</section> </div>
</> </section>
); );
} }
+82 -43
View File
@@ -10,6 +10,8 @@ import {
OnlineUsersWidget, OnlineUsersWidget,
StatusCard, StatusCard,
} from "@/components/admin/dashboard"; } from "@/components/admin/dashboard";
import Link from "@/components/link";
import { env } from "@/env";
import { fetchOpsHealth } from "@/lib/admin/ops-health"; import { fetchOpsHealth } from "@/lib/admin/ops-health";
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users"; import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
import { db, EmulatorErrors, StaffActivities, User } from "@/lib/db"; import { db, EmulatorErrors, StaffActivities, User } from "@/lib/db";
@@ -17,6 +19,7 @@ import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions"; import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings"; import { siteSettings } from "@/lib/services/site-settings";
import { CommandocentrumControls } from "./commandocentrum-controls"; import { CommandocentrumControls } from "./commandocentrum-controls";
import { RefreshStatus } from "./refresh-status";
function decodeStacktrace(raw: unknown): string { function decodeStacktrace(raw: unknown): string {
if (raw == null) return ""; if (raw == null) return "";
@@ -47,28 +50,36 @@ export default async function CommandoCentrum() {
const t = await getTranslations("pages.admin.commandocentrum"); const t = await getTranslations("pages.admin.commandocentrum");
const [totalUsersRows, errors, health, maintenance, activity, onlineRoster] = const [
await Promise.all([ totalUsersRows,
db errorRows,
.select({ total: count() }) health,
.from(User) maintenance,
.catch(() => [{ total: -1 }]), activity,
db onlineRoster,
.select() ] = await Promise.all([
.from(EmulatorErrors) db
.orderBy(desc(EmulatorErrors.timestamp)) .select({ total: count() })
.limit(50) .from(User)
.catch(() => []), .catch(() => [{ total: -1 }]),
fetchOpsHealth(), db
siteSettings.getBool("maintenance_enabled", false), .select()
db .from(EmulatorErrors)
.select() .orderBy(desc(EmulatorErrors.timestamp))
.from(StaffActivities) .limit(50)
.orderBy(desc(StaffActivities.createdAt)) .catch(() => null),
.limit(12) fetchOpsHealth(),
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]), siteSettings.getBool("maintenance_enabled", false),
fetchOpsOnlineUsers(40), db
]); .select()
.from(StaffActivities)
.orderBy(desc(StaffActivities.createdAt))
.limit(12)
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]),
fetchOpsOnlineUsers(40),
]);
const errors = errorRows ?? [];
const errorsAvailable = errorRows !== null;
const totalUsers = Number(totalUsersRows[0]?.total ?? -1); const totalUsers = Number(totalUsersRows[0]?.total ?? -1);
const dbOk = health.dbOk; const dbOk = health.dbOk;
@@ -91,12 +102,30 @@ export default async function CommandoCentrum() {
} }
const appKeySet = !!process.env.APP_KEY; const appKeySet = !!process.env.APP_KEY;
const smtpSet = !!process.env.SMTP_HOST; const smtpSet = !!(env.SMTP_HOST || env.RESEND_API_KEY);
const rconSet = !!process.env.RCON_HOST; const rconSet =
env.EMULATOR_MODE === "api" ? !!env.EMULATOR_API_URL : !!env.RCON_HOST;
return ( return (
<AdminPageShell icon={Terminal} title={t("title")} subtitle={t("subtitle")}> <AdminPageShell icon={Terminal} title={t("title")} subtitle={t("subtitle")}>
<main> <div className="min-w-0">
<div className="mb-4 flex flex-wrap items-center gap-3">
<p className="text-sm">
{dbOk && emulatorOnline && redisOk !== false
? t("servicesHealthy")
: t("serviceAttention")}{" "}
·{" "}
{t("checkedAt", {
time: formatDate(new Date(), "datetime-seconds"),
})}
</p>
<RefreshStatus label={t("refreshStatus")} />
{canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank) ? (
<Link href="/admin/devops/cms-errors" className="btn btn-outline">
{t("cmsErrors")}
</Link>
) : null}
</div>
{/* ── Live status ────────────────────────────────────────── */} {/* ── Live status ────────────────────────────────────────── */}
<div <div
className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6" className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6"
@@ -104,7 +133,7 @@ export default async function CommandoCentrum() {
> >
<StatusCard <StatusCard
label={t("playersOnline")} label={t("playersOnline")}
value={onlineCount} value={dbOk ? onlineCount : "—"}
hint={ hint={
totalUsers >= 0 totalUsers >= 0
? t("totalAccounts", { count: totalUsers }) ? t("totalAccounts", { count: totalUsers })
@@ -129,13 +158,15 @@ export default async function CommandoCentrum() {
/> />
<StatusCard <StatusCard
label={t("emulatorErrors")} label={t("emulatorErrors")}
value={errors.length} value={errorsAvailable ? errors.length : "—"}
hint={t("last50Logged")} hint={errorsAvailable ? t("last50Logged") : t("readFailed")}
state={errors.length === 0 ? "ok" : "warn"} state={errorsAvailable && errors.length === 0 ? "ok" : "warn"}
icon="⚠️" icon="⚠️"
/> />
</div> </div>
<CommandocentrumControls />
{/* ── Online users widget ───────────────────────────────── */} {/* ── Online users widget ───────────────────────────────── */}
<section id="online" className="admin-card mb-6 scroll-mt-4"> <section id="online" className="admin-card mb-6 scroll-mt-4">
<h2 className="admin-section-title">{t("onlineUsers")}</h2> <h2 className="admin-section-title">{t("onlineUsers")}</h2>
@@ -144,8 +175,10 @@ export default async function CommandoCentrum() {
{/* ── Server info + diagnostics ──────────────────────────── */} {/* ── Server info + diagnostics ──────────────────────────── */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2"> <div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<section className="admin-card p-5"> <details className="admin-card p-5 self-start">
<h2 className="admin-section-title">{t("serverInfo")}</h2> <summary className="admin-section-title cursor-pointer">
{t("serverInfo")}
</summary>
<div className="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-2.5"> <div className="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-2.5">
<InfoItem label={t("node")} value={process.version} /> <InfoItem label={t("node")} value={process.version} />
<InfoItem <InfoItem
@@ -171,7 +204,7 @@ export default async function CommandoCentrum() {
/> />
<InfoItem label={t("cpus")} value={os.cpus().length} /> <InfoItem label={t("cpus")} value={os.cpus().length} />
</div> </div>
</section> </details>
<section className="admin-card p-5"> <section className="admin-card p-5">
<h2 className="admin-section-title">{t("systemDiagnostics")}</h2> <h2 className="admin-section-title">{t("systemDiagnostics")}</h2>
@@ -206,8 +239,8 @@ export default async function CommandoCentrum() {
state={emulatorOnline ? "ok" : "warn"} state={emulatorOnline ? "ok" : "warn"}
/> />
<DiagnosticRow <DiagnosticRow
label={t("rconConfig")} label={t("emulatorConfig")}
detail={rconSet ? t("rconHostSet") : t("rconHostNotConfigured")} detail={rconSet ? t("configured") : t("notConfigured")}
state={rconSet ? "ok" : "warn"} state={rconSet ? "ok" : "warn"}
/> />
<DiagnosticRow <DiagnosticRow
@@ -216,8 +249,8 @@ export default async function CommandoCentrum() {
state={appKeySet ? "ok" : "warn"} state={appKeySet ? "ok" : "warn"}
/> />
<DiagnosticRow <DiagnosticRow
label={t("emailSmtp")} label={t("emailConfig")}
detail={smtpSet ? t("smtpHostSet") : t("notConfigured")} detail={smtpSet ? t("configured") : t("notConfigured")}
state={smtpSet ? "ok" : "warn"} state={smtpSet ? "ok" : "warn"}
/> />
<DiagnosticRow <DiagnosticRow
@@ -231,8 +264,6 @@ export default async function CommandoCentrum() {
</section> </section>
</div> </div>
<CommandocentrumControls />
{/* ── Staff activity ─────────────────────────────────────── */} {/* ── Staff activity ─────────────────────────────────────── */}
<section className="mt-6"> <section className="mt-6">
<h2 className="admin-section-title">{t("recentStaffActivity")}</h2> <h2 className="admin-section-title">{t("recentStaffActivity")}</h2>
@@ -277,7 +308,9 @@ export default async function CommandoCentrum() {
{/* ── Emulator errors ────────────────────────────────────── */} {/* ── Emulator errors ────────────────────────────────────── */}
<section className="mt-6"> <section className="mt-6">
<h2 className="admin-section-title">{t("recentEmulatorErrors")}</h2> <h2 className="admin-section-title">{t("recentEmulatorErrors")}</h2>
{errors.length === 0 ? ( {!errorsAvailable ? (
<div className="admin-empty">{t("readFailed")}</div>
) : errors.length === 0 ? (
<div className="admin-empty">{t("noEmulatorErrors")}</div> <div className="admin-empty">{t("noEmulatorErrors")}</div>
) : ( ) : (
<div className="admin-card overflow-x-auto"> <div className="admin-card overflow-x-auto">
@@ -312,9 +345,15 @@ export default async function CommandoCentrum() {
{e.version} {e.version}
</td> </td>
<td> <td>
<pre className="text-xs p-2 bg-[var(--admin-canvas)] text-[var(--color-text-readable)] rounded overflow-auto max-h-[160px]"> <details>
{snippet} <summary className="cursor-pointer text-xs">
</pre> {trace.split("\n")[0]?.slice(0, 120) ||
t("empty")}
</summary>
<pre className="text-xs p-2 bg-[var(--admin-canvas)] text-[var(--color-text-readable)] rounded overflow-auto max-h-[160px]">
{snippet}
</pre>
</details>
</td> </td>
</tr> </tr>
); );
@@ -324,7 +363,7 @@ export default async function CommandoCentrum() {
</div> </div>
)} )}
</section> </section>
</main> </div>
</AdminPageShell> </AdminPageShell>
); );
} }
@@ -0,0 +1,20 @@
"use client";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import { Button } from "@/components/ui/button";
export function RefreshStatus({ label }: { label: string }) {
const router = useRouter();
const [pending, startTransition] = useTransition();
return (
<Button
type="button"
variant="outline"
disabled={pending}
aria-busy={pending}
onClick={() => startTransition(() => router.refresh())}
>
{pending ? "… " : ""}
{label}
</Button>
);
}
+19 -1
View File
@@ -2431,7 +2431,25 @@
}, },
"messagePlaceholder": "Message (for alerts)…", "messagePlaceholder": "Message (for alerts)…",
"amountPlaceholder": "Amount…", "amountPlaceholder": "Amount…",
"badgePlaceholder": "Badge code…" "badgePlaceholder": "Badge code…",
"commandCategories": "Command categories",
"groups": {
"hotel": "Hotel",
"players": "Players",
"rewards": "Currency and gifts",
"advanced": "Advanced"
},
"executing": "Executing command…",
"chooseCommand": "Choose an operation. Values entered are preserved when switching categories.",
"refreshStatus": "Refresh status",
"checkedAt": "Checked at {time}",
"serviceAttention": "Some services need attention",
"servicesHealthy": "Core services are responding",
"cmsErrors": "CMS error center",
"readFailed": "Data could not be loaded",
"emulatorConfig": "Emulator connection",
"emailConfig": "Email delivery",
"configured": "Configured"
}, },
"emulator": { "emulator": {
"title": "Emulator Config", "title": "Emulator Config",
+19 -1
View File
@@ -2313,7 +2313,25 @@
}, },
"messagePlaceholder": "Messaggio (per avvisi)…", "messagePlaceholder": "Messaggio (per avvisi)…",
"amountPlaceholder": "Importo…", "amountPlaceholder": "Importo…",
"badgePlaceholder": "Codice distintivo…" "badgePlaceholder": "Codice distintivo…",
"commandCategories": "Categorie comandi",
"groups": {
"hotel": "Hotel",
"players": "Giocatori",
"rewards": "Valute e regali",
"advanced": "Avanzati"
},
"executing": "Esecuzione comando…",
"chooseCommand": "Scegli un’operazione. I valori inseriti restano salvati cambiando categoria.",
"refreshStatus": "Aggiorna stato",
"checkedAt": "Verificato alle {time}",
"serviceAttention": "Alcuni servizi richiedono attenzione",
"servicesHealthy": "I servizi principali rispondono",
"cmsErrors": "Centro errori CMS",
"readFailed": "Impossibile caricare i dati",
"emulatorConfig": "Connessione emulatore",
"emailConfig": "Invio email",
"configured": "Configurato"
}, },
"emulator": { "emulator": {
"title": "Configurazione emulatore", "title": "Configurazione emulatore",