Zwei Befunde aus dem Betriebslog
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s

1. /api/auditlog antwortete mit 500. `displayAvatarURL({ size: 48 })` — 48 ist
   keine Zweierpotenz, Discord nimmt nur 16 bis 4096. Ueberall sonst im Code
   stehen 64 oder 128, nur hier nicht. Der Wurf lief durch das map, also riss
   ein einziges Avatar die ganze Liste mit. Jetzt 64, und die Bildsuche faengt
   ihre Fehler selbst ab. Dazu eine Pruefung, die verbotene Groessen im
   Quelltext findet, bevor sie jemand im Log findet.

2. "Sprachverbindung kam nicht zustande" sagt jetzt, WO sie stehen blieb.
   Das entscheidet naemlich die Ursache: bleibt sie bei `signalling`, hat
   Discord nie einen Sprachserver genannt (Rechte, Gateway). Bleibt sie bei
   `connecting`, ist der Sprachserver bekannt und der UDP-Austausch mit ihm
   scheitert — praktisch immer gesperrtes ausgehendes UDP. Beides steht jetzt
   im Log und in der Antwort auf /radio.

   Dazu der dokumentierte Umgang mit Close-Code 4014: rausgeworfen oder
   verschoben heisst nicht wiederverbinden, sondern aufhoeren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 12:00:14 +02:00
co-authored by Claude Opus 5
parent 4d71122820
commit e246dadbec
2 changed files with 34 additions and 6 deletions
+25 -2
View File
@@ -172,7 +172,15 @@ export async function spielen(kanal, station) {
});
// Discord verschiebt Sprachserver im Betrieb; ohne dieses Zurückholen
// bleibt die Verbindung dann still stehen, statt sich zu erholen.
verbindung.on(VoiceConnectionStatus.Disconnected, async () => {
//
// 4014 heisst „rausgeworfen oder verschoben" — da hilft kein
// Wiederverbinden, das wäre nur ein Ringen mit dem Moderator.
verbindung.on(VoiceConnectionStatus.Disconnected, async (_, stand) => {
if (stand.closeCode === 4014) {
stoppen(guildId);
clearRadioState(guildId);
return;
}
try {
await Promise.race([
entersState(verbindung, VoiceConnectionStatus.Signalling, 5000),
@@ -194,9 +202,24 @@ export async function spielen(kanal, station) {
try {
await entersState(verbindung, VoiceConnectionStatus.Ready, 20_000);
} catch {
// Wo genau sie hängen blieb, entscheidet die Ursache — deshalb steht
// der Zustand in der Meldung und nicht nur „ging nicht":
//
// signalling → Discord hat den Sprachserver nie genannt. Das ist ein
// Rechte- oder Gateway-Problem, kein Netzproblem.
// connecting → der Sprachserver ist bekannt, aber der UDP-Austausch
// mit ihm scheitert. Praktisch immer: ausgehendes UDP
// ist dicht (Container, Firewall, Hoster).
const steckt = verbindung.state.status;
verbindung.destroy();
laufend.delete(guildId);
throw new Error('Die Sprachverbindung kam nicht zustande.');
const grund = steckt === VoiceConnectionStatus.Connecting
? 'Der Sprachserver antwortet nicht auf UDP — ausgehendes UDP ist vermutlich gesperrt.'
: steckt === VoiceConnectionStatus.Signalling
? 'Discord nennt keinen Sprachserver — fehlen dem Bot Rechte im Kanal?'
: `Die Verbindung blieb bei „${steckt}" stehen.`;
console.error(`[radio] Verbindung steckt bei "${steckt}": ${grund}`);
throw new Error(`Sprachverbindung kam nicht zustande. ${grund}`);
}
const player = vorher?.player ?? createAudioPlayer({
+9 -4
View File
@@ -2471,11 +2471,16 @@ ${rssItems}
// Audit-Log: die letzten Team-/Owner-Aktionen im Webinterface (nur Owner)
app.get('/api/auditlog', async (request, reply) => {
if (requireAdmin(request, reply)) return;
// 64 und nicht 48: Discord nimmt nur Zweierpotenzen von 16 bis 4096.
// Mit 48 warf die Bibliothek, und weil der Wurf durch das map lief,
// riss ein einziges Avatar das ganze Protokoll mit — 500 statt Liste.
const bild = (id) => {
for (const guild of client.guilds.cache.values()) {
const m = guild.members?.cache?.get(id);
if (m) return m.displayAvatarURL?.({ size: 48 }) ?? null;
}
try {
for (const guild of client.guilds.cache.values()) {
const m = guild.members?.cache?.get(id);
if (m) return m.displayAvatarURL?.({ size: 64 }) ?? null;
}
} catch { /* ein fehlendes Bild ist kein Grund, die Liste zu verlieren */ }
return null;
};
return { entries: listAudit(100).map((e) => ({ ...e, avatar: bild(e.user_id) })) };