forked from syntaxbullet/aurorabot
feat: add admin panel with Discord OAuth and dashboard
Adds a React admin panel (panel/) with Discord OAuth2 login, live dashboard via WebSocket, and settings/management pages. Includes Docker build support, Vite proxy config for dev, game_settings migration, and open-redirect protection on auth callback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
84
panel/src/pages/Dashboard.tsx
Normal file
84
panel/src/pages/Dashboard.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { get } from "../lib/api";
|
||||
|
||||
interface Stats {
|
||||
bot: { name: string; avatarUrl: string | null; status: string | null };
|
||||
guilds: { count: number };
|
||||
users: { total: number; active: number };
|
||||
economy: { totalWealth: string; avgLevel: number; topStreak: number; totalItems: number };
|
||||
commands: { total: number; active: number; disabled: number };
|
||||
ping: { avg: number };
|
||||
uptime: number;
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const [stats, setStats] = useState<Stats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
get<Stats>("/api/stats")
|
||||
.then(setStats)
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
// Connect WebSocket for live updates
|
||||
const protocol = location.protocol === "https:" ? "wss:" : "ws:";
|
||||
const ws = new WebSocket(`${protocol}//${location.host}/ws`);
|
||||
wsRef.current = ws;
|
||||
|
||||
ws.onmessage = (e) => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data);
|
||||
if (msg.type === "STATS_UPDATE") setStats(msg.data);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
return () => ws.close();
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center p-12">
|
||||
<span className="loading loading-spinner loading-lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!stats) return <div className="alert alert-error">Failed to load stats</div>;
|
||||
|
||||
const uptimeHours = Math.floor((stats.uptime ?? 0) / 3600);
|
||||
const uptimeMins = Math.floor(((stats.uptime ?? 0) % 3600) / 60);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-6">Dashboard</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
|
||||
<div className="stat bg-base-100 rounded-box shadow">
|
||||
<div className="stat-title">Uptime</div>
|
||||
<div className="stat-value text-lg">{uptimeHours}h {uptimeMins}m</div>
|
||||
<div className="stat-desc">Ping: {stats.ping?.avg ?? 0}ms</div>
|
||||
</div>
|
||||
<div className="stat bg-base-100 rounded-box shadow">
|
||||
<div className="stat-title">Guilds</div>
|
||||
<div className="stat-value text-lg">{stats.guilds?.count ?? 0}</div>
|
||||
</div>
|
||||
<div className="stat bg-base-100 rounded-box shadow">
|
||||
<div className="stat-title">Users</div>
|
||||
<div className="stat-value text-lg">{stats.users?.total ?? 0}</div>
|
||||
<div className="stat-desc">{stats.users?.active ?? 0} active</div>
|
||||
</div>
|
||||
<div className="stat bg-base-100 rounded-box shadow">
|
||||
<div className="stat-title">Economy</div>
|
||||
<div className="stat-value text-lg">{Number(stats.economy?.totalWealth ?? 0).toLocaleString()}g</div>
|
||||
<div className="stat-desc">{stats.economy?.totalItems ?? 0} items in circulation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-sm text-base-content/50">
|
||||
Live data via WebSocket — updates every 5 seconds
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user