Revert "Merge pull request 'Complete Housekeeping migration and /ase cutover' (#52) from codex/housekeeping-complete into main"

This reverts commit 488b6e57c4, reversing
changes made to b506b4499a.
This commit is contained in:
Simo committed 2026-08-30 21:31:34 +02:00
1 parent 488b6e57c4
commit b1ddda66ff
802 files changed
+61370 -76659

No files matched your search

@@ -0,0 +1,798 @@
"use client";
import { useTranslations } from "next-intl";
import { useState } from "react";
import {
alertUser,
disconnectUser,
executeCommand,
forwardUser,
giveBadge,
giveCredits,
giveDiamonds,
giveDuckets,
hotelAlert,
sendGift,
setMotto,
setRank,
updateCatalog,
updateNavigator,
updateWordFilter,
} from "@/actions/commandocentrum";
import { Button } from "@/components/ui/button";
import { useServerAction } from "@/hooks/use-server-action";
const RCON_ERROR = "RCON command failed. Is the emulator running?";
export function CommandocentrumControls() {
const t = useTranslations("pages.admin.commandocentrum");
const { run, isPending } = useServerAction();
const [hotelMessage, setHotelMessage] = useState("");
const [disconnect, setDisconnect] = useState({ userId: "", username: "" });
const [alert, setAlert] = useState({ userId: "", message: "" });
const [forward, setForward] = useState({ userId: "", roomId: "" });
const [credits, setCredits] = useState({ userId: "", credits: "" });
const [duckets, setDuckets] = useState({ userId: "", amount: "" });
const [diamonds, setDiamonds] = useState({ userId: "", amount: "" });
const [badge, setBadge] = useState({ userId: "", badge: "" });
const [motto, setMottoForm] = useState({ userId: "", motto: "" });
const [rank, setRankForm] = useState({ userId: "", rank: "" });
const [command, setCommand] = useState({ userId: "", command: "" });
const [gift, setGift] = useState({
userId: "",
itemId: "",
message: t("defaultGiftMessage"),
});
return (
<>
<section className="mt-6">
<h2 className="admin-section-title">{t("emulatorControls")}</h2>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("updateCatalog")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
{t("updateCatalogDesc")}
</p>
<Button
type="button"
variant="default"
disabled={isPending}
onClick={() =>
run(() => updateCatalog(), {
successMessage: "Catalog updated.",
errorMessage: RCON_ERROR,
})
}
>
{t("updateCatalog")}
</Button>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("updateWordFilter")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
{t("updateWordFilterDesc")}
</p>
<Button
type="button"
variant="default"
disabled={isPending}
onClick={() =>
run(() => updateWordFilter(), {
successMessage: "Word filter updated.",
errorMessage: RCON_ERROR,
})
}
>
{t("updateWordFilter")}
</Button>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("updateNavigator")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-4">
{t("updateNavigatorDesc")}
</p>
<Button
type="button"
variant="default"
disabled={isPending}
onClick={() =>
run(() => updateNavigator(), {
successMessage: "Navigator updated.",
errorMessage: RCON_ERROR,
})
}
>
{t("updateNavigator")}
</Button>
</div>
</div>
<div className="admin-card mt-6">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("hotelAlert")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("hotelAlertDesc")}
</p>
<div className="flex gap-2">
<input
value={hotelMessage}
onChange={(e) => setHotelMessage(e.target.value)}
required
maxLength={512}
placeholder={t("messageToBroadcast")}
className="flex-1 min-w-[220px]"
disabled={isPending}
/>
<Button
type="button"
variant="destructive"
disabled={isPending || !hotelMessage.trim()}
onClick={() =>
run(() => hotelAlert({ message: hotelMessage }), {
successMessage: "Hotel alert sent.",
errorMessage: RCON_ERROR,
onSuccess: () => setHotelMessage(""),
})
}
>
{t("sendAlert")}
</Button>
</div>
</div>
</section>
<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">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("actions.disconnect")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("disconnectDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={disconnect.userId}
onChange={(e) =>
setDisconnect((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="text"
placeholder={t("usernameRequired")}
value={disconnect.username}
onChange={(e) =>
setDisconnect((s) => ({ ...s, username: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="destructive"
className="w-full"
disabled={
isPending || !disconnect.userId || !disconnect.username.trim()
}
onClick={() =>
run(
() =>
disconnectUser({
userId: Number(disconnect.userId),
username: disconnect.username,
}),
{
successMessage: "User disconnected.",
errorMessage: RCON_ERROR,
onSuccess: () =>
setDisconnect({ userId: "", username: "" }),
},
)
}
>
{t("actions.disconnect")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("actions.alert")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("alertDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={alert.userId}
onChange={(e) =>
setAlert((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="text"
placeholder={t("alertMessage")}
value={alert.message}
onChange={(e) =>
setAlert((s) => ({ ...s, message: e.target.value }))
}
maxLength={512}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !alert.userId || !alert.message.trim()}
onClick={() =>
run(
() =>
alertUser({
userId: Number(alert.userId),
message: alert.message,
}),
{
successMessage: "Alert sent.",
errorMessage: RCON_ERROR,
onSuccess: () => setAlert({ userId: "", message: "" }),
},
)
}
>
{t("actions.alert")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("forwardToRoom")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("forwardDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={forward.userId}
onChange={(e) =>
setForward((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="number"
min={1}
placeholder={t("roomId")}
value={forward.roomId}
onChange={(e) =>
setForward((s) => ({ ...s, roomId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !forward.userId || !forward.roomId}
onClick={() =>
run(
() =>
forwardUser({
userId: Number(forward.userId),
roomId: Number(forward.roomId),
}),
{
successMessage: "User forwarded.",
errorMessage: RCON_ERROR,
onSuccess: () => setForward({ userId: "", roomId: "" }),
},
)
}
>
{t("forwardUser")}
</Button>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveCredits")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("giveCreditsDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={credits.userId}
onChange={(e) =>
setCredits((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="number"
min={1}
placeholder={t("amount")}
value={credits.credits}
onChange={(e) =>
setCredits((s) => ({ ...s, credits: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !credits.userId || !credits.credits}
onClick={() =>
run(
() =>
giveCredits({
userId: Number(credits.userId),
credits: Number(credits.credits),
}),
{
successMessage: "Credits sent.",
errorMessage: RCON_ERROR,
onSuccess: () => setCredits({ userId: "", credits: "" }),
},
)
}
>
{t("giveCredits")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveDuckets")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("giveDucketsDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={duckets.userId}
onChange={(e) =>
setDuckets((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="number"
min={1}
placeholder={t("amount")}
value={duckets.amount}
onChange={(e) =>
setDuckets((s) => ({ ...s, amount: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !duckets.userId || !duckets.amount}
onClick={() =>
run(
() =>
giveDuckets({
userId: Number(duckets.userId),
amount: Number(duckets.amount),
}),
{
successMessage: "Duckets sent.",
errorMessage: RCON_ERROR,
onSuccess: () => setDuckets({ userId: "", amount: "" }),
},
)
}
>
{t("giveDuckets")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveDiamonds")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("giveDiamondsDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={diamonds.userId}
onChange={(e) =>
setDiamonds((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="number"
min={1}
placeholder={t("amount")}
value={diamonds.amount}
onChange={(e) =>
setDiamonds((s) => ({ ...s, amount: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !diamonds.userId || !diamonds.amount}
onClick={() =>
run(
() =>
giveDiamonds({
userId: Number(diamonds.userId),
amount: Number(diamonds.amount),
}),
{
successMessage: "Diamonds sent.",
errorMessage: RCON_ERROR,
onSuccess: () => setDiamonds({ userId: "", amount: "" }),
},
)
}
>
{t("giveDiamonds")}
</Button>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("giveBadge")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("giveBadgeDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={badge.userId}
onChange={(e) =>
setBadge((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="text"
placeholder={t("badgeCode")}
value={badge.badge}
onChange={(e) =>
setBadge((s) => ({ ...s, badge: e.target.value }))
}
maxLength={32}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !badge.userId || !badge.badge.trim()}
onClick={() =>
run(
() =>
giveBadge({
userId: Number(badge.userId),
badge: badge.badge,
}),
{
successMessage: "Badge granted.",
errorMessage: RCON_ERROR,
onSuccess: () => setBadge({ userId: "", badge: "" }),
},
)
}
>
{t("giveBadge")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("setMotto")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("setMottoDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={motto.userId}
onChange={(e) =>
setMottoForm((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="text"
placeholder={t("newMotto")}
value={motto.motto}
onChange={(e) =>
setMottoForm((s) => ({ ...s, motto: e.target.value }))
}
maxLength={127}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !motto.userId || !motto.motto.trim()}
onClick={() =>
run(
() =>
setMotto({
userId: Number(motto.userId),
motto: motto.motto,
}),
{
successMessage: "Motto updated.",
errorMessage: RCON_ERROR,
onSuccess: () => setMottoForm({ userId: "", motto: "" }),
},
)
}
>
{t("setMotto")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("setRank")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("setRankDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={rank.userId}
onChange={(e) =>
setRankForm((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="number"
min={0}
max={10}
placeholder={t("rankPlaceholder")}
value={rank.rank}
onChange={(e) =>
setRankForm((s) => ({ ...s, rank: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !rank.userId || rank.rank === ""}
onClick={() =>
run(
() =>
setRank({
userId: Number(rank.userId),
rank: Number(rank.rank),
}),
{
successMessage: "Rank updated.",
errorMessage: RCON_ERROR,
onSuccess: () => setRankForm({ userId: "", rank: "" }),
},
)
}
>
{t("setRank")}
</Button>
</div>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4 mt-4">
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("executeCommand")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("executeCommandDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={command.userId}
onChange={(e) =>
setCommand((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="text"
placeholder={t("commandPlaceholder")}
value={command.command}
onChange={(e) =>
setCommand((s) => ({ ...s, command: e.target.value }))
}
maxLength={100}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={
isPending || !command.userId || !command.command.trim()
}
onClick={() =>
run(
() =>
executeCommand({
userId: Number(command.userId),
command: command.command,
}),
{
successMessage: "Command executed.",
errorMessage: RCON_ERROR,
onSuccess: () => setCommand({ userId: "", command: "" }),
},
)
}
>
{t("execute")}
</Button>
</div>
</div>
<div className="admin-card">
<h3 className="text-sm font-bold text-[var(--admin-text)] mb-2">
{t("sendGift")}
</h3>
<p className="text-xs text-[var(--admin-text-muted)] mb-3">
{t("sendGiftDesc")}
</p>
<div className="space-y-2">
<input
type="number"
min={1}
placeholder={t("userId")}
value={gift.userId}
onChange={(e) =>
setGift((s) => ({ ...s, userId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="number"
min={1}
placeholder={t("furniId")}
value={gift.itemId}
onChange={(e) =>
setGift((s) => ({ ...s, itemId: e.target.value }))
}
className="w-full"
disabled={isPending}
/>
<input
type="text"
placeholder={t("giftMessage")}
value={gift.message}
onChange={(e) =>
setGift((s) => ({ ...s, message: e.target.value }))
}
maxLength={255}
className="w-full"
disabled={isPending}
/>
<Button
type="button"
variant="default"
className="w-full"
disabled={isPending || !gift.userId || !gift.itemId}
onClick={() =>
run(
() =>
sendGift({
userId: Number(gift.userId),
itemId: Number(gift.itemId),
message: gift.message,
}),
{
successMessage: "Gift sent.",
errorMessage: RCON_ERROR,
onSuccess: () =>
setGift({
userId: "",
itemId: "",
message: t("defaultGiftMessage"),
}),
},
)
}
>
{t("sendGift")}
</Button>
</div>
</div>
</div>
</section>
</>
);
}
@@ -0,0 +1 @@
export { default } from "../loading";
+330
View File
@@ -0,0 +1,330 @@
import os from "node:os";
import { count, desc, inArray } from "drizzle-orm";
import { Terminal } from "lucide-react";
import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { AdminPageShell } from "@/components/admin/admin-page-shell";
import {
DiagnosticRow,
InfoItem,
OnlineUsersWidget,
StatusCard,
} from "@/components/admin/dashboard";
import { fetchOpsHealth } from "@/lib/admin/ops-health";
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
import { db, EmulatorErrors, StaffActivities, User } from "@/lib/db";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { siteSettings } from "@/lib/services/site-settings";
import { CommandocentrumControls } from "./commandocentrum-controls";
function decodeStacktrace(raw: unknown): string {
if (raw == null) return "";
try {
if (typeof raw === "string") return raw;
return Buffer.from(raw as Uint8Array).toString("utf8");
} catch {
return "";
}
}
function mb(bytes: number): string {
return `${Math.round(bytes / 1024 / 1024)} MB`;
}
function uptime(seconds: number): string {
const d = Math.floor(seconds / 86400);
const h = Math.floor((seconds % 86400) / 3600);
const m = Math.floor((seconds % 3600) / 60);
return d > 0 ? `${d}d ${h}h ${m}m` : h > 0 ? `${h}h ${m}m` : `${m}m`;
}
export default async function CommandoCentrum() {
const { session, permissions } = await getAdminContext();
if (!canAccess(permissions, PERMS.RCON_EXECUTE, session.user.rank)) {
redirect("/admin");
}
const t = await getTranslations("pages.admin.commandocentrum");
const [totalUsersRows, errors, health, maintenance, activity, onlineRoster] =
await Promise.all([
db
.select({ total: count() })
.from(User)
.catch(() => [{ total: -1 }]),
db
.select()
.from(EmulatorErrors)
.orderBy(desc(EmulatorErrors.timestamp))
.limit(50)
.catch(() => []),
fetchOpsHealth(),
siteSettings.getBool("maintenance_enabled", false),
db
.select()
.from(StaffActivities)
.orderBy(desc(StaffActivities.createdAt))
.limit(12)
.catch(() => [] as (typeof StaffActivities.$inferSelect)[]),
fetchOpsOnlineUsers(40),
]);
const totalUsers = Number(totalUsersRows[0]?.total ?? -1);
const dbOk = health.dbOk;
const redisOk = health.redisOk;
const emulatorOnline = health.emulatorOk;
const onlineCount = onlineRoster.count;
const onlineUsers = onlineRoster.users;
const staffIds = Array.from(
new Set(activity.map((a) => Number(a.userId))),
).filter((n) => n > 0);
let nameById = new Map<number, string>();
if (staffIds.length) {
const users = await db
.select({ id: User.id, username: User.username })
.from(User)
.where(inArray(User.id, staffIds))
.catch(() => []);
nameById = new Map(users.map((u) => [u.id, u.username]));
}
const appKeySet = !!process.env.APP_KEY;
const smtpSet = !!process.env.SMTP_HOST;
const rconSet = !!process.env.RCON_HOST;
return (
<AdminPageShell icon={Terminal} title={t("title")} subtitle={t("subtitle")}>
<main>
{/* ── Live status ────────────────────────────────────────── */}
<div
className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5 mb-6"
style={{ marginTop: "1.1rem" }}
>
<StatusCard
label={t("playersOnline")}
value={onlineCount}
hint={
totalUsers >= 0
? t("totalAccounts", { count: totalUsers })
: t("dbUnavailable")
}
state="neutral"
icon="👤"
/>
<StatusCard
label={t("emulator")}
value={emulatorOnline ? t("online") : t("offline")}
hint={emulatorOnline ? t("rconReachable") : t("rconNotResponding")}
state={emulatorOnline ? "ok" : "danger"}
icon="🎮"
/>
<StatusCard
label={t("database")}
value={dbOk ? t("connected") : t("down")}
hint={dbOk ? t("queriesResponding") : t("cannotReachDb")}
state={dbOk ? "ok" : "danger"}
icon="🗄️"
/>
<StatusCard
label={t("emulatorErrors")}
value={errors.length}
hint={t("last50Logged")}
state={errors.length === 0 ? "ok" : "warn"}
icon="⚠️"
/>
</div>
{/* ── Online users widget ───────────────────────────────── */}
<section id="online" className="admin-card mb-6 scroll-mt-4">
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
<OnlineUsersWidget users={onlineUsers} isLoading={false} />
</section>
{/* ── Server info + diagnostics ──────────────────────────── */}
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
<section className="admin-card p-5">
<h2 className="admin-section-title">{t("serverInfo")}</h2>
<div className="grid grid-cols-[repeat(auto-fit,minmax(150px,1fr))] gap-2.5">
<InfoItem label={t("node")} value={process.version} />
<InfoItem
label={t("platform")}
value={`${process.platform} ${process.arch}`}
/>
<InfoItem
label={t("appUptime")}
value={uptime(process.uptime())}
/>
<InfoItem
label={t("heapUsed")}
value={mb(process.memoryUsage().heapUsed)}
/>
<InfoItem
label={t("rss")}
value={mb(process.memoryUsage().rss)}
/>
<InfoItem label={t("hostUptime")} value={uptime(os.uptime())} />
<InfoItem
label={t("load1m")}
value={os.loadavg()[0]?.toFixed(2) ?? "—"}
/>
<InfoItem label={t("cpus")} value={os.cpus().length} />
</div>
</section>
<section className="admin-card p-5">
<h2 className="admin-section-title">{t("systemDiagnostics")}</h2>
<div className="flex flex-col">
<DiagnosticRow
label={t("database")}
detail={dbOk ? t("connectionHealthy") : t("unreachable")}
state={dbOk ? "ok" : "danger"}
/>
<DiagnosticRow
label="Redis"
detail={
redisOk === true
? "PONG"
: redisOk === false
? "Unreachable"
: "REDIS_URL not configured"
}
state={
redisOk === true
? "ok"
: redisOk === false
? "danger"
: "warn"
}
/>
<DiagnosticRow
label={t("emulatorRcon")}
detail={
emulatorOnline ? t("respondingToPing") : t("noResponse")
}
state={emulatorOnline ? "ok" : "warn"}
/>
<DiagnosticRow
label={t("rconConfig")}
detail={rconSet ? t("rconHostSet") : t("rconHostNotConfigured")}
state={rconSet ? "ok" : "warn"}
/>
<DiagnosticRow
label={t("twoFaEncryptionKey")}
detail={appKeySet ? t("appKeyConfigured") : t("appKeyMissing")}
state={appKeySet ? "ok" : "warn"}
/>
<DiagnosticRow
label={t("emailSmtp")}
detail={smtpSet ? t("smtpHostSet") : t("notConfigured")}
state={smtpSet ? "ok" : "warn"}
/>
<DiagnosticRow
label={t("maintenanceMode")}
detail={
maintenance ? t("siteIsInMaintenance") : t("siteIsLive")
}
state={maintenance ? "warn" : "ok"}
/>
</div>
</section>
</div>
<CommandocentrumControls />
{/* ── Staff activity ─────────────────────────────────────── */}
<section className="mt-6">
<h2 className="admin-section-title">{t("recentStaffActivity")}</h2>
{activity.length === 0 ? (
<div className="admin-empty">{t("noStaffActivity")}</div>
) : (
<div className="admin-card overflow-x-auto">
<table>
<thead>
<tr>
<th>{t("colWhen")}</th>
<th>{t("colStaff")}</th>
<th>{t("colAction")}</th>
<th>{t("colDescription")}</th>
</tr>
</thead>
<tbody>
{activity.map((a) => (
<tr key={String(a.id)}>
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
{formatDate(a.createdAt, "datetime-seconds")}
</td>
<td>
{nameById.get(Number(a.userId)) ?? `#${a.userId}`}
</td>
<td>
<span className="inline-block text-[0.6rem] font-bold px-2 py-0.5 rounded-full bg-[var(--admin-accent)]/18 text-[var(--admin-text)]">
{a.action}
</span>
</td>
<td className="text-sm theme-text-muted dark:theme-text-muted">
{a.description}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</section>
{/* ── Emulator errors ────────────────────────────────────── */}
<section className="mt-6">
<h2 className="admin-section-title">{t("recentEmulatorErrors")}</h2>
{errors.length === 0 ? (
<div className="admin-empty">{t("noEmulatorErrors")}</div>
) : (
<div className="admin-card overflow-x-auto">
<table>
<thead>
<tr>
<th>{t("colWhen")}</th>
<th>{t("colType")}</th>
<th>{t("colVersion")}</th>
<th>{t("colStacktrace")}</th>
</tr>
</thead>
<tbody>
{errors.map((e) => {
const trace = decodeStacktrace(e.stacktrace);
const snippet = trace
? trace.split("\n").slice(0, 20).join("\n") +
(trace.split("\n").length > 20
? `\n… ${t("truncated")}`
: "")
: t("empty");
return (
<tr key={e.id}>
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
{formatDate(
e.timestamp ? new Date(e.timestamp * 1000) : null,
"datetime-seconds",
)}
</td>
<td>{e.type}</td>
<td className="text-sm theme-text-muted dark:theme-text-muted whitespace-nowrap">
{e.version}
</td>
<td>
<pre className="text-xs p-2 bg-[var(--admin-canvas)] text-[var(--color-text-readable)] rounded overflow-auto max-h-[160px]">
{snippet}
</pre>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</section>
</main>
</AdminPageShell>
);
}