fix(admin): live online widget, ticket queue clarity, i18n+contract coverage
Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
9177230dc2
commit
01126207dc
26 files changed
+1635
-114
No files matched your search
@@ -53,6 +53,7 @@ export default async function CommandoCentrum() {
|
|||||||
emulatorOnline,
|
emulatorOnline,
|
||||||
maintenance,
|
maintenance,
|
||||||
activity,
|
activity,
|
||||||
|
onlineUsersRaw,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
prisma.user.count({ where: { online: "1" } }).catch(() => 0),
|
prisma.user.count({ where: { online: "1" } }).catch(() => 0),
|
||||||
prisma.user.count().catch(() => -1),
|
prisma.user.count().catch(() => -1),
|
||||||
@@ -70,8 +71,30 @@ export default async function CommandoCentrum() {
|
|||||||
.catch(
|
.catch(
|
||||||
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
|
() => [] 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(
|
const staffIds = Array.from(
|
||||||
new Set(activity.map((a) => Number(a.userId))),
|
new Set(activity.map((a) => Number(a.userId))),
|
||||||
).filter((n) => n > 0);
|
).filter((n) => n > 0);
|
||||||
@@ -134,7 +157,7 @@ export default async function CommandoCentrum() {
|
|||||||
{/* ── Online users widget ───────────────────────────────── */}
|
{/* ── Online users widget ───────────────────────────────── */}
|
||||||
<section className="admin-card mb-6">
|
<section className="admin-card mb-6">
|
||||||
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
|
<h2 className="admin-section-title">{t("onlineUsers")}</h2>
|
||||||
<OnlineUsersWidget users={[]} isLoading={false} />
|
<OnlineUsersWidget users={onlineUsers} isLoading={false} />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* ── Server info + diagnostics ──────────────────────────── */}
|
{/* ── Server info + diagnostics ──────────────────────────── */}
|
||||||
|
|||||||
@@ -160,6 +160,20 @@ export default async function AdminHelpTicketsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
<StatusCard label={t("statusClosed")} value={closedCount} icon="✅" />
|
||||||
|
|||||||
@@ -116,6 +116,20 @@ export default async function AdminTicketsPage({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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">
|
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
|
||||||
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
|
||||||
<StatusCard
|
<StatusCard
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const ROUTES: Array<[string, string]> = [
|
|||||||
["shop", "PERMS.SHOP_VIEW"],
|
["shop", "PERMS.SHOP_VIEW"],
|
||||||
["transactions", "PERMS.SHOP_VIEW"],
|
["transactions", "PERMS.SHOP_VIEW"],
|
||||||
["vouchers", "PERMS.SHOP_VIEW"],
|
["vouchers", "PERMS.SHOP_VIEW"],
|
||||||
|
["marketplace", "PERMS.SHOP_VIEW"],
|
||||||
["vpn", "PERMS.SETTINGS_VIEW"],
|
["vpn", "PERMS.SETTINGS_VIEW"],
|
||||||
["ip", "PERMS.SETTINGS_VIEW"],
|
["ip", "PERMS.SETTINGS_VIEW"],
|
||||||
["maintenance", "PERMS.SETTINGS_VIEW"],
|
["maintenance", "PERMS.SETTINGS_VIEW"],
|
||||||
@@ -32,11 +33,25 @@ const ROUTES: Array<[string, string]> = [
|
|||||||
["badges", "PERMS.CATALOG_VIEW"],
|
["badges", "PERMS.CATALOG_VIEW"],
|
||||||
["teams", "PERMS.USERS_VIEW"],
|
["teams", "PERMS.USERS_VIEW"],
|
||||||
["applications", "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"],
|
["logs", "PERMS.LOGS_VIEW"],
|
||||||
["catalog", "PERMS.CATALOG_VIEW"],
|
["catalog", "PERMS.CATALOG_VIEW"],
|
||||||
["rooms/edit/[id]", "PERMS.ROOMS_EDIT"],
|
["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]> = [
|
const ACTION_GATES: Array<[string, string]> = [
|
||||||
["admin-settings.ts", "PERMS.SETTINGS_EDIT"],
|
["admin-settings.ts", "PERMS.SETTINGS_EDIT"],
|
||||||
["admin-theme.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"],
|
["admin-alerts.ts", "PERMS.NOTIFICATIONS_EDIT"],
|
||||||
["commandocentrum.ts", "PERMS.RCON_EXECUTE"],
|
["commandocentrum.ts", "PERMS.RCON_EXECUTE"],
|
||||||
["translations.ts", "PERMS.SETTINGS_EDIT"],
|
["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", () => {
|
describe("admin operations route contract", () => {
|
||||||
@@ -63,6 +83,19 @@ describe("admin operations route contract", () => {
|
|||||||
expect(readFileSync(path, "utf8"), path).toContain(permission);
|
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", () => {
|
it("guards radio section via layout", () => {
|
||||||
const path = "src/app/admin/radio/layout.tsx";
|
const path = "src/app/admin/radio/layout.tsx";
|
||||||
expect(existsSync(path), path).toBe(true);
|
expect(existsSync(path), path).toBe(true);
|
||||||
|
|||||||
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
@@ -1492,8 +1492,10 @@
|
|||||||
"settingsSaved": "VPN settings saved successfully"
|
"settingsSaved": "VPN settings saved successfully"
|
||||||
},
|
},
|
||||||
"tickets": {
|
"tickets": {
|
||||||
"title": "Tickets",
|
"title": "CMS support desk",
|
||||||
"subtitle": "Support tickets from players",
|
"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",
|
"noTickets": "No active tickets",
|
||||||
"searchPlaceholder": "Search subject, user, category or ID…",
|
"searchPlaceholder": "Search subject, user, category or ID…",
|
||||||
"presetActive": "Active",
|
"presetActive": "Active",
|
||||||
@@ -1510,7 +1512,9 @@
|
|||||||
},
|
},
|
||||||
"helpTickets": {
|
"helpTickets": {
|
||||||
"title": "Help center tickets",
|
"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",
|
"noTickets": "No open help center tickets",
|
||||||
"searchPlaceholder": "Search title, user or ID…",
|
"searchPlaceholder": "Search title, user or ID…",
|
||||||
"presetOpen": "Open",
|
"presetOpen": "Open",
|
||||||
@@ -2520,7 +2524,7 @@
|
|||||||
"events": "Events",
|
"events": "Events",
|
||||||
"eventsSubtitle": "Hotel events and event types",
|
"eventsSubtitle": "Hotel events and event types",
|
||||||
"tickets": "Support desk",
|
"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",
|
"staffAccess": "Ranks & permissions",
|
||||||
"staffAccessSubtitle": "Staff ranks and live access. Archive is compare/export only.",
|
"staffAccessSubtitle": "Staff ranks and live access. Archive is compare/export only.",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
|
|||||||
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+11
-7
@@ -1467,8 +1467,8 @@
|
|||||||
"settingsSaved": "Impostazioni VPN salvate con successo"
|
"settingsSaved": "Impostazioni VPN salvate con successo"
|
||||||
},
|
},
|
||||||
"tickets": {
|
"tickets": {
|
||||||
"title": "Ticket",
|
"title": "Supporto CMS",
|
||||||
"subtitle": "Ticket di supporto dai giocatori",
|
"subtitle": "Ticket interni CMS (assegnatario, priorita, workflow staff). Non sono i ticket del help center /help.",
|
||||||
"noTickets": "Nessun ticket attivo",
|
"noTickets": "Nessun ticket attivo",
|
||||||
"searchPlaceholder": "Cerca oggetto, utente, categoria o ID…",
|
"searchPlaceholder": "Cerca oggetto, utente, categoria o ID…",
|
||||||
"presetActive": "Attivi",
|
"presetActive": "Attivi",
|
||||||
@@ -1481,11 +1481,13 @@
|
|||||||
"colPriority": "Priorità",
|
"colPriority": "Priorità",
|
||||||
"colAssigned": "Assegnato",
|
"colAssigned": "Assegnato",
|
||||||
"colDate": "Data",
|
"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": {
|
"helpTickets": {
|
||||||
"title": "Ticket centro assistenza",
|
"title": "Ticket help center",
|
||||||
"subtitle": "Ticket dei giocatori da /help/tickets",
|
"subtitle": "Ticket aperti dai player su /help/tickets. Separati dal supporto CMS.",
|
||||||
"noTickets": "Nessun ticket aperto nel centro assistenza",
|
"noTickets": "Nessun ticket aperto nel centro assistenza",
|
||||||
"searchPlaceholder": "Cerca titolo, utente o ID…",
|
"searchPlaceholder": "Cerca titolo, utente o ID…",
|
||||||
"presetOpen": "Aperti",
|
"presetOpen": "Aperti",
|
||||||
@@ -1516,7 +1518,9 @@
|
|||||||
"replied": "Risposta inviata.",
|
"replied": "Risposta inviata.",
|
||||||
"closed": "Ticket chiuso.",
|
"closed": "Ticket chiuso.",
|
||||||
"reopened": "Ticket riaperto."
|
"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": {
|
"cfh": {
|
||||||
"title": "Call for Help",
|
"title": "Call for Help",
|
||||||
@@ -2791,7 +2795,7 @@
|
|||||||
"events": "Eventi",
|
"events": "Eventi",
|
||||||
"eventsSubtitle": "Eventi hotel e tipi evento",
|
"eventsSubtitle": "Eventi hotel e tipi evento",
|
||||||
"tickets": "Assistenza",
|
"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",
|
"staffAccess": "Gradi e permessi",
|
||||||
"staffAccessSubtitle": "Gradi staff e accesso live. L'archivio e solo confronto/export.",
|
"staffAccessSubtitle": "Gradi staff e accesso live. L'archivio e solo confronto/export.",
|
||||||
"analytics": "Analitiche",
|
"analytics": "Analitiche",
|
||||||
|
|||||||
+11
-7
@@ -2842,7 +2842,7 @@
|
|||||||
"events": "Evenementen",
|
"events": "Evenementen",
|
||||||
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
|
"eventsSubtitle": "Hotel-evenementen en evenementtypes",
|
||||||
"tickets": "Supportbalie",
|
"tickets": "Supportbalie",
|
||||||
"ticketsSubtitle": "CMS-tickets, helpcentrum-tickets en antwoordtemplates",
|
"ticketsSubtitle": "Twee aparte wachtrijen: CMS support desk en publieke helpcenter-tickets",
|
||||||
"staffAccess": "Rangen & rechten",
|
"staffAccess": "Rangen & rechten",
|
||||||
"staffAccessSubtitle": "Staffrangen en live toegang. Archief is alleen vergelijken/exporteren.",
|
"staffAccessSubtitle": "Staffrangen en live toegang. Archief is alleen vergelijken/exporteren.",
|
||||||
"analytics": "Analytics",
|
"analytics": "Analytics",
|
||||||
@@ -2851,8 +2851,8 @@
|
|||||||
"devopsSubtitle": "Servicegezondheid en emulatorfouten"
|
"devopsSubtitle": "Servicegezondheid en emulatorfouten"
|
||||||
},
|
},
|
||||||
"tickets": {
|
"tickets": {
|
||||||
"title": "Tickets",
|
"title": "CMS support desk",
|
||||||
"subtitle": "Ondersteuningstickets van spelers",
|
"subtitle": "Interne CMS-tickets (assignee, prioriteit, staff-workflow). Niet hetzelfde als helpcenter-tickets van /help.",
|
||||||
"noTickets": "Geen actieve tickets",
|
"noTickets": "Geen actieve tickets",
|
||||||
"searchPlaceholder": "Zoek op onderwerp, gebruiker, categorie of ID…",
|
"searchPlaceholder": "Zoek op onderwerp, gebruiker, categorie of ID…",
|
||||||
"presetActive": "Actief",
|
"presetActive": "Actief",
|
||||||
@@ -2865,11 +2865,13 @@
|
|||||||
"colPriority": "Prioriteit",
|
"colPriority": "Prioriteit",
|
||||||
"colAssigned": "Toegewezen",
|
"colAssigned": "Toegewezen",
|
||||||
"colDate": "Datum",
|
"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": {
|
"helpTickets": {
|
||||||
"title": "Helpcentrum-tickets",
|
"title": "Helpcenter-tickets",
|
||||||
"subtitle": "Spelertickets van /help/tickets",
|
"subtitle": "Tickets die spelers openen op /help/tickets. Apart van de CMS support desk.",
|
||||||
"noTickets": "Geen open helpcentrum-tickets",
|
"noTickets": "Geen open helpcentrum-tickets",
|
||||||
"searchPlaceholder": "Zoek op titel, gebruiker of ID…",
|
"searchPlaceholder": "Zoek op titel, gebruiker of ID…",
|
||||||
"presetOpen": "Open",
|
"presetOpen": "Open",
|
||||||
@@ -2900,7 +2902,9 @@
|
|||||||
"replied": "Antwoord verzonden.",
|
"replied": "Antwoord verzonden.",
|
||||||
"closed": "Ticket gesloten.",
|
"closed": "Ticket gesloten.",
|
||||||
"reopened": "Ticket heropend."
|
"reopened": "Ticket heropend."
|
||||||
}
|
},
|
||||||
|
"queueHint": "Deze wachtrij is het publieke helpcenter. CMS support desk tickets staan onder Support desk.",
|
||||||
|
"queueOtherCta": "Open CMS support desk"
|
||||||
},
|
},
|
||||||
"cfh": {
|
"cfh": {
|
||||||
"title": "Call for Help",
|
"title": "Call for Help",
|
||||||
|
|||||||
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
+80
-5
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user