fix(admin): live online widget, ticket queue clarity, i18n+contract coverage
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m27s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-28 20:22:50 +02:00
1 parent 9177230dc2
commit 01126207dc
26 files changed
+1635 -114

No files matched your search

+24 -1
View File
@@ -53,6 +53,7 @@ export default async function CommandoCentrum() {
emulatorOnline,
maintenance,
activity,
onlineUsersRaw,
] = await Promise.all([
prisma.user.count({ where: { online: "1" } }).catch(() => 0),
prisma.user.count().catch(() => -1),
@@ -70,8 +71,30 @@ export default async function CommandoCentrum() {
.catch(
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
),
prisma.user
.findMany({
where: { online: "1" },
select: {
id: true,
username: true,
look: true,
lastOnline: true,
},
orderBy: { lastOnline: "desc" },
take: 40,
})
.catch(() => []),
]);
const onlineUsers = onlineUsersRaw.map((u) => ({
id: u.id,
username: u.username,
look: u.look,
roomId: null as number | null,
roomName: null as string | null,
lastActivity: u.lastOnline > 0 ? new Date(u.lastOnline * 1000) : new Date(),
}));
const staffIds = Array.from(
new Set(activity.map((a) => Number(a.userId))),
).filter((n) => n > 0);
@@ -134,7 +157,7 @@ export default async function CommandoCentrum() {
{/* ── Online users widget ───────────────────────────────── */}
<section className="admin-card mb-6">
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
<OnlineUsersWidget users={[]} isLoading={false} />
<OnlineUsersWidget users={onlineUsers} isLoading={false} />
</section>
{/* ── Server info + diagnostics ──────────────────────────── */}
+14
View File
@@ -160,6 +160,20 @@ export default async function AdminHelpTicketsPage({
return (
<div className="space-y-6">
<div
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
role="status"
>
<p className="m-0">{t("queueHint")}</p>
<p className="m-0 mt-1">
<a
href="/admin/tickets"
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
>
{t("queueOtherCta")}
</a>
</p>
</div>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
+14
View File
@@ -116,6 +116,20 @@ export default async function AdminTicketsPage({
return (
<div className="space-y-6">
<div
className="rounded-lg border border-[var(--admin-info-border)] bg-[var(--admin-info-subtle)] px-4 py-3 text-sm"
role="status"
>
<p className="m-0">{t("queueHint")}</p>
<p className="m-0 mt-1">
<a
href="/admin/help-tickets"
className="font-medium text-[var(--admin-accent)] underline-offset-2 hover:underline"
>
{t("queueOtherCta")}
</a>
</p>
</div>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
<StatusCard
+33
View File
@@ -21,6 +21,7 @@ const ROUTES: Array<[string, string]> = [
["shop", "PERMS.SHOP_VIEW"],
["transactions", "PERMS.SHOP_VIEW"],
["vouchers", "PERMS.SHOP_VIEW"],
["marketplace", "PERMS.SHOP_VIEW"],
["vpn", "PERMS.SETTINGS_VIEW"],
["ip", "PERMS.SETTINGS_VIEW"],
["maintenance", "PERMS.SETTINGS_VIEW"],
@@ -32,11 +33,25 @@ const ROUTES: Array<[string, string]> = [
["badges", "PERMS.CATALOG_VIEW"],
["teams", "PERMS.USERS_VIEW"],
["applications", "PERMS.USERS_VIEW"],
["guilds", "PERMS.USERS_VIEW"],
["tickets", "PERMS.TICKETS_VIEW"],
["help-tickets", "PERMS.TICKETS_VIEW"],
["permissions", "PERMS.PERMISSIONS_MANAGE"],
["housekeeping", "PERMS.SETTINGS_VIEW"],
["logs", "PERMS.LOGS_VIEW"],
["catalog", "PERMS.CATALOG_VIEW"],
["rooms/edit/[id]", "PERMS.ROOMS_EDIT"],
];
const MOD_ROUTES: Array<[string, string]> = [
["", "requireMod"],
["cfh", "PERMS.MOD_CFH_VIEW"],
["actions", "PERMS.MOD_ACTIONS"],
["bans", "PERMS.MOD_BANS_VIEW"],
["tickets", "PERMS.MOD_TICKETS_VIEW"],
["team", "PERMS.MOD_TEAM_VIEW"],
];
const ACTION_GATES: Array<[string, string]> = [
["admin-settings.ts", "PERMS.SETTINGS_EDIT"],
["admin-theme.ts", "PERMS.SETTINGS_EDIT"],
@@ -54,6 +69,11 @@ const ACTION_GATES: Array<[string, string]> = [
["admin-alerts.ts", "PERMS.NOTIFICATIONS_EDIT"],
["commandocentrum.ts", "PERMS.RCON_EXECUTE"],
["translations.ts", "PERMS.SETTINGS_EDIT"],
["tickets.ts", "PERMS.TICKETS_EDIT"],
["admin-help-tickets.ts", "PERMS.TICKETS_EDIT"],
["permissions.ts", "PERMS.PERMISSIONS_MANAGE"],
["admin-guilds.ts", "PERMS.USERS_EDIT"],
["moderation.ts", "PERMS.MODERATION_EDIT"],
];
describe("admin operations route contract", () => {
@@ -63,6 +83,19 @@ describe("admin operations route contract", () => {
expect(readFileSync(path, "utf8"), path).toContain(permission);
});
it.each(MOD_ROUTES)("provides and guards /mod/%s", (route, permission) => {
const path =
route === "" ? "src/app/mod/page.tsx" : `src/app/mod/${route}/page.tsx`;
expect(existsSync(path), path).toBe(true);
const source = readFileSync(path, "utf8");
const layout = readFileSync("src/app/mod/layout.tsx", "utf8");
if (permission === "requireMod") {
expect(layout).toContain("requireMod");
} else {
expect(source).toContain(permission);
}
});
it("guards radio section via layout", () => {
const path = "src/app/admin/radio/layout.tsx";
expect(existsSync(path), path).toBe(true);
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+8 -4
View File
@@ -1492,8 +1492,10 @@
"settingsSaved": "VPN settings saved successfully"
},
"tickets": {
"title": "Tickets",
"subtitle": "Support tickets from players",
"title": "CMS support desk",
"subtitle": "Internal CMS tickets (assignee, priority, staff workflow). Not the same as help-center tickets from /help.",
"queueHint": "This queue is the CMS support desk. Player help-center tickets live in the Help-center tickets tab.",
"queueOtherCta": "Open help-center tickets",
"noTickets": "No active tickets",
"searchPlaceholder": "Search subject, user, category or ID…",
"presetActive": "Active",
@@ -1510,7 +1512,9 @@
},
"helpTickets": {
"title": "Help center tickets",
"subtitle": "Player tickets from /help/tickets",
"subtitle": "Tickets players open at /help/tickets. Separate from the CMS support desk.",
"queueHint": "This queue is the public help center. CMS support desk tickets are in the Support desk tab.",
"queueOtherCta": "Open CMS support desk",
"noTickets": "No open help center tickets",
"searchPlaceholder": "Search title, user or ID…",
"presetOpen": "Open",
@@ -2520,7 +2524,7 @@
"events": "Events",
"eventsSubtitle": "Hotel events and event types",
"tickets": "Support desk",
"ticketsSubtitle": "CMS tickets, help-center tickets, and reply templates",
"ticketsSubtitle": "Two separate queues: CMS support desk and public help-center tickets",
"staffAccess": "Ranks & permissions",
"staffAccessSubtitle": "Staff ranks and live access. Archive is compare/export only.",
"analytics": "Analytics",
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+11 -7
View File
@@ -1467,8 +1467,8 @@
"settingsSaved": "Impostazioni VPN salvate con successo"
},
"tickets": {
"title": "Ticket",
"subtitle": "Ticket di supporto dai giocatori",
"title": "Supporto CMS",
"subtitle": "Ticket interni CMS (assegnatario, priorita, workflow staff). Non sono i ticket del help center /help.",
"noTickets": "Nessun ticket attivo",
"searchPlaceholder": "Cerca oggetto, utente, categoria o ID…",
"presetActive": "Attivi",
@@ -1481,11 +1481,13 @@
"colPriority": "Priorità",
"colAssigned": "Assegnato",
"colDate": "Data",
"meta": "di {user} · {count} msg · {category}"
"meta": "di {user} · {count} msg · {category}",
"queueHint": "Questa coda e il supporto CMS. I ticket del help center pubblici stanno nella tab Help-center tickets.",
"queueOtherCta": "Apri help-center tickets"
},
"helpTickets": {
"title": "Ticket centro assistenza",
"subtitle": "Ticket dei giocatori da /help/tickets",
"title": "Ticket help center",
"subtitle": "Ticket aperti dai player su /help/tickets. Separati dal supporto CMS.",
"noTickets": "Nessun ticket aperto nel centro assistenza",
"searchPlaceholder": "Cerca titolo, utente o ID…",
"presetOpen": "Aperti",
@@ -1516,7 +1518,9 @@
"replied": "Risposta inviata.",
"closed": "Ticket chiuso.",
"reopened": "Ticket riaperto."
}
},
"queueHint": "Questa coda e il help center pubblico. I ticket del supporto CMS stanno nella tab Support desk.",
"queueOtherCta": "Apri supporto CMS"
},
"cfh": {
"title": "Call for Help",
@@ -2791,7 +2795,7 @@
"events": "Eventi",
"eventsSubtitle": "Eventi hotel e tipi evento",
"tickets": "Assistenza",
"ticketsSubtitle": "Ticket CMS, ticket centro assistenza e template risposte",
"ticketsSubtitle": "Due code separate: supporto CMS e ticket help center pubblici",
"staffAccess": "Gradi e permessi",
"staffAccessSubtitle": "Gradi staff e accesso live. L'archivio e solo confronto/export.",
"analytics": "Analitiche",
+11 -7
View File
@@ -2842,7 +2842,7 @@
"events": "Evenementen",
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
"tickets": "Supportbalie",
"ticketsSubtitle": "CMS-tickets, helpcentrum-tickets en antwoordtemplates",
"ticketsSubtitle": "Twee aparte wachtrijen: CMS support desk en publieke helpcenter-tickets",
"staffAccess": "Rangen & rechten",
"staffAccessSubtitle": "Staffrangen en live toegang. Archief is alleen vergelijken/exporteren.",
"analytics": "Analytics",
@@ -2851,8 +2851,8 @@
"devopsSubtitle": "Servicegezondheid en emulatorfouten"
},
"tickets": {
"title": "Tickets",
"subtitle": "Ondersteuningstickets van spelers",
"title": "CMS support desk",
"subtitle": "Interne CMS-tickets (assignee, prioriteit, staff-workflow). Niet hetzelfde als helpcenter-tickets van /help.",
"noTickets": "Geen actieve tickets",
"searchPlaceholder": "Zoek op onderwerp, gebruiker, categorie of ID…",
"presetActive": "Actief",
@@ -2865,11 +2865,13 @@
"colPriority": "Prioriteit",
"colAssigned": "Toegewezen",
"colDate": "Datum",
"meta": "door {user} · {count} berichten · {category}"
"meta": "door {user} · {count} berichten · {category}",
"queueHint": "Deze wachtrij is de CMS support desk. Speler-helpcenter tickets staan onder Help-center tickets.",
"queueOtherCta": "Open helpcenter-tickets"
},
"helpTickets": {
"title": "Helpcentrum-tickets",
"subtitle": "Spelertickets van /help/tickets",
"title": "Helpcenter-tickets",
"subtitle": "Tickets die spelers openen op /help/tickets. Apart van de CMS support desk.",
"noTickets": "Geen open helpcentrum-tickets",
"searchPlaceholder": "Zoek op titel, gebruiker of ID…",
"presetOpen": "Open",
@@ -2900,7 +2902,9 @@
"replied": "Antwoord verzonden.",
"closed": "Ticket gesloten.",
"reopened": "Ticket heropend."
}
},
"queueHint": "Deze wachtrij is het publieke helpcenter. CMS support desk tickets staan onder Support desk.",
"queueOtherCta": "Open CMS support desk"
},
"cfh": {
"title": "Call for Help",
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff
+80 -5
View File
File diff suppressed because it is too large. Load diff