Admin: Reiter Matches und Einstellungen, Discord-Embeds, CSTV-Zugang
Der Admin-Bereich bekommt Reiter. Die Discord-Einstellungen liegen unter „Einstellungen“. Meldungen sind als Embeds formatiert, und die Match-Startmeldung enthält den CSTV-Zugang (Passwort und Adresse). Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0197cd05ce
commit
e9efe38b76
6 files changed
+79
-21
No files matched your search
@@ -81,7 +81,8 @@ export function createAdminRouter({ service, manager, config }) {
|
||||
router.put('/notifications', requireAdmin, wrap((req) => service.setNotificationEvents(req.body?.events)));
|
||||
router.post('/notifications/test', requireAdmin, wrap(async () => {
|
||||
if (!service.notifier.enabled) throw new HttpError(409, 'Kein Discord-Webhook eingerichtet (DISCORD_WEBHOOK_URL)', 'not_configured');
|
||||
if (!(await service.notifier.send('Testmeldung aus CS2 Mapveto'))) {
|
||||
const test = { embeds: [{ title: '🧪 Testmeldung', description: 'Die Verbindung zu CS2 Mapveto funktioniert.', color: 0x3b82f6 }] };
|
||||
if (!(await service.notifier.send(test))) {
|
||||
throw new HttpError(502, 'Discord hat die Meldung nicht angenommen (siehe Log)', 'send_failed');
|
||||
}
|
||||
return { ok: true };
|
||||
|
||||
@@ -27,12 +27,14 @@ export function createDiscordNotifier(url, { fetchImpl = fetch, logger = console
|
||||
return {
|
||||
enabled: true,
|
||||
/** Liefert true, wenn Discord die Nachricht angenommen hat. */
|
||||
async send(content) {
|
||||
async send(message) {
|
||||
// Text oder vollständige Discord-Payload (z. B. mit embeds)
|
||||
const payload = typeof message === 'string' ? { content: message.slice(0, MAX_LENGTH) } : message;
|
||||
try {
|
||||
const res = await fetchImpl(url, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ content: content.slice(0, MAX_LENGTH) }),
|
||||
body: JSON.stringify(payload),
|
||||
signal: AbortSignal.timeout(TIMEOUT_MS),
|
||||
});
|
||||
if (res.ok) return true;
|
||||
|
||||
+40
-11
@@ -341,7 +341,12 @@ export class MatchService extends EventEmitter {
|
||||
.run(firstTeam, this.now(), this.now(), match.id);
|
||||
const how = match.veto_start === 'coin' ? 'per Münzwurf ausgelost' : 'vom Organisator festgelegt';
|
||||
this.log(match.id, 'veto', `Veto gestartet – ${this.teamName(match, firstTeam)} beginnt (${how})`);
|
||||
this.notify('veto_start', match.id, `Veto gestartet: ${this.matchTitle(match.id)}, ${this.teamName(match, firstTeam)} beginnt`);
|
||||
this.notify('veto_start', match.id, {
|
||||
title: '🎮 Veto gestartet',
|
||||
description: `**${this.matchTitle(match.id)}**`,
|
||||
color: 0x3b82f6,
|
||||
fields: [{ name: 'Beginnt', value: this.teamName(match, firstTeam), inline: true }],
|
||||
});
|
||||
})();
|
||||
this.changed(viewer.matchId);
|
||||
}
|
||||
@@ -519,8 +524,16 @@ export class MatchService extends EventEmitter {
|
||||
const winner = state.winner ? this.teamName(match, state.winner) : 'Unentschieden';
|
||||
this.log(matchId, 'score',
|
||||
`Map ${row.number} (${this.mapName(match, row.map)}) beendet: ${match.team_a_name} ${state.score.A}:${state.score.B} ${match.team_b_name} – Sieger: ${winner}`);
|
||||
this.notify('map_end', matchId, `${this.matchTitle(matchId)}
|
||||
Map ${row.number} (${this.mapName(match, row.map)}) beendet: ${state.score.A}:${state.score.B}, Sieger ${winner}`);
|
||||
this.notify('map_end', matchId, {
|
||||
title: '🗺️ Map beendet',
|
||||
description: `**${this.matchTitle(matchId)}**`,
|
||||
color: 0xf59e0b,
|
||||
fields: [
|
||||
{ name: `Map ${row.number}`, value: this.mapName(match, row.map), inline: true },
|
||||
{ name: 'Stand', value: `${state.score.A} : ${state.score.B}`, inline: true },
|
||||
{ name: 'Sieger', value: winner, inline: true },
|
||||
],
|
||||
});
|
||||
}
|
||||
})();
|
||||
if (ended) {
|
||||
@@ -529,8 +542,18 @@ Map ${row.number} (${this.mapName(match, row.map)}) beendet: ${state.score.A}:${
|
||||
const needed = Math.floor(BEST_OF[ended.match.format] / 2) + 1;
|
||||
const decided = score.A >= needed ? 'A' : score.B >= needed ? 'B' : null;
|
||||
if (decided) this.log(matchId, 'score', `Serie entschieden: ${this.teamName(ended.match, decided)} gewinnt ${score[decided]}:${score[decided === 'A' ? 'B' : 'A']}`);
|
||||
if (decided) this.notify('series_end', matchId, `${this.matchTitle(matchId)}
|
||||
Serie entschieden: ${this.teamName(ended.match, decided)} gewinnt ${score[decided]}:${score[decided === 'A' ? 'B' : 'A']}`);
|
||||
if (decided) {
|
||||
const loser = decided === 'A' ? 'B' : 'A';
|
||||
this.notify('series_end', matchId, {
|
||||
title: '🏆 Serie entschieden',
|
||||
description: `**${this.matchTitle(matchId)}**`,
|
||||
color: 0x22c55e,
|
||||
fields: [
|
||||
{ name: 'Sieger', value: this.teamName(ended.match, decided), inline: true },
|
||||
{ name: 'Serienstand', value: `${score[decided]} : ${score[loser]}`, inline: true },
|
||||
],
|
||||
});
|
||||
}
|
||||
const hasNext = maps.some((m) => m.number === ended.row.number + 1);
|
||||
this.changed(matchId);
|
||||
this.emit('mapEnded', { matchId, number: ended.row.number, decided, hasNext });
|
||||
@@ -562,8 +585,12 @@ Serie entschieden: ${this.teamName(ended.match, decided)} gewinnt ${score[decide
|
||||
this.db.prepare("UPDATE series_maps SET status = 'done' WHERE match_id = ? AND status IN ('live','next')").run(match.id);
|
||||
}
|
||||
this.log(match.id, 'status', message ?? (status === 'finished' ? 'Match abgeschlossen' : 'Match abgebrochen'));
|
||||
this.notify('match_closed', match.id, `${this.matchTitle(match.id)}
|
||||
${message ?? (status === 'finished' ? 'Match abgeschlossen' : 'Match abgebrochen')}`);
|
||||
const finished = status === 'finished';
|
||||
this.notify('match_closed', match.id, {
|
||||
title: finished ? '✅ Match abgeschlossen' : '⛔ Match abgebrochen',
|
||||
description: `**${this.matchTitle(match.id)}**${message ? `\n${message}` : ''}`,
|
||||
color: finished ? 0x6b7280 : 0x9ca3af,
|
||||
});
|
||||
this.setStatus(match.id, status);
|
||||
this.emit('closed', match.id);
|
||||
}
|
||||
@@ -610,11 +637,13 @@ ${message ?? (status === 'finished' ? 'Match abgeschlossen' : 'Match abgebrochen
|
||||
return this.notificationSettings();
|
||||
}
|
||||
|
||||
notify(type, matchId, text) {
|
||||
/** Meldung als Discord-Embed. Ohne Stream-Schlüssel ist der Titel ein Link auf die Match-Übersicht. */
|
||||
notify(type, matchId, embed) {
|
||||
if (!this.notifier.enabled || !this.notificationSettings().events[type]) return;
|
||||
const link = this.config.streamKey ? '' : `
|
||||
${this.config.publicUrl}/stream/match/${matchId}`;
|
||||
this.notifier.send(`${text}${link}`);
|
||||
const url = this.config.streamKey ? undefined : `${this.config.publicUrl}/stream/match/${matchId}`;
|
||||
this.notifier.send({
|
||||
embeds: [{ ...embed, url, timestamp: new Date(this.now()).toISOString(), footer: { text: 'CS2 Mapveto' } }],
|
||||
});
|
||||
}
|
||||
|
||||
log(matchId, type, message) {
|
||||
|
||||
@@ -265,7 +265,11 @@ export class ServerManager {
|
||||
} else if (status.state === 'failed') {
|
||||
Object.assign(fields, { status: current.status === 'starting' ? 'start_failed' : 'failed', message: status.message });
|
||||
this.service.log(matchId, 'server', `Server-Fehler: ${status.message}`);
|
||||
this.service.notify('server_error', matchId, `Server-Fehler bei ${this.service.matchTitle(matchId)}: ${status.message}`);
|
||||
this.service.notify('server_error', matchId, {
|
||||
title: '⚠️ Serverfehler',
|
||||
description: `**${this.service.matchTitle(matchId)}**\n${status.message}`,
|
||||
color: 0xdc2626,
|
||||
});
|
||||
} else if (status.state === 'stopped') {
|
||||
Object.assign(fields, { status: 'stopped', stopped_at: Date.now(), message: status.message ?? 'Server wurde extern beendet' });
|
||||
this.service.log(matchId, 'server', 'Server wurde außerhalb der Anwendung beendet');
|
||||
@@ -388,7 +392,17 @@ export class ServerManager {
|
||||
throw new HttpError(502, `Befehl konnte nicht gesendet werden: ${err.message}`, 'server_error');
|
||||
}
|
||||
this.service.log(viewer.matchId, 'server', this.adapter.simulated ? 'Aufwärmphase beendet (simuliert)' : 'Aufwärmphase beendet – Match startet');
|
||||
this.service.notify('match_start', viewer.matchId, `Match gestartet: ${this.service.matchTitle(viewer.matchId)}`);
|
||||
// CSTV-Zugang für Zuschauer, gleiche Form wie im Zuschauerbereich der Webseite
|
||||
const fields = [];
|
||||
if (this.config.match.tvEnabled && row.tv_port) {
|
||||
fields.push({ name: '📺 CSTV', value: `\`password ${row.tv_password}; connect ${row.host}:${row.tv_port}\`` });
|
||||
}
|
||||
this.service.notify('match_start', viewer.matchId, {
|
||||
title: '🔴 Match läuft',
|
||||
description: `**${this.service.matchTitle(viewer.matchId)}**`,
|
||||
color: 0xef4444,
|
||||
fields,
|
||||
});
|
||||
if (this.adapter.simulated) this.startDemoSimulation(viewer.matchId);
|
||||
this.service.changed(viewer.matchId);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user