leaderBoard realtime per player and global

This commit is contained in:
2025-08-27 20:18:36 -06:00
parent bbfbd047c6
commit 0f6083453d
2 changed files with 336 additions and 27 deletions

View File

@@ -5,7 +5,9 @@ import { NameManager } from "./utils/nameManager";
import { getAllowedUuidCount, listAllowedUuids } from "./utils/uuidRegistry";
// SSE connections storage
const sseClients = new Set<Response>();
const sseClients = new Set<Response>(); // dashboard/rooms stream
const sseUuidClients = new Set<Response>(); // uuids stream
const ssePlayerActionsClients = new Set<Response>(); // per-player actions stream
const adminRouter = Router();
@@ -686,9 +688,164 @@ async function sendDashboardUpdate(client?: Response) {
}
}
// SSE endpoint for real-time UUIDs (allowlist + known names)
adminRouter.get("/uuids-stream", (req: Request, res: Response) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'X-Accel-Buffering': 'no'
});
try { (res as any).flushHeaders?.(); } catch {}
sseUuidClients.add(res);
console.log(`[AdminAPI] UUID SSE client connected. Total: ${sseUuidClients.size}`);
// Initial push
sendUuidsUpdate(res);
req.on('close', () => {
sseUuidClients.delete(res);
console.log(`[AdminAPI] UUID SSE client disconnected. Total: ${sseUuidClients.size}`);
});
const heartbeat = setInterval(() => {
if ((res as any).destroyed) {
clearInterval(heartbeat);
sseUuidClients.delete(res);
return;
}
res.write(':heartbeat\n\n');
}, 30000);
req.on('close', () => clearInterval(heartbeat));
});
async function sendUuidsUpdate(client?: Response) {
try {
const uuids = listAllowedUuids();
const nameManager = NameManager.getInstance();
const payload = {
count: (uuids || []).length,
uuids: (uuids || []).map(uuid => ({
uuid,
name: nameManager.getPlayerName(uuid) || null,
hasName: !!nameManager.getPlayerName(uuid)
}))
};
const message = `data: ${JSON.stringify(payload)}\n\n`;
if (client) {
if (!(client as any).destroyed) client.write(message);
} else {
const dead: Response[] = [];
sseUuidClients.forEach(c => {
if ((c as any).destroyed) dead.push(c);
else {
try { c.write(message); } catch { dead.push(c); }
}
});
dead.forEach(c => sseUuidClients.delete(c));
}
} catch (error) {
console.error('[AdminAPI] Error sending UUIDs SSE update:', error);
}
}
// Function to broadcast dashboard updates (called from room events)
function broadcastDashboardUpdate() {
sendDashboardUpdate();
// Also push UUIDs updates for any name/assignment changes indirectly affected
sendUuidsUpdate();
sendPlayersActionsUpdate();
}
// SSE endpoint for per-player actions made (real-time)
adminRouter.get("/players-actions-stream", (req: Request, res: Response) => {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache, no-transform',
'Connection': 'keep-alive',
'Access-Control-Allow-Origin': '*',
'X-Accel-Buffering': 'no'
});
try { (res as any).flushHeaders?.(); } catch {}
ssePlayerActionsClients.add(res);
console.log(`[AdminAPI] Player actions SSE client connected. Total: ${ssePlayerActionsClients.size}`);
sendPlayersActionsUpdate(res);
req.on('close', () => {
ssePlayerActionsClients.delete(res);
console.log(`[AdminAPI] Player actions SSE client disconnected. Total: ${ssePlayerActionsClients.size}`);
});
const heartbeat = setInterval(() => {
if ((res as any).destroyed) {
clearInterval(heartbeat);
ssePlayerActionsClients.delete(res);
return;
}
res.write(':heartbeat\n\n');
}, 30000);
req.on('close', () => clearInterval(heartbeat));
});
function isActionMade(kind: string, role?: string) {
const k = (kind || '').toString();
if (!k) return false;
const prefix = k.slice(0,3).toLowerCase();
if (prefix === 'p1_' || prefix === 'p2_') {
const evRole = prefix === 'p1_' ? 'P1' : 'P2';
return ((role || '').toUpperCase() === evRole);
}
// System/agnostic events are ignored for this stream; only actions list below are counted
return false;
}
const ACTION_EVENTS = [
'p1_propose','p1_no_offer','p2_snatch','p2_accept','p2_force','p2_no_force','p2_reject','p1_shame','p1_no_shame','p1_report','p1_no_report'
];
async function sendPlayersActionsUpdate(client?: Response) {
try {
const nameManager = NameManager.getInstance();
const uuids = nameManager.getAllKnownUuids?.() || [];
const players = uuids.map((uuid: string) => {
const history = nameManager.getSystemHistory(uuid) || [];
const counts: any = Object.fromEntries(ACTION_EVENTS.map(k => [k, 0]));
for (const entry of history) {
const kind = (entry as any)?.kind || '';
if (!ACTION_EVENTS.includes(kind)) continue;
if (!isActionMade(kind, (entry as any)?.role)) continue;
counts[kind] = (counts[kind] || 0) + 1;
}
const total = ACTION_EVENTS.reduce((acc, k) => acc + (counts[k] || 0), 0);
return {
uuid,
name: nameManager.getPlayerName(uuid) || null,
counts,
total
};
}).filter((p: any) => p.total > 0 || p.name);
const payload = { players };
const message = `data: ${JSON.stringify(payload)}\n\n`;
if (client) {
if (!(client as any).destroyed) client.write(message);
} else {
const dead: Response[] = [];
ssePlayerActionsClients.forEach(c => {
if ((c as any).destroyed) dead.push(c);
else { try { c.write(message); } catch { dead.push(c); } }
});
dead.forEach(c => ssePlayerActionsClients.delete(c));
}
} catch (error) {
console.error('[AdminAPI] Error sending players actions SSE update:', error);
}
}
export { adminRouter, broadcastDashboardUpdate };