148 lines
5.3 KiB
TypeScript
148 lines
5.3 KiB
TypeScript
import { AlertTriangle, Database, HeartPulse, Server } from "lucide-react";
|
|
import { redirect } from "next/navigation";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
|
import { prisma } from "@/lib/prisma";
|
|
import { HealthCheckClient } from "./health-check-client";
|
|
|
|
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" } });
|
|
|
|
return {
|
|
dbStatus,
|
|
dbLatency,
|
|
recentErrors,
|
|
errorsToday,
|
|
emuVersion: emuVersion?.value ?? "Unknown",
|
|
onlineUsers,
|
|
};
|
|
}
|
|
|
|
export default async function DevOpsPage() {
|
|
const { session, permissions } = await getAdminContext();
|
|
if (!canAccess(permissions, PERMS.DEVOPS_VIEW, session.user.rank)) {
|
|
redirect("/admin");
|
|
}
|
|
|
|
const data = await getDevOpsData();
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div>
|
|
<h1 className="text-3xl font-bold tracking-tight">Health Status</h1>
|
|
<p className="text-muted-foreground">System health, emulator status and error monitoring</p>
|
|
</div>
|
|
|
|
{/* Status cards */}
|
|
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Database</CardTitle>
|
|
<Database className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Badge variant={data.dbStatus === "healthy" ? "default" : "destructive"}>
|
|
{data.dbStatus === "healthy" ? "Healthy" : "Error"}
|
|
</Badge>
|
|
<p className="text-xs text-muted-foreground mt-1">Latency: {data.dbLatency}ms</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Emulator</CardTitle>
|
|
<Server className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<HealthCheckClient />
|
|
<p className="text-xs text-muted-foreground mt-1">Version: {data.emuVersion}</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Online Users</CardTitle>
|
|
<HeartPulse className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{data.onlineUsers}</div>
|
|
<p className="text-xs text-muted-foreground">Current active connections</p>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
|
<CardTitle className="text-sm font-medium">Errors (24h)</CardTitle>
|
|
<AlertTriangle className="h-4 w-4 text-muted-foreground" />
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="text-2xl font-bold">{data.errorsToday}</div>
|
|
<p className="text-xs text-muted-foreground">Emulator errors today</p>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Recent errors */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<AlertTriangle className="h-4 w-4" /> Recent Emulator Errors
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
{data.recentErrors.length === 0 ? (
|
|
<p className="text-sm text-muted-foreground">No recent errors</p>
|
|
) : (
|
|
<div className="space-y-2 max-h-[500px] overflow-y-auto">
|
|
{data.recentErrors.map((error) => (
|
|
<div key={error.id} className="rounded-md border p-3 text-sm">
|
|
<div className="flex items-center justify-between mb-1">
|
|
<span className="font-mono text-xs text-muted-foreground">#{error.id}</span>
|
|
<span className="text-xs text-muted-foreground">
|
|
{new Date(error.timestamp * 1000).toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<pre className="text-xs whitespace-pre-wrap break-all font-mono text-destructive">
|
|
{Buffer.from(error.stacktrace).toString("utf-8")}
|
|
</pre>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|