145 lines
3.6 KiB
TypeScript
145 lines
3.6 KiB
TypeScript
import { createFileRoute, redirect } from "@tanstack/react-router";
|
|
import { useEffect, useState } from "react";
|
|
import { authClient } from "@/lib/auth-client";
|
|
import socket from "@/lib/socket.io";
|
|
|
|
export const Route = createFileRoute("/admin/logs")({
|
|
beforeLoad: async ({ location }) => {
|
|
const { data: session } = await authClient.getSession();
|
|
|
|
if (!session?.user) {
|
|
throw redirect({
|
|
to: "/",
|
|
search: {
|
|
redirect: location.href,
|
|
},
|
|
});
|
|
}
|
|
|
|
if (session.user.role !== "admin") {
|
|
throw redirect({
|
|
to: "/",
|
|
});
|
|
}
|
|
|
|
return { user: session.user };
|
|
},
|
|
component: RouteComponent,
|
|
});
|
|
|
|
interface LogEntry {
|
|
level: number;
|
|
createdAt: Date;
|
|
pid: number;
|
|
hostname: string;
|
|
module: string;
|
|
message?: string;
|
|
[key: string]: any; // catch any extra fields
|
|
}
|
|
|
|
function LevelBadge({ level }: { level: number }) {
|
|
const config: Record<number, { label: string; className: string }> = {
|
|
10: { label: "TRACE", className: "bg-gray-100 text-gray-600" },
|
|
20: { label: "DEBUG", className: "bg-blue-100 text-blue-700" },
|
|
30: { label: "INFO", className: "bg-green-100 text-green-700" },
|
|
40: { label: "WARN", className: "bg-yellow-100 text-yellow-700" },
|
|
50: { label: "ERROR", className: "bg-red-100 text-red-700" },
|
|
60: { label: "FATAL", className: "bg-purple-100 text-purple-700" },
|
|
};
|
|
|
|
const { label, className } = config[level] ?? {
|
|
label: String(level),
|
|
className: "bg-gray-100",
|
|
};
|
|
|
|
return (
|
|
<span className={`px-2 py-0.5 rounded text-xs font-medium ${className}`}>
|
|
{label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function RouteComponent() {
|
|
//const { user } = Route.useRouteContext();
|
|
//const router = useRouter();
|
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
|
|
|
useEffect(() => {
|
|
// Connect if not already connected
|
|
if (!socket.connected) {
|
|
socket.connect();
|
|
}
|
|
|
|
socket.on("connect", () => {
|
|
socket.emit("join-room", "logs");
|
|
});
|
|
|
|
socket.emit("join-room", "logs");
|
|
socket.on(
|
|
"room-update",
|
|
(data: { payloads: LogEntry[]; roomId: string }) => {
|
|
setLogs((prev) => [...data.payloads, ...prev]);
|
|
},
|
|
);
|
|
|
|
// socket.on("logs", (data) => {
|
|
// console.log(data);
|
|
// setLogs((prev) => [...data.payloads, ...prev]);
|
|
// });
|
|
|
|
// Cleanup listeners on unmount
|
|
return () => {
|
|
socket.emit("leave-room", "logs");
|
|
socket.off("room-update");
|
|
socket.off("logs");
|
|
};
|
|
}, []);
|
|
return (
|
|
<div>
|
|
{/* Log Table */}
|
|
<div className="rounded border overflow-auto max-h-[600px]">
|
|
<table className="w-full text-sm">
|
|
<thead className="bg-muted sticky top-0">
|
|
<tr>
|
|
<th className="text-left px-3 py-2">Time</th>
|
|
<th className="text-left px-3 py-2">Level</th>
|
|
<th className="text-left px-3 py-2">Module</th>
|
|
<th className="text-left px-3 py-2">Host</th>
|
|
<th className="text-left px-3 py-2">Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
{logs.length === 0 ? (
|
|
<tr>
|
|
<td
|
|
colSpan={6}
|
|
className="text-center py-6 text-muted-foreground"
|
|
>
|
|
No logs yet — join the room to start receiving
|
|
</td>
|
|
</tr>
|
|
) : (
|
|
logs.map((log, i) => (
|
|
<tr
|
|
key={`${log.id}-${i}`}
|
|
className="border-t hover:bg-muted/50"
|
|
>
|
|
<td className="px-3 py-2 whitespace-nowrap">
|
|
{new Date(log.createdAt).toLocaleTimeString()}
|
|
</td>
|
|
<td className="px-3 py-2">
|
|
<LevelBadge level={log.level} />
|
|
</td>
|
|
<td className="px-3 py-2">{log.module}</td>
|
|
<td className="px-3 py-2">{log.hostname}</td>
|
|
<td className="px-3 py-2 max-w-sm truncate">{log.message}</td>
|
|
</tr>
|
|
))
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|