feat: implement real-time dashboard updates via WebSockets

This commit is contained in:
syntaxbullet
2026-01-08 21:01:33 +01:00
parent fff90804c0
commit 1251df286e
7 changed files with 267 additions and 73 deletions

View File

@@ -51,12 +51,22 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
}
}
// Interval for broadcasting stats to all connected WS clients
let statsBroadcastInterval: Timer | undefined;
const server = serve({
port,
hostname,
async fetch(req) {
async fetch(req, server) {
const url = new URL(req.url);
// Upgrade to WebSocket
if (url.pathname === "/ws") {
const success = server.upgrade(req);
if (success) return undefined;
return new Response("WebSocket upgrade failed", { status: 400 });
}
// API routes
if (url.pathname === "/api/health") {
return Response.json({ status: "ok", timestamp: Date.now() });
@@ -64,47 +74,7 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
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 = {
bot: clientStats.bot,
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,
};
const stats = await getFullDashboardStats();
return Response.json(stats);
} catch (error) {
console.error("Error fetching dashboard stats:", error);
@@ -145,9 +115,93 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
return new Response(Bun.file(join(distDir, "index.html")));
},
websocket: {
open(ws) {
ws.subscribe("dashboard");
console.log(`🔌 [WS] Client connected. Total: ${server.pendingWebSockets}`);
// Send initial stats
getFullDashboardStats().then(stats => {
ws.send(JSON.stringify({ type: "STATS_UPDATE", data: stats }));
});
// Start broadcast interval if this is the first client
if (!statsBroadcastInterval) {
statsBroadcastInterval = setInterval(async () => {
try {
const stats = await getFullDashboardStats();
server.publish("dashboard", JSON.stringify({ type: "STATS_UPDATE", data: stats }));
} catch (error) {
console.error("Error in stats broadcast:", error);
}
}, 5000);
}
},
message(ws, message) {
// Handle messages if needed (e.g. heartbeat)
try {
const data = JSON.parse(message.toString());
if (data.type === "PING") ws.send(JSON.stringify({ type: "PONG" }));
} catch (e) { }
},
close(ws) {
ws.unsubscribe("dashboard");
console.log(`🔌 [WS] Client disconnected.`);
// Stop broadcast interval if no clients left
if (server.pendingWebSockets === 0 && statsBroadcastInterval) {
clearInterval(statsBroadcastInterval);
statsBroadcastInterval = undefined;
}
}
},
development: isDev,
});
/**
* Helper to fetch full dashboard stats object
*/
async function getFullDashboardStats() {
// 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),
]);
return {
bot: clientStats.bot,
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,
};
}
// Listen for real-time events from the system bus
const { systemEvents, EVENTS } = await import("@shared/lib/events");
systemEvents.on(EVENTS.DASHBOARD.NEW_EVENT, (event) => {
server.publish("dashboard", JSON.stringify({ type: "NEW_EVENT", data: event }));
});
const url = `http://${hostname}:${port}`;
return {
@@ -157,6 +211,9 @@ export async function createWebServer(config: WebServerConfig = {}): Promise<Web
if (buildProcess) {
buildProcess.kill();
}
if (statsBroadcastInterval) {
clearInterval(statsBroadcastInterval);
}
server.stop(true);
},
};