From 21b4e9bd0c0241119cd61b641e9fe9d2a39dd99c Mon Sep 17 00:00:00 2001 From: vibe-bot Date: Sat, 9 Aug 2025 23:20:13 +0200 Subject: [PATCH] Nightly: Partymode-Status global per SSE /api/events Broadcast; Panic/Stop/Start senden Status an alle Clients --- server/src/index.ts | 29 +++++++++++++++++++++++++++++ web/src/App.tsx | 15 ++++++++++++++- web/src/api.ts | 8 ++++++++ 3 files changed, 51 insertions(+), 1 deletion(-) diff --git a/server/src/index.ts b/server/src/index.ts index f792dd2..dc9d281 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -152,6 +152,14 @@ const guildAudioState = new Map(); // Partymode: serverseitige Steuerung (global pro Guild) const partyTimers = new Map(); const partyActive = new Set(); +// SSE-Klienten für Broadcasts (z.B. Partymode Status) +const sseClients = new Set(); +function sseBroadcast(payload: any) { + const data = `data: ${JSON.stringify(payload)}\n\n`; + for (const res of sseClients) { + try { res.write(data); } catch {} + } +} async function playFilePath(guildId: string, channelId: string, filePath: string, volume?: number, relativeKey?: string): Promise { const guild = client.guilds.cache.get(guildId); @@ -871,6 +879,7 @@ app.post('/api/stop', (req: Request, res: Response) => { if (t) clearTimeout(t); partyTimers.delete(guildId); partyActive.delete(guildId); + sseBroadcast({ type: 'party', guildId, active: false }); } catch {} return res.json({ ok: true }); } catch (e: any) { @@ -921,6 +930,8 @@ function schedulePartyPlayback(guildId: string, channelId: string) { // Start: sofort spielen und nächste planen partyActive.add(guildId); void loop(); + // Broadcast Status + sseBroadcast({ type: 'party', guildId, active: true, channelId }); } app.post('/api/party/start', async (req: Request, res: Response) => { @@ -946,6 +957,7 @@ app.post('/api/party/stop', (req: Request, res: Response) => { const t = partyTimers.get(id); if (t) clearTimeout(t); partyTimers.delete(id); partyActive.delete(id); + sseBroadcast({ type: 'party', guildId: id, active: false }); return res.json({ ok: true }); } catch (e: any) { console.error('party/stop error', e); @@ -953,6 +965,23 @@ app.post('/api/party/stop', (req: Request, res: Response) => { } }); +// Server-Sent Events Endpoint +app.get('/api/events', (req: Request, res: Response) => { + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders?.(); + + // Snapshot senden + try { res.write(`data: ${JSON.stringify({ type: 'snapshot', party: Array.from(partyActive) })}\n\n`); } catch {} + + sseClients.add(res); + req.on('close', () => { + sseClients.delete(res); + try { res.end(); } catch {} + }); +}); + // Static Frontend ausliefern (Vite build) const webDistPath = path.resolve(__dirname, '../../web/dist'); if (fs.existsSync(webDistPath)) { diff --git a/web/src/App.tsx b/web/src/App.tsx index e7d82dd..9ea23cc 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useMemo, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop } from './api'; +import { fetchChannels, fetchSounds, playSound, setVolumeLive, getVolume, adminStatus, adminLogin, adminLogout, adminDelete, adminRename, playUrl, fetchCategories, createCategory, assignCategories, clearBadges, updateCategory, deleteCategory, partyStart, partyStop, subscribeEvents } from './api'; import type { VoiceChannelInfo, Sound, Category } from './types'; import { getCookie, setCookie } from './cookies'; @@ -74,6 +74,19 @@ export default function App() { const h = await fetch('/api/health').then(r => r.json()).catch(() => null); if (h && typeof h.totalPlays === 'number') setTotalPlays(h.totalPlays); } catch {} + // SSE: Partymode Status global synchronisieren + const unsub = subscribeEvents((msg)=>{ + if (msg?.type === 'party') { + const [gid] = (selected||'').split(':'); + if (gid && msg.guildId === gid) { + setChaosMode(!!msg.active); + } + } else if (msg?.type === 'snapshot') { + const [gid] = (selected||'').split(':'); + if (gid) setChaosMode(msg.party?.includes(gid)); + } + }); + return () => { try { unsub(); } catch {} }; })(); }, []); diff --git a/web/src/api.ts b/web/src/api.ts index f43bc7a..2e487a6 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -106,6 +106,14 @@ export async function partyStop(guildId: string) { if (!res.ok) throw new Error('Partymode Stop fehlgeschlagen'); } +export function subscribeEvents(onMessage: (data: any)=>void) { + const ev = new EventSource(`${API_BASE}/events`); + ev.onmessage = (e) => { + try { const data = JSON.parse(e.data); onMessage(data); } catch {} + }; + return () => ev.close(); +} + export async function setVolumeLive(guildId: string, volume: number): Promise { const res = await fetch(`${API_BASE}/volume`, { method: 'POST',