feat(admin): organize command center controls and diagnostics
This commit is contained in:
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
Reference in new issue
Block a user