feat: integrate real data into dashboard

- Created dashboard service with DB queries for users, economy, events
- Added client stats provider with 30s caching for Discord metrics
- Implemented /api/stats endpoint aggregating all dashboard data
- Created useDashboardStats React hook with auto-refresh
- Updated Dashboard.tsx to display real data with loading/error states
- Added comprehensive test coverage (11 tests passing)
- Replaced all mock values with live Discord and database metrics
This commit is contained in:
syntaxbullet
2026-01-08 18:50:44 +01:00
parent a207d511be
commit 17cb70ec00
10 changed files with 861 additions and 35 deletions

View File

@@ -0,0 +1,78 @@
import { useState, useEffect } from "react";
interface DashboardStats {
guilds: {
count: number;
};
users: {
active: number;
total: number;
};
commands: {
total: number;
};
ping: {
avg: number;
};
economy: {
totalWealth: string;
avgLevel: number;
topStreak: number;
};
recentEvents: Array<{
type: 'success' | 'error' | 'info';
message: string;
timestamp: string;
icon?: string;
}>;
uptime: number;
lastCommandTimestamp: number | null;
}
interface UseDashboardStatsResult {
stats: DashboardStats | null;
loading: boolean;
error: string | null;
}
/**
* Custom hook to fetch and auto-refresh dashboard statistics
* Polls the API every 30 seconds
*/
export function useDashboardStats(): UseDashboardStatsResult {
const [stats, setStats] = useState<DashboardStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchStats = async () => {
try {
const response = await fetch("/api/stats");
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setStats(data);
setError(null);
} catch (err) {
console.error("Failed to fetch dashboard stats:", err);
setError(err instanceof Error ? err.message : "Failed to fetch stats");
} finally {
setLoading(false);
}
};
useEffect(() => {
// Initial fetch
fetchStats();
// Set up polling every 30 seconds
const interval = setInterval(fetchStats, 30000);
// Cleanup on unmount
return () => clearInterval(interval);
}, []);
return { stats, loading, error };
}

View File

@@ -6,8 +6,49 @@ import {
CardTitle,
} from "@/components/ui/card";
import { Activity, Server, Users, Zap } from "lucide-react";
import { useDashboardStats } from "@/hooks/use-dashboard-stats";
export function Dashboard() {
const { stats, loading, error } = useDashboardStats();
if (loading && !stats) {
return (
<div className="space-y-6">
<div>
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<p className="text-muted-foreground">Loading dashboard data...</p>
</div>
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{[1, 2, 3, 4].map((i) => (
<Card key={i}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Loading...</CardTitle>
</CardHeader>
<CardContent>
<div className="h-8 w-20 bg-muted animate-pulse rounded" />
</CardContent>
</Card>
))}
</div>
</div>
);
}
if (error) {
return (
<div className="space-y-6">
<div>
<h2 className="text-3xl font-bold tracking-tight">Dashboard</h2>
<p className="text-destructive">Error loading dashboard: {error}</p>
</div>
</div>
);
}
if (!stats) {
return null;
}
return (
<div className="space-y-6">
<div>
@@ -23,8 +64,8 @@ export function Dashboard() {
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12</div>
<p className="text-xs text-muted-foreground">+2 from last month</p>
<div className="text-2xl font-bold">{stats.guilds.count}</div>
<p className="text-xs text-muted-foreground">Active guilds</p>
</CardContent>
</Card>
@@ -34,19 +75,21 @@ export function Dashboard() {
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">1,234</div>
<p className="text-xs text-muted-foreground">+10% from last month</p>
<div className="text-2xl font-bold">{stats.users.active.toLocaleString()}</div>
<p className="text-xs text-muted-foreground">
{stats.users.total.toLocaleString()} total registered
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Commands Run</CardTitle>
<CardTitle className="text-sm font-medium">Commands</CardTitle>
<Zap className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">12,345</div>
<p className="text-xs text-muted-foreground">+5% from last month</p>
<div className="text-2xl font-bold">{stats.commands.total}</div>
<p className="text-xs text-muted-foreground">Registered commands</p>
</CardContent>
</Card>
@@ -56,8 +99,8 @@ export function Dashboard() {
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24ms</div>
<p className="text-xs text-muted-foreground">+2ms from last hour</p>
<div className="text-2xl font-bold">{stats.ping.avg}ms</div>
<p className="text-xs text-muted-foreground">WebSocket latency</p>
</CardContent>
</Card>
</div>
@@ -65,11 +108,25 @@ export function Dashboard() {
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-7">
<Card className="col-span-4">
<CardHeader>
<CardTitle>Activity Overview</CardTitle>
<CardTitle>Economy Overview</CardTitle>
<CardDescription>Server economy statistics</CardDescription>
</CardHeader>
<CardContent>
<div className="h-[200px] w-full bg-muted/20 flex items-center justify-center border-2 border-dashed border-muted rounded-md text-muted-foreground">
Chart Placeholder
<div className="space-y-4">
<div>
<p className="text-sm font-medium">Total Wealth</p>
<p className="text-2xl font-bold">{BigInt(stats.economy.totalWealth).toLocaleString()} AU</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-sm font-medium">Average Level</p>
<p className="text-xl font-bold">{stats.economy.avgLevel}</p>
</div>
<div>
<p className="text-sm font-medium">Top Streak</p>
<p className="text-xl font-bold">{stats.economy.topStreak} days</p>
</div>
</div>
</div>
</CardContent>
</Card>
@@ -81,27 +138,30 @@ export function Dashboard() {
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-emerald-500 mr-2" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">New guild joined</p>
<p className="text-sm text-muted-foreground">2 minutes ago</p>
</div>
</div>
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-destructive mr-2" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Error in verify command</p>
<p className="text-sm text-muted-foreground">15 minutes ago</p>
</div>
</div>
<div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-blue-500 mr-2" />
<div className="space-y-1">
<p className="text-sm font-medium leading-none">Bot restarted</p>
<p className="text-sm text-muted-foreground">1 hour ago</p>
</div>
</div>
{stats.recentEvents.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent events</p>
) : (
stats.recentEvents.slice(0, 5).map((event, i) => (
<div key={i} className="flex items-center">
<div
className={`w-2 h-2 rounded-full mr-2 ${event.type === 'success'
? 'bg-emerald-500'
: event.type === 'error'
? 'bg-destructive'
: 'bg-blue-500'
}`}
/>
<div className="space-y-1 flex-1">
<p className="text-sm font-medium leading-none">
{event.icon} {event.message}
</p>
<p className="text-sm text-muted-foreground">
{new Date(event.timestamp).toLocaleString()}
</p>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>

View File

@@ -62,6 +62,58 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
return Response.json({ status: "ok", timestamp: Date.now() });
}
if (url.pathname === "/api/stats") {
try {
// Import services (dynamic to avoid circular deps)
const { dashboardService } = await import("@shared/modules/dashboard/dashboard.service");
const { getClientStats } = await import("../../bot/lib/clientStats");
// Fetch all data in parallel
const [clientStats, activeUsers, totalUsers, economyStats, recentEvents] = await Promise.all([
Promise.resolve(getClientStats()),
dashboardService.getActiveUserCount(),
dashboardService.getTotalUserCount(),
dashboardService.getEconomyStats(),
dashboardService.getRecentEvents(10),
]);
const stats = {
guilds: {
count: clientStats.guilds,
},
users: {
active: activeUsers,
total: totalUsers,
},
commands: {
total: clientStats.commandsRegistered,
},
ping: {
avg: clientStats.ping,
},
economy: {
totalWealth: economyStats.totalWealth.toString(),
avgLevel: economyStats.avgLevel,
topStreak: economyStats.topStreak,
},
recentEvents: recentEvents.map(event => ({
...event,
timestamp: event.timestamp.toISOString(),
})),
uptime: clientStats.uptime,
lastCommandTimestamp: clientStats.lastCommandTimestamp,
};
return Response.json(stats);
} catch (error) {
console.error("Error fetching dashboard stats:", error);
return Response.json(
{ error: "Failed to fetch dashboard statistics" },
{ status: 500 }
);
}
}
// Static File Serving
let pathName = url.pathname;
if (pathName === "/") pathName = "/index.html";