feat(admin): analytics redis cache, shared ops health, ticket queue clarity
CI / check (push) Successful in 29s
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m25s
CI / deploy (push) Failing after 10s

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
SimoandCursor committed 2026-07-30 19:57:00 +02:00
1 parent 474de0717b
commit 58fae1f90f
38 files changed
+348 -245

No files matched your search

+4 -1
View File
@@ -5,6 +5,7 @@ import { StatsCard } from "@/components/admin/stats-card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { redisCache } from "@/lib/redis-cache";
type DayRow = {
label: string;
@@ -192,7 +193,9 @@ export default async function ActivityPage() {
const locale = await getLocale();
const t = await getTranslations("pages.admin.analytics.activity");
const tRoot = await getTranslations("pages.admin.analytics");
const data = await getActivityData(locale);
const data = await redisCache(`admin:analytics:activity:${locale}`, 120, () =>
getActivityData(locale),
);
return (
<div className="space-y-6">
+6 -1
View File
@@ -7,6 +7,7 @@ import { CurrencyIcon } from "@/components/shared/currency-icon";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { redisCache } from "@/lib/redis-cache";
type EconomyData = {
totalCredits: number;
@@ -172,7 +173,11 @@ export default async function EconomyPage() {
const locale = await getLocale();
const t = await getTranslations("pages.admin.analytics.economy");
const tRoot = await getTranslations("pages.admin.analytics");
const { data, degraded } = await getEconomyData(locale);
const { data, degraded } = await redisCache(
`admin:analytics:economy:${locale}`,
120,
() => getEconomyData(locale),
);
return (
<div className="space-y-6">
+6 -1
View File
@@ -14,6 +14,7 @@ import { StatsCard } from "@/components/admin/stats-card";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { redisCache } from "@/lib/redis-cache";
type AnalyticsData = {
totalUsers: number;
@@ -202,7 +203,11 @@ export default async function AnalyticsPage() {
const locale = await getLocale();
const t = await getTranslations("pages.admin.analytics");
const { data, degraded } = await getAnalyticsData(locale);
const { data, degraded } = await redisCache(
`admin:analytics:overview:${locale}`,
120,
() => getAnalyticsData(locale),
);
return (
<div className="space-y-6">
+19 -27
View File
@@ -7,11 +7,11 @@ import {
OnlineUsersWidget,
StatusCard,
} from "@/components/admin/dashboard";
import { fetchOpsHealth } from "@/lib/admin/ops-health";
import { fetchOpsOnlineUsers } from "@/lib/admin/ops-online-users";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { rcon } from "@/lib/services/rcon";
import { siteSettings } from "@/lib/services/site-settings";
import { CommandocentrumControls } from "./commandocentrum-controls";
@@ -46,33 +46,25 @@ export default async function CommandoCentrum() {
const t = await getTranslations("pages.admin.commandocentrum");
const [
totalUsers,
errors,
dbOk,
emulatorOnline,
maintenance,
activity,
onlineRoster,
] = await Promise.all([
prisma.user.count().catch(() => -1),
prisma.emulatorErrors
.findMany({ orderBy: { timestamp: "desc" }, take: 50 })
.catch(() => []),
prisma.websiteSetting
.count()
.then(() => true)
.catch(() => false),
rcon.send("ping", null).catch(() => false),
siteSettings.getBool("maintenance_enabled", false),
prisma.staffActivities
.findMany({ orderBy: { createdAt: "desc" }, take: 12 })
.catch(
() => [] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
),
fetchOpsOnlineUsers(40),
]);
const [totalUsers, errors, health, maintenance, activity, onlineRoster] =
await Promise.all([
prisma.user.count().catch(() => -1),
prisma.emulatorErrors
.findMany({ orderBy: { timestamp: "desc" }, take: 50 })
.catch(() => []),
fetchOpsHealth(),
siteSettings.getBool("maintenance_enabled", false),
prisma.staffActivities
.findMany({ orderBy: { createdAt: "desc" }, take: 12 })
.catch(
() =>
[] as Awaited<ReturnType<typeof prisma.staffActivities.findMany>>,
),
fetchOpsOnlineUsers(40),
]);
const dbOk = health.dbOk;
const emulatorOnline = health.emulatorOk;
const onlineCount = onlineRoster.count;
const onlineUsers = onlineRoster.users;
+17 -31
View File
@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { Badge } from "@/components/ui/badge";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { fetchOpsHealth } from "@/lib/admin/ops-health";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
import { HealthCheckClient } from "./health-check-client";
@@ -12,42 +13,27 @@ async function getDevOpsData() {
const now = Math.floor(Date.now() / 1000);
const dayAgo = now - 86400;
// DB health — simple count query
let dbStatus: "healthy" | "error" = "healthy";
let dbLatency = 0;
try {
const start = Date.now();
await prisma.user.count({ take: 1 });
dbLatency = Date.now() - start;
} catch {
dbStatus = "error";
}
// Recent emulator errors
const recentErrors = await prisma.emulatorErrors.findMany({
orderBy: { id: "desc" },
take: 20,
});
const errorsToday = await prisma.emulatorErrors.count({
where: { timestamp: { gte: dayAgo } },
});
// Emulator settings (version info)
const emuVersion = await prisma.emulatorSettings.findFirst({
where: { key: "version" },
});
// Online user count as proxy for emulator health
const onlineUsers = await prisma.user.count({ where: { online: "1" } });
const [health, recentErrors, errorsToday, emuVersion] = await Promise.all([
fetchOpsHealth(),
prisma.emulatorErrors.findMany({
orderBy: { id: "desc" },
take: 20,
}),
prisma.emulatorErrors.count({
where: { timestamp: { gte: dayAgo } },
}),
prisma.emulatorSettings.findFirst({
where: { key: "version" },
}),
]);
return {
dbStatus,
dbLatency,
dbStatus: health.dbOk ? ("healthy" as const) : ("error" as const),
dbLatency: health.dbLatencyMs,
recentErrors,
errorsToday,
emuVersion: emuVersion?.value ?? "Unknown",
onlineUsers,
onlineUsers: health.onlineUsers,
};
}
+29 -19
View File
@@ -1,4 +1,6 @@
import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { positiveBigInt } from "@/lib/api";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
@@ -17,6 +19,7 @@ export default async function AdminHelpTicketDetailPage({
redirect("/admin");
}
const t = await getTranslations("pages.admin.helpTickets");
const { id } = await params;
const ticketId = positiveBigInt(id);
if (!ticketId) notFound();
@@ -103,24 +106,31 @@ export default async function AdminHelpTicketDetailPage({
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
return (
<AdminHelpTicketDetail
ticket={{
id: String(ticket.id),
title: ticket.title,
open: ticket.open,
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
creator: creator
? {
id: creator.id,
username: creator.username,
rank: creator.rank,
mail: creator.mail ?? "",
}
: null,
}}
messages={messages}
canEdit={canEdit}
/>
<div className="space-y-6">
<TicketQueueBanner
hint={t("queueHint")}
otherHref="/admin/tickets"
otherLabel={t("queueOtherCta")}
/>
<AdminHelpTicketDetail
ticket={{
id: String(ticket.id),
title: ticket.title,
open: ticket.open,
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
creator: creator
? {
id: creator.id,
username: creator.username,
rank: creator.rank,
mail: creator.mail ?? "",
}
: null,
}}
messages={messages}
canEdit={canEdit}
/>
</div>
);
}
+7 -1
View File
@@ -3,6 +3,7 @@ import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import type { Prisma } from "@/generated/prisma/client";
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
@@ -82,7 +83,7 @@ export default async function AdminHelpTicketsPage({
[orderField]: parsed.order,
} as Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput;
const [total, openCount, closedCount] = await Promise.all([
const [total, openCount, closedCount, queues] = await Promise.all([
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
prisma.websiteHelpCenterTickets
.count({ where: { open: true } })
@@ -90,6 +91,7 @@ export default async function AdminHelpTicketsPage({
prisma.websiteHelpCenterTickets
.count({ where: { open: false } })
.catch(() => 0),
fetchTicketQueueOpenCounts(),
]);
const pagination = calcPagination(total, parsed.page, parsed.perPage);
@@ -165,6 +167,10 @@ export default async function AdminHelpTicketsPage({
hint={t("queueHint")}
otherHref="/admin/tickets"
otherLabel={t("queueOtherCta")}
countsLine={t("queueCounts", {
here: queues.helpOpen,
other: queues.cmsOpen,
})}
/>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
+48 -38
View File
@@ -1,4 +1,6 @@
import { notFound, redirect } from "next/navigation";
import { getTranslations } from "next-intl/server";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import { getMinStaffRank } from "@/lib/admin/min-staff-rank";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
@@ -14,6 +16,7 @@ export default async function AdminTicketDetailPage({
redirect("/admin");
}
const t = await getTranslations("pages.admin.tickets");
const { id } = await params;
const ticketId = Number(id);
if (Number.isNaN(ticketId)) notFound();
@@ -57,43 +60,50 @@ export default async function AdminTicketDetailPage({
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
return (
<AdminTicketDetail
ticket={{
id: ticket.id,
subject: ticket.subject,
category: ticket.category,
priority: ticket.priority,
status: ticket.status,
createdAt: ticket.createdAt.toISOString(),
closedAt: ticket.closedAt?.toISOString() ?? null,
creator: {
id: ticket.creator.id,
username: ticket.creator.username,
look: ticket.creator.look,
rank: ticket.creator.rank,
mail: ticket.creator.mail ?? "",
ipRegister: ticket.creator.ipRegister,
accountCreated: ticket.creator.accountCreated,
},
assignee: ticket.assignee
? { id: ticket.assignee.id, username: ticket.assignee.username }
: null,
}}
messages={ticket.messages.map((m) => ({
id: m.id,
message: m.message,
isStaff: m.isStaff === 1,
createdAt: m.createdAt.toISOString(),
user: {
id: m.user.id,
username: m.user.username,
look: m.user.look,
rank: m.user.rank,
},
}))}
staffMembers={staffMembers}
currentUserId={session.user.id}
canEdit={canEdit}
/>
<div className="space-y-6">
<TicketQueueBanner
hint={t("queueHint")}
otherHref="/admin/help-tickets"
otherLabel={t("queueOtherCta")}
/>
<AdminTicketDetail
ticket={{
id: ticket.id,
subject: ticket.subject,
category: ticket.category,
priority: ticket.priority,
status: ticket.status,
createdAt: ticket.createdAt.toISOString(),
closedAt: ticket.closedAt?.toISOString() ?? null,
creator: {
id: ticket.creator.id,
username: ticket.creator.username,
look: ticket.creator.look,
rank: ticket.creator.rank,
mail: ticket.creator.mail ?? "",
ipRegister: ticket.creator.ipRegister,
accountCreated: ticket.creator.accountCreated,
},
assignee: ticket.assignee
? { id: ticket.assignee.id, username: ticket.assignee.username }
: null,
}}
messages={ticket.messages.map((m) => ({
id: m.id,
message: m.message,
isStaff: m.isStaff === 1,
createdAt: m.createdAt.toISOString(),
user: {
id: m.user.id,
username: m.user.username,
look: m.user.look,
rank: m.user.rank,
},
}))}
staffMembers={staffMembers}
currentUserId={session.user.id}
canEdit={canEdit}
/>
</div>
);
}
+7 -1
View File
@@ -3,6 +3,7 @@ import { getTranslations } from "next-intl/server";
import { StatusCard } from "@/components/admin/dashboard";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import type { Prisma } from "@/generated/prisma/client";
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { formatDate } from "@/lib/format-date";
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
@@ -71,7 +72,7 @@ export default async function AdminTicketsPage({
[orderField]: parsed.order,
} as Prisma.WebsiteTicketOrderByWithRelationInput;
const [total, openCount, inProgressCount, waitingCount, closedCount] =
const [total, openCount, inProgressCount, waitingCount, closedCount, queues] =
await Promise.all([
prisma.websiteTicket.count({ where }).catch(() => 0),
prisma.websiteTicket.count({ where: { status: "open" } }).catch(() => 0),
@@ -84,6 +85,7 @@ export default async function AdminTicketsPage({
prisma.websiteTicket
.count({ where: { status: "closed" } })
.catch(() => 0),
fetchTicketQueueOpenCounts(),
]);
const pagination = calcPagination(total, parsed.page, parsed.perPage);
@@ -121,6 +123,10 @@ export default async function AdminTicketsPage({
hint={t("queueHint")}
otherHref="/admin/help-tickets"
otherLabel={t("queueOtherCta")}
countsLine={t("queueCounts", {
here: queues.cmsOpen,
other: queues.helpOpen,
})}
/>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
+6 -49
View File
@@ -1,59 +1,16 @@
import net from "node:net";
import { NextResponse } from "next/server";
import { env } from "@/env";
import { fetchOpsHealth } from "@/lib/admin/ops-health";
import { withAdmin } from "@/lib/api-handler";
import { PERMS } from "@/lib/permissions";
import { prisma } from "@/lib/prisma";
function checkRcon(): Promise<boolean> {
return new Promise((resolve) => {
const host = env.RCON_HOST;
const port = env.RCON_PORT;
const socket = new net.Socket();
const timeout = setTimeout(() => {
socket.destroy();
resolve(false);
}, 3000);
socket.connect(port, host, () => {
clearTimeout(timeout);
socket.destroy();
resolve(true);
});
socket.on("error", () => {
clearTimeout(timeout);
socket.destroy();
resolve(false);
});
});
}
export const GET = withAdmin({ permission: PERMS.DEVOPS_VIEW }, async () => {
// DB health
let dbOk = false;
let dbLatency = 0;
try {
const start = Date.now();
await prisma.$queryRaw`SELECT 1`;
dbLatency = Date.now() - start;
dbOk = true;
} catch {
dbOk = false;
}
// Emulator health (RCON TCP check)
const emulator = await checkRcon();
// Online users
const onlineUsers = await prisma.user.count({ where: { online: "1" } });
const health = await fetchOpsHealth();
return NextResponse.json({
database: dbOk,
dbLatency,
emulator,
onlineUsers,
database: health.dbOk,
dbLatency: health.dbLatencyMs,
emulator: health.emulatorOk,
onlineUsers: health.onlineUsers,
timestamp: new Date().toISOString(),
});
});
+7 -1
View File
@@ -7,6 +7,7 @@ import { StatusCard } from "@/components/admin/dashboard";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import type { Prisma } from "@/generated/prisma/client";
import { requireModPermission } from "@/lib/admin/guard";
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { formatDate } from "@/lib/format-date";
import { PERMS } from "@/lib/permissions";
@@ -82,7 +83,7 @@ export default async function ModHelpTicketsPage({
[orderField]: parsed.order,
} as Prisma.WebsiteHelpCenterTicketsOrderByWithRelationInput;
const [total, openCount, closedCount] = await Promise.all([
const [total, openCount, closedCount, queues] = await Promise.all([
prisma.websiteHelpCenterTickets.count({ where }).catch(() => 0),
prisma.websiteHelpCenterTickets
.count({ where: { open: true } })
@@ -90,6 +91,7 @@ export default async function ModHelpTicketsPage({
prisma.websiteHelpCenterTickets
.count({ where: { open: false } })
.catch(() => 0),
fetchTicketQueueOpenCounts(),
]);
const pagination = calcPagination(total, parsed.page, parsed.perPage);
@@ -165,6 +167,10 @@ export default async function ModHelpTicketsPage({
hint={t("queueHint")}
otherHref="/mod/tickets"
otherLabel={t("queueOtherCta")}
countsLine={t("queueCounts", {
here: queues.helpOpen,
other: queues.cmsOpen,
})}
/>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />
+7 -1
View File
@@ -7,6 +7,7 @@ import { StatusCard } from "@/components/admin/dashboard";
import { TicketQueueBanner } from "@/components/admin/ticket-queue-banner";
import type { Prisma } from "@/generated/prisma/client";
import { requireModPermission } from "@/lib/admin/guard";
import { fetchTicketQueueOpenCounts } from "@/lib/admin/ticket-queue-counts";
import { calcPagination, parseListParams } from "@/lib/admin-helpers";
import { formatDate } from "@/lib/format-date";
import { PERMS } from "@/lib/permissions";
@@ -71,7 +72,7 @@ export default async function ModTicketsPage({
[orderField]: parsed.order,
} as Prisma.WebsiteTicketOrderByWithRelationInput;
const [total, openCount, inProgressCount, waitingCount, closedCount] =
const [total, openCount, inProgressCount, waitingCount, closedCount, queues] =
await Promise.all([
prisma.websiteTicket.count({ where }).catch(() => 0),
prisma.websiteTicket.count({ where: { status: "open" } }).catch(() => 0),
@@ -84,6 +85,7 @@ export default async function ModTicketsPage({
prisma.websiteTicket
.count({ where: { status: "closed" } })
.catch(() => 0),
fetchTicketQueueOpenCounts(),
]);
const pagination = calcPagination(total, parsed.page, parsed.perPage);
@@ -121,6 +123,10 @@ export default async function ModTicketsPage({
hint={t("queueHint")}
otherHref="/mod/help-tickets"
otherLabel={t("queueOtherCta")}
countsLine={t("queueCounts", {
here: queues.cmsOpen,
other: queues.helpOpen,
})}
/>
<div className="grid grid-cols-[repeat(auto-fit,minmax(180px,1fr))] gap-3.5">
<StatusCard label={t("statusOpen")} value={openCount} icon="🎫" />