170 Commits
Author SHA1 Message Date
D4rkst3randClaude Opus 5 00e4774a5f feat(whitelist): /whitelist add und remove
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der lesende Weg ist am 04.09.2026 mit echten Daten durchgelaufen
("ja (2 von 2 Zutrittsrollen)"), also kommt jetzt die schreibende
Haelfte - in dieser Reihenfolge und nicht umgekehrt.

Welche Rolle vergeben wird
--------------------------
d4rk_discord:grantrole, eine EIGENE Convar in secrets.cfg. Nicht das
Adminpanel: eine Rollen-ID, die man an einer zweiten Stelle pflegt,
laeuft von der ersten weg - genau daran ist heute frueh schon das
Eingabefeld gescheitert. Die Rollenpolitik liegt vollstaendig in
secrets.cfg.

SIE MUSS EINE DER ZUTRITTSROLLEN SEIN. d4rk_lib prueft das und meldet
`vergabeGueltig`; ist sie es nicht, verweigert der Befehl. Eine Rolle
zu vergeben, die niemanden hereinlaesst, waere ein Knopf, der nichts
tut und dabei "erledigt" meldet - und das faellt erst auf, wenn jemand
vor der Tuer steht.

NICHT GESETZT IST KEIN FEHLER, sondern der Zustand im geschlossenen
Test. Der Befehl sagt das dann auch so.

Was geprueft wird, bevor etwas passiert
---------------------------------------
Rechtesystem erreichbar - Aufrufer hat d4rk.whitelist - Rollen lesbar -
Vergaberolle gesetzt - Vergaberolle oeffnet Zutritt - Person ist auf
dem Discord - Rolle existiert - Bot hat "Rollen verwalten" - Rolle
steht unter der hoechsten Rolle des Bots.

Die Hierarchiepruefung ist keine Kosmetik: ohne sie kommt eine nackte
HTTP-50013, und die liest sich wie "der Bot ist kaputt" statt wie "die
Rolle steht zu weit oben".

"Hat sie schon" und "hat sie gar nicht" sind Antworten, keine
Fehlschlaege.

Wo es landet
------------
Im Devlog-Kanal (neuer Helfer melden.js:inDevlog, wirft nie - ein
fehlgeschlagener Logeintrag darf den Vorgang nicht mitreissen, ueber
den er berichtet). NICHT im Serverprotokoll: der Weg dorthin ginge nur
ueber einen Schreibzugang fuer den Bot, und der wuerde die absichtlich
schmale Tuer aufmachen. Die Freischaltung steht ausserdem in Discords
Audit-Log, und beim Verbinden protokolliert d4rk_lib die Zulassung mit
Namen.

Gemessen
--------
Befehlsdefinition gebaut: whitelist status(person*) add(person*,grund)
remove(person*,grund), keine Ueberschreitung der Discord-Grenzen. Die
Grenzpruefung hat eine Kontrolle mit bekanntem Ausgang, die anschlaegt -
sonst waere "nichts ueber der Grenze" von "Pruefung ist blind" nicht zu
unterscheiden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:23:23 +02:00
D4rkst3randClaude Opus 5 90fefb2865 feat(zutritt): die Rollen werden gelesen, nicht abgeschrieben
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
GEMESSEN am 04.09.2026 an der Serverkonsole:

    "d4rk_discord:role" is "1439019849664696460,1544029550802108437"

ZWEI Rollen. Im Adminpanel stand seit heute frueh ein Eingabefeld, in
dem EINE davon stand. Wer nur die andere hat, kommt auf den Server -
und /whitelist status haette ihm "Whitelist-Rolle: nein" gesagt.

Das ist die Sorte Fehler, vor der der Kommentar an genau dieser Stelle
gewarnt hat, waehrend der Code daneben sie gebaut hat: zwei Orte fuer
dieselbe Wahrheit.

Was sich aendert
----------------
d4rk_lib exportiert `ZutrittRollen` - DIESELBE Parsung, die auch die
Zutrittspruefung benutzt, nicht eine zweite. Sie gibt Zutrittsrollen,
Sperrrollen, den ACE-Notausgang und ob der Discord-Token gesetzt ist
(nur ob, nie den Wert).

d4rk_web reicht das als /zutritt heraus. Nur lesend: setzen kann das
Panel es nicht, die Convar steht in secrets.cfg und wirkt erst nach
einem Serverstart. Ein Feld, das sich speichern laesst und nichts
aendert, ist schlimmer als keines.

Das Panel zeigt es an, statt es abzufragen. Das Eingabefeld ist weg;
ein bereits gespeicherter Wert bleibt unbeachtet liegen, statt beim
Start still geloescht zu werden.

Der Bot prueft gegen die MENGE, nicht gegen eine ID, und nennt die
Zahl ("ja (1 von 2)"). Die Sperrrolle wird ZUERST geprueft - sie
schlaegt jede Zutrittsrolle, und sie danach zu pruefen liesse ein "ja"
stehen fuer jemanden, der nicht hereinkommt.

KEIN RUECKFALL AUF EINE GESPEICHERTE ID mehr. Eine veraltete Liste
antwortet selbstbewusst falsch; "nicht pruefbar" ist die richtige
Antwort auf eine Frage, die man gerade nicht beantworten kann.

Gemessen nach dem Neustart:
  /zutritt -> rollen: 2, sperrrollen: 0, aceKnoten d4rk.whitelist,
              tokenGesetzt true  - deckt sich mit der Konsole

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 12:11:27 +02:00
D4rkst3randClaude Opus 5 8b4efb2cb9 feat(bot): Whitelist-Rolle kommt vom Panel, nicht aus der Umgebung
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Zwei Aenderungen, die zusammengehoeren.

1. darfWhitelist las ein Feld, das es nicht mehr gibt
------------------------------------------------------
/person im Spielserver nimmt jetzt das zu pruefende Recht als Parameter
und antwortet mit `darf` statt mit `darfWhitelist`. Der alte Zugriff
haette ab sofort still immer false ergeben - also: jeder im Team
ausgesperrt, ohne eine Fehlermeldung, die darauf hindeutet.

`person()` nimmt das Recht jetzt optional mit; ohne Recht bleibt der
Aufruf, was er war.

2. Die Rollen-ID stand an zwei Orten
------------------------------------
Sie muss zu der passen, die der Spielserver prueft. Stimmen sie nicht
ueberein, zeigt /whitelist status "hat die Rolle" fuer eine Rolle, die
den Zutritt gar nicht oeffnet - ein Fehler, der wie eine richtige
Antwort aussieht.

Deshalb wird sie nur noch an EINEM Ort eingetragen: auf der
Einstellungsseite des Adminpanels. Der Bot holt sie ueber /bot/config,
30 Sekunden gepuffert. Ein Fehlschlag wird NICHT gepuffert, sonst bliebe
eine Stoerung eine halbe Minute stehen, nachdem sie vorbei ist.

WHITELIST_ROLE_ID bleibt als Rueckfall, damit der Befehl auch dann noch
etwas anzeigen kann, wenn das Panel nicht antwortet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:58:47 +02:00
D4rkst3r c840f6fc7e merge: feat/bot-ueber-panel
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
2026-09-04 10:43:34 +02:00
D4rkst3randClaude Opus 5 b1ec1a2a29 feat(bot): der Bot fragt das Panel, nicht den Spielserver
Betreiber am 04.09.2026: "koennen wir das nicht mit in die webseite einbauen,
das er sich das ueber das acp holt?" - das ist der bessere Entwurf, und der
Grund ist das geringste Recht.

WAS DER ERSTE ANLAUF FALSCH GEMACHT HAETTE

Er haette dem Bot das Geheimnis von d4rk_web gegeben. Dann haetten ZWEI
Dienste den Schluessel zu allem gehabt: Rechtematrix, Spielerliste, Protokoll,
Schreibweg. Ein Bot, der nur wissen soll, ob jemand die Whitelist verwalten
darf, braucht davon nichts.

Jetzt bleibt das Panel der einzige, der mit dem Spielserver redet, und der Bot
bekommt einen eigenen Schluessel (BOT_TOKEN), der genau eine Tuer oeffnet:
/bot/person.

EIGENER PFAD, EIGENER WAECHTER

/bot/ statt /api/bot/, weil der Sitzungswaechter dort Discord-Anmeldung und
Rolle prueft - beides hat eine Maschine nicht. Der Botwaechter vergleicht den
Schluessel zeichenweise (dieselbe Ueberlegung wie in d4rk_web) und antwortet
bei fehlendem oder falschem Schluessel mit 404, nicht 401: wer von aussen
probiert, soll nicht erfahren, dass es hier einen Maschinenzugang gibt.

Unter 32 Zeichen gibt es den Weg gar nicht - eine halbe Konfiguration darf
keine Tuer aufmachen.

DURCHGEREICHT, NICHT AUSGEWERTET. Das Panel entscheidet an dieser Stelle
nichts; sonst waere es die zweite Rechteverwaltung, die ADR-0012 ausschliesst.

NEBENBEI ZWEI FALLEN VERMIEDEN

Dem Bot fehlt extra_hosts - host.docker.internal haette gar nicht aufgeloest,
und der Fehler haette wie "Spielserver antwortet nicht" ausgesehen. Ueber das
oeffentliche Panel braucht er es nicht.

Und das Panel nennt seine zwei Werte D4RK_WEB_URL/D4RK_WEB_TOKEN; mein
Botmodul hiess erst SPIELSERVER_*. Zwei Vokabeln fuer eine Sache bedeuten,
dass man beim Umzug an zwei Stellen sucht und eine vergisst. Die Datei heisst
jetzt panel.js - ein Modul namens spielserver.js, das mit dem Panel redet,
waere ein Name, der luegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 10:43:34 +02:00
D4rkst3r 6fab871225 merge: feat/bot-whitelist
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
2026-09-04 09:52:28 +02:00
D4rkst3randClaude Opus 5 958b3b64f2 feat(bot): /whitelist status - der Bot fragt den Spielserver, wer was darf
Betreiber am 04.09.2026: "wenn wir das in den d4rkbot mit einbauen koennen
warum nicht".

DIE WHITELIST *IST* EINE DISCORD-ROLLE

d4rk_lib prueft bei jeder Verbindung live, ob die Person eine bestimmte Rolle
auf diesem Discord hat. "Jemanden auf die Whitelist setzen" heisst also: ihm
die Rolle geben. Das kann nur der Bot - der Spielserver hat keinen Zugriff auf
Discord.

Umgekehrt weiss nur der Spielserver, WER das darf. Deshalb fragt der Bot ihn,
statt Discord-Berechtigungen zu pruefen: sonst gaebe es zwei
Rechteverwaltungen, und die zweite erfuehre nie von einer Aenderung in der
Matrix. Dieselbe Regel wie ADR-0012 fuer das Panel setzt.

DIESER SCHRITT LIEST NUR

`/whitelist status @person` beantwortet drei Fragen an einem Ort: hat sie die
Rolle (fragt der Bot bei Discord), kennt der Spielserver ihr Konto, welche
Teamrolle hat sie bei uns.

Vergeben und Entziehen kommt als eigener Schritt - erst wenn dieser Weg
nachweislich durchlaeuft. Ein Schreibbefehl auf einem Kanal, den niemand
gemessen hat, ist ein Schreibbefehl ins Ungewisse.

DREI AUSGAENGE BEI DER RECHTEFRAGE, NICHT ZWEI

erlaubt, verboten, und "ich weiss es nicht". Der dritte ist der wichtige: ein
Bot, der bei einer Stoerung "verboten" sagt, sperrt das Team aus; einer, der
"erlaubt" sagt, oeffnet es fuer alle. Beides waere geraten.

WAS DER SPIELSERVER NICHT WEISS, BEHAUPTET ER AUCH NICHT: ob die Discord-Rolle
gesetzt ist, sieht er nur im Moment einer Verbindung. Der Bot fragt sie selbst
- er hat den Zugang.

DER ZWEITE HALTER DES GEHEIMNISSES

Bisher kannte nur das Adminpanel den Token fuer d4rk_web. Der Bot ist jetzt der
zweite. Beide sind eigene Container auf demselben Host und reden ueber das
interne Netz - aber es bleibt eine Verdopplung, und sie steht deshalb im Kopf
der Datei und in der .env.example, nicht in einer Fussnote.

Ohne SPIELSERVER_URL und SPIELSERVER_TOKEN tut /whitelist NICHTS und sagt es.
Ein Bot, der bei fehlender Konfiguration durchwinkt, waere schlimmer als einer,
der schweigt.

NOCH NICHT GEMESSEN: der Weg vom Bot zum Spielserver. Dafuer muessen die drei
Umgebungsvariablen im Container gesetzt sein, und das ist eine Handlung des
Betreibers. Die Serverseite (/person) ist dagegen gemessen: eine erfundene
Kennung liefert rolle=keine, kontoGelesen=true, konto=nil.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-04 09:52:28 +02:00
D4rkst3r 7e7d04378a Pause fuer YouTube -- und der Bot bleibt draussen, wenn er rausgeht
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Zwei Befunde aus dem Betrieb.

1. Der Bot ging aus dem leeren Kanal und kam sofort wieder rein.

`stoppen()` hielt erst den Spieler an und loeschte dann aus `laufend`.
`player.stop()` meldet den Leerlauf aber SYNCHRON -- gemessen mit
@discordjs/voice 0.19: der Idle-Melder laeuft mitten in stop(), noch vor
der naechsten Zeile. Er fand den Eintrag also noch, hielt das Ende fuer
"Titel vorbei" und schaltete weiter: naechster Titel oder zurueck auf den
Heimsender. Von aussen genau das gemeldete Bild.

Jetzt wird erst geloescht, dann angehalten. Nachgestellt mit einem echten
AudioPlayer und beiden Reihenfolgen -- die Kontrolle ist dabei die alte
Reihenfolge: sie schaltet nachweislich weiter (1x), die neue nicht (0x).
Ohne diese Gegenprobe koennte der Test etwas ganz anderes messen.

2. Im YouTube-Modus fehlte die Pause.

`pausieren()` gibt es jetzt, ausdruecklich nur fuer YouTube. Ein Sender
laesst sich nicht sinnvoll anhalten: er laeuft beim Betreiber weiter,
waehrend hier die Leitung volllaeuft, und beim Fortsetzen kaeme eine
Konserve, die immer weiter hinterherhinkt. Fuer einen Sender heisst Pause
Stopp -- dafuer gibt es den Stopp-Knopf, und der Pause-Knopf graut aus.

Die verstrichene Zeit rechnet Pausen heraus, sonst liefe der
Fortschrittsbalken nach dem Fortsetzen um die Pausenlaenge voraus.
`verstrichen()` steht an EINER Stelle, die sich Discord-Tafel und Panel
teilen -- zwei Rechnungen waeren zwei Gelegenheiten, die Pause zu
vergessen.

Die Musik-Tafel bekommt eine zweite Knopfreihe: mit der Pause waeren es
sechs Knoepfe, Discord laesst fuenf je Reihe zu. Platz ist reichlich (vier
von fuenf Reihen), und die Trennung ist ohnehin sinnvoller -- oben, was
gerade klingt, unten, was mit der Liste passiert. Nachgezaehlt: 2 Reihen
ohne Liste, 4 mit.

Beim Quellenwechsel wird ausdruecklich fortgesetzt: der Spieler ueberlebt
den Wechsel, und stand er auf Pause, waere der neue Titel stumm gestartet
waehrend der Eintrag "laeuft" sagt.

Nebenbei: mein Feld-Abgleich hielt eine zweite Kopie der Feldliste von
`radioStand()` und driftete beim ersten neuen Feld prompt ab -- er meldete
`pausiert` als fehlend, obwohl die API es liefert. Er liest die Form jetzt
aus der Quelle, statt sie doppelt zu pflegen.
2026-08-28 21:46:24 +02:00
D4rkst3r 15babcd75d Zwei Fehler aus dem Betrieb: fehlender Import, geschlossenes stdin
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Beide aus dem Serverlog vom 28.08.2026, beide meine.

1. `getStation is not defined` bei jedem /api/radio/status, sobald etwas
lief. Die Funktion stand in api.js im Code und in keinem Import.

Weder `node --check` noch das Laden des Moduls sieht das: ESM prueft beim
Verlinken nur, ob die IMPORTIERTEN Namen in der Quelle existieren, nicht
ob die BENUTZTEN importiert sind. Mein API-Testlauf kam nie an die Zeile,
weil dort nie etwas lief -- genau die Luecke, die ich beim Ausliefern als
"nicht geprueft" benannt hatte.

Ein Pruefer dafuer liegt jetzt im Scratchpad und meldet fuer src/ null
Treffer. Er hat zwei Gegenproben: ein eingebauter Aufruf ohne Import muss
gefunden werden, ein dynamisch importierter darf NICHT gemeldet werden.
Seine ersten beiden Fassungen haben Fehlalarm geschlagen (14 bzw. 11
Treffer, alle dynamische Importe) -- ein Pruefer, der Richtiges als falsch
meldet, ist so wertlos wie einer, der nichts sieht.

2. `Cannot read properties of null (reading 'on')` in tonquelle, direkt
gefolgt von `ffmpeg: pipe:0: Invalid data found when processing input`.

`wandler` startete ffmpeg immer mit stdio ['ignore','pipe','pipe'] --
stdin also ZU. Fuer YouTube bekommt ffmpeg aber `-i pipe:0`. Damit las es
aus /dev/null, und `ff.stdin` war null, woran schon das Anhaengen der
Fehlerbehandlung zerbrach. Nachgestellt und bestaetigt: dieselbe
stdio-Form liefert stdin === null, mit 'pipe' eine Rohrleitung.

stdin ist jetzt offen, wenn die Eingabe `pipe:0` ist. Und `tonquelle`
prueft die Rohrenden, bevor es sie anfasst: startet ein Prozess nicht,
gibt es eine Meldung statt eines TypeErrors, der die ganze Interaktion
mitreisst.
2026-08-28 14:17:36 +02:00
D4rkst3r e6f8e3c3c5 Beim Sender kam die Warteschlange nie dran
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Im Betrieb gemeldet: Link eingereiht, Bot blieb beim Radio.

`ggfAnwerfen` stieg aus, sobald ueberhaupt etwas lief -- Sender wie Titel.
Der Kommentar daneben sagte "die Liste kommt dran, wenn sie dran ist", und
beim Sender war das schlicht falsch: ein Sender endet nie, es gibt also
kein Idle, das weiterschaltet. Der einzige Idle, den es gibt, ist ein
abgerissener Strom -- und der startet denselben Sender neu. Der Titel
wartete auf einen Moment, der nie kommt.

Damit war auch der Rueckweg unerreichbar: "Liste leer -> zurueck auf den
Sender" setzt voraus, dass man ueberhaupt erst vom Sender wegkommt.

Jetzt uebernimmt ein frisch eingereihter Titel sofort, wenn ein Sender
laeuft. Der Sender ist nicht verloren -- er steht als heimStation im
laufenden Eintrag und kommt zurueck, sobald die Liste leer ist. Ein
laufender YouTube-Titel wird weiterhin nicht unterbrochen.

Die Regel steckte als Bedingung mitten in der Funktion und war von aussen
weder zu lesen noch zu pruefen. Sie heisst jetzt `uebernimmtSofort()` und
laesst sich mit allen drei Eingaben durchspielen -- genau das ist gemacht:
nichts laeuft -> true, Sender laeuft -> true (war vorher false, das war der
Fehler), YouTube laeuft -> false.

Der Quellenwechsel geht jetzt ins Log. Von aussen sieht "YouTube
uebernimmt" genauso aus wie "nichts passiert", und der Unterschied ist
genau der, den es hier zu sehen gibt.

Die Musik-Tafel behauptete "kommt als Naechstes dran" -- korrigiert auf
"uebernimmt sofort".
2026-08-28 14:09:09 +02:00
D4rkst3r c46581a548 Merge pull request 'Radio youtube' (#2) from radio-youtube into main
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Reviewed-on: #2
2026-08-28 14:02:21 +02:00
D4rkst3r e165402f7d Dokumentation nachziehen -- und einen verlorenen Wunsch retten
Der README behauptete an prominenter Stelle "bewusst kein YouTube".
Das stimmt seit vier Commits nicht mehr, und ein Satz, der das Gegenteil
des Codes sagt, wird geglaubt. Ersetzt samt der PO-Token-Lage und dem
Hinweis auf Cookies und eigene Argumente.

.env.example kennt jetzt FFMPEG_PATH und YTDLP_PATH -- mit dem Hinweis,
dass YTDLP_PATH=/bin/false der Weg ist, den Fehlerpfad zu pruefen.

Beim letzten Durchlesen aufgefallen: `weiter()` holt den naechsten Titel
aus der Liste, BEVOR `spielen()` laeuft. Scheitert der Beitritt in den
Sprachkanal -- fehlendes Recht, voller Kanal, DAVE --, war der Eintrag
weg. Genau im haeufigsten Fall: jemand reiht etwas ein, waehrend der Bot
noch gar nicht im Kanal sitzt, der erste Beitritt ist der, der scheitern
kann, und der Wunsch verschwindet wortlos.

Jetzt wird er wieder ganz vorn eingereiht und der Fehler weitergereicht.
Gegenprobe mit einem Kanal, dessen Beitritt garantiert wirft: drei Titel
vorher, drei Titel nachher, "Erster" wieder an erster Stelle.
2026-08-28 13:52:56 +02:00
D4rkst3r dbec4e9775 Panel: die volle Fernbedienung ueber der Senderliste
Was laeuft, wo, aus welcher Quelle, mit Fortschrittsbalken. Sprachkanal
waehlen, Sender abspielen, Ueberspringen, Zum Radio, Stopp,
Lautstaerke-Schieber. Einreihen per Link oder Suchbegriff, Warteschlange
mit Hoch/Runter/Weg und Leeren. Darunter ein aufklappbarer yt-dlp-Block:
Zustand, Fassung, cookies.txt, eigene Argumente, Aktualisieren -- und die
letzte Fehlerzeile im Klartext.

Steht ueber der Senderliste, weil man oefter etwas abspielt als die Liste
pflegt.

Die Abfrage laeuft nur, solange der Community-Reiter offen ist. Sonst
ginge alle fuenf Sekunden eine Anfrage gegen eine Ansicht raus, die
niemand ansieht.

Der Balken erscheint nur bei bekannter Dauer. Bei einem Livestream gaebe
er einen Stand vor, den niemand kennt -- dort steht stattdessen "live".

Die Kanalauswahl graut aus, wo der Bot nicht rein darf. Ein Kanal, den das
Panel anbietet und in den der Bot dann nicht kommt, sieht aus wie ein
kaputter Knopf.

Geprueft: der Vite-Build laeuft durch, und ein Abgleich stellt alle 30
Feldpfade, die die Oberflaeche vom Stand liest, gegen die echte
API-Antwort -- 0 fehlen. Der Abgleich hat eine Gegenprobe mit bekanntem
Ausgang (ein Feld, das es nicht gibt, muss als fehlend erkannt werden) und
prueft vorher, ob die sonst leeren Zweige (Warteschlange, Cookies, letzter
Fehler) ueberhaupt besetzt sind -- sonst meldet er "ungueltig" statt "in
Ordnung". Seine erste Fassung hatte Fehlalarm geschlagen (?. und .map als
fehlende Felder gelesen); das ist behoben.

NICHT geprueft: das tatsaechliche Rendern im Browser -- hier ist weder
Browser noch DOM-Werkzeug vorhanden.
2026-08-28 13:50:06 +02:00
D4rkst3r 1ac349029f Die Fernbedienung: Radio vom Panel aus steuern
Neun Endpunkte unter /api/radio: Stand, Steuern, Warteschlange (einreihen,
entfernen, leeren, sortieren), Suche und der yt-dlp-Block.

Alles geht durch dieselben Funktionen wie die Discord-Tafeln. Eine zweite
Abspiellogik neben radio.js waere eine zweite Gelegenheit, sich anders zu
verhalten als die erste.

Start, Stopp, Skip, Sender und Lautstaerke liegen als Aktionen auf einem
Endpunkt statt auf fuenf: alle brauchen dieselbe Server-Aufloesung,
dieselbe Rechtepruefung und antworten mit demselben Stand. Fuenf Geruest-
Kopien waeren fuenf Gelegenheiten, eine davon zu vergessen.

Ein Suchbegriff wird auch hier nicht geraten -- die Treffer gehen zurueck,
gewaehlt wird im Panel. Genau wie in Discord.

Die cookies.txt bekommt einen eigenen Endpunkt statt eines Feldes in den
Einstellungen: der Inhalt ist mehrzeilig und enthaelt Sitzungsschluessel
eines echten Kontos. Er gehoert in eine Datei mit 0600 neben die Datenbank,
und zurueck kommt nie der Inhalt, nur Groesse und Datum. Nur der Owner
darf ihn setzen, nicht das Team.

listVoiceChannels() prueft die Rechte mit: ein Kanal, den das Panel
anbietet und in den der Bot dann nicht darf, sieht aus wie ein kaputter
Knopf.

Beim Schreiben aufgefallen und behoben: radioState() wurde benutzt, aber
nicht importiert -- das haette beim ersten Senderwechsel aus dem Panel
geknallt. Der Syntaxcheck sieht so etwas nicht, das Laden des Moduls schon.

Geprueft mit echtem Fastify, signiertem Sitzungs-Cookie und gefaelschtem
Discord-Client: 24 Faelle, davon acht Gegenproben, die fehlschlagen
muessen (ohne Anmeldung 401, unbekannte Aktion, Sender ohne ID, Skip ohne
Kanal, leere Eingabe, kaputte Adresse, Sortieren ohne Angabe, Cookies ohne
Netscape-Kennzeile). Alle 24 wie erwartet.
2026-08-28 13:44:28 +02:00
D4rkst3r 2a0cb1905d Die zweite Tafel: /musik, Warteschlange, Eingabefenster
Eine eigene Nachricht statt ein paar Knoepfe mehr an der Radio-Tafel.
Discord laesst fuenf Reihen je Nachricht zu, vier davon gehoeren dort den
Sender-Menues -- fuer Ueberspringen, Einreihen, Warteschlange und Leeren
ist kein Platz, und ein Sender-Menue zu opfern hiesse, Sender aus der
Auswahl zu werfen.

Der Vorzug ist groesser als die Notloesung: /radio bleibt genau das, was
es war. Wer nie ein Video einreiht, merkt vom ganzen Umbau nichts.

Bei einem Suchbegriff wird bewusst nicht der erste Treffer genommen,
sondern die Auswahl gezeigt: "Sleeping Sun" hat drei Fassungen, und
welche gemeint war, weiss nur wer es getippt hat. Eine Adresse geht
direkt rein, eine Playlist mit allen Eintraegen auf einmal.

Die Abhaengigkeit zeigt nur in eine Richtung: radio-musik.js kennt das
Radio, das Radio kennt es nicht -- es meldet ueber beiAenderung() nur,
dass sich etwas geaendert hat. Ein gegenseitiger Import zwischen zwei
Dateien beisst beim naechsten Umbau.

Gemessen gegen echtes YouTube am 28.08.2026: Adresse landet in der Liste,
Suchbegriff liefert fuenf Treffer und reiht NICHTS ein (Liste nachweislich
unveraendert), kaputte Adresse meldet "Video unavailable" und laesst die
Liste in Ruhe. Die Tafel zeigt Dauer je Titel, Gesamtdauer, Wunschgeber
und die letzte Fehlerzeile im Klartext.

Nicht geprueft, weil es dafuer eine echte Discord-Sprachverbindung
braucht: die Darstellung waehrend etwas laeuft, und das Weiterschalten.
2026-08-28 13:39:46 +02:00
D4rkst3r 409b28e9e0 Eine Quelle, zwei Arten -- Sender und Titel gehen denselben Weg
Der ganze Unterschied ist einer: ein Sender endet nie, ein Titel schon.
Also verzweigt genau eine Abspielfunktion und genau ein Idle-Melder nach
der Art der Quelle, statt dass ein zweiter Pfad daneben entsteht -- zwei
Pfade, die dasselbe tun sollen, driften auseinander.

Beim Sender heisst Idle "die Leitung ist weg" (neu aufbauen), bei YouTube
"fertig, der naechste bitte". Danach: Warteschlange, sonst der Sender von
vorher, sonst raus. Das Radio ist die Grundstellung, YouTube die
Unterbrechung.

Bremse gegen das Durchrutschen: ein Eintrag ohne Ton geht sofort wieder
auf Idle. Ohne Zaehlung waere eine Liste mit einem kaputten Eintrag in
Sekundenbruchteilen leer, und niemand haette gesehen warum. Nach drei
Fehlschlaegen in Folge wird angehalten und der Grund gemeldet. Von Hand
Uebersprungenes zaehlt nicht mit.

Zwei Fehler nebenbei, beide vorher schon da:

Der Idle-Melder hielt `kanal` aus dem Abschluss des ERSTEN Aufrufs fest.
Nach einem Kanalwechsel haette der Wiederanlauf in den alten Kanal
gespielt. Er liest den Kanal jetzt frisch aus `laufend`.

`radioLautstaerke()` gab bei unbelegter Einstellung 10 zurueck statt der
50, die der Kommentar zwei Zeilen darueber als Vorgabe begruendet:
`Number(null)` ist 0 und damit endlich, die Pruefung auf "nicht endlich"
hat den unbelegten Fall nie erwischt und auf die Untergrenze geklemmt.
Eine frische Installation startete also sehr leise. Gemessen gegen eine
leere Datenbank, mit Gegenproben fuer leeren String, Unsinn, 0 und 999.
2026-08-28 13:33:42 +02:00
D4rkst3r af16bc1b18 YouTube als zweite Tonquelle: yt-dlp, Warteschlange, Abbild
Das Fundament, noch ohne Anbindung ans Radio.

yt-dlp holt selbst und schiebt rohe Bytes durch ein Rohr an ffmpeg. Der
bequemere Weg waere `-g` und die fertige Adresse -- der ist eine Falle:
googlevideo-Adressen laufen ab und haengen an der IP, die sie geholt hat.
Das schlaegt nach zehn Minuten zu und sieht dann aus wie ein Netzproblem.

Beim Stroemen nach stdout ist die Vorgabe von yt-dlp *mit* Bild; ohne ein
ausdrueckliches `-f bestaudio/best` laedt ein Tonstrom das ganze Video.
Nachgelesen im README der Fassung 2026.08.19.

stderr wird aufgehoben statt weggeworfen. Genau hier entsteht sonst der
Fehler, den niemand deuten kann: YouTube antwortet "Sign in to confirm",
yt-dlp bricht ab, der Bot sitzt still im Kanal. Die letzte ERROR-Zeile
geht ins Panel.

Gemessen am 28.08.2026 gegen echtes YouTube (yt-dlp 2026.08.19, von hier
aus, NICHT auf dem Zielhost): Einzelvideo, Suche und flache Playlist
liefern die erwarteten Felder, eine kaputte Video-ID meldet "Video
unavailable" und wird als Fehler durchgereicht statt als leere Liste.

Die Warteschlange steht in der Datenbank, nicht nur im Speicher -- ein
Deploy soll die Musik unterbrechen, nicht die Wuensche von fuenf Leuten
wegwerfen. Die YouTube-Tafel bekommt eine eigene Tabelle, weil
`radio_state` beim Stoppen geloescht wird und die Tafel das ueberleben
soll.

Nebenbei: der ffmpeg-Kommentar im Dockerfile beschrieb noch die alte
Ogg/Opus-Kette. Seit der Lautstaerkeregelung laeuft dort PCM.
2026-08-28 13:26:05 +02:00
D4rkst3randClaude Opus 5 19010e9fde Die Panel-Anbindung entfaellt -- das Panel postet jetzt selbst
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Entscheidung des Betreibers am 19.08.2026: das d4rk_gameserver-Panel
baut und stellt seine Status-Embeds ab sofort selbst in den Discord.
Damit hat diese Anbindung keinen Zweck mehr.

WARUM SIE UEBERHAUPT WEG SOLL, und nicht bloss ungenutzt liegenbleibt:
sie war die zweite Meinung ueber denselben Zustand. `zustandText()` gab
`null` zurueck, wenn ein Server LIEF UND BEREIT war -- also im
Normalfall -- und der Monitor fiel dann auf seine gamedig-Abfrage
zurueck. Erreichte die den Server nicht, stand im Kanal "Offline",
waehrend dasselbe Embed RAM und CPU aus dem Panel anzeigte. Ein
Widerspruch, den man von keiner der beiden Seiten aufloesen konnte.

ENTFERNT:

    src/panel.js                      die Anbindung
    src/panel-abgleich.js             das Anlegen/Wegraeumen von Eintraegen
    tools/panel-pruefen.mjs           die zugehoerigen Proben
    tools/panel-abgleich-pruefen.mjs
    panelUrl/panelZeichen             in runtime-settings.js
    panel_url/panel_zeichen           als Einstellung und im Formular
    Servertyp `panel`                 er fragte nicht selbst, sondern las ab

WAS DER MONITOR DAMIT VERLIERT, und es gehoert dazugesagt: die
Schonfrist beim Hochfahren. Ein startendes Modpack antwortet minutenlang
nicht -- am ATM10 gemessen 306 Sekunden -- und meldet dadurch wieder
einen Ausfall, wo vorher "startet gerade" stand. Die
Zwei-Fehlschlaege-Schwelle federt das teilweise ab, nicht ganz.

Fuer Server im Panel ist das kein Verlust: dort postet das Panel und
kennt den Unterschied. Fuer FiveM und LS25 hat es ihn nie gegeben.

Geprueft: alle 281 relativen Importe loesen auf, die geaenderten Dateien
sind syntaktisch in Ordnung, das Frontend baut. Der Bot selbst ist NICHT
neu gestartet worden -- das ist eine Entscheidung des Betreibers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-19 12:01:21 +02:00
D4rkst3randClaude Opus 5 0d7628f14e Den Entwurf und das Logo-Original einsortieren
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Drei Dateien lagen unversioniert im Wurzelverzeichnis herum. Alle drei
gehoeren ins Repo, nur nicht dorthin:

  logo.png                        -> docs/brand/logo.png
  ChatGPT Image 23. Juni 2025 ... -> docs/brand/studie-kopf-2025-06.png
  design_handoff_redesign/        -> docs/redesign/

logo.png ist NICHT irgendein Logo, sondern das Original, von dem
docs/brand/README.md die ganze Zeit spricht: "ein einziges Original, 2048x2048
mit Transparenz". Nachgerechnet statt geraten -- auf den Inhalt beschnitten
ergibt es 1986x1963, exakt die Masse von lockup.png. Es lag also die Quelle
der Marke unversioniert daneben.

Dabei aufgefallen und in der README vermerkt, nicht repariert: build.py liest
das Original ueber einen festen Pfad ausserhalb des Repos
(QUELLE = A:/Users/DAR/logo.png). Jetzt sind das zwei Kopien, die
auseinanderlaufen koennen. Umstellen kann nur, wer weiss, welche der beiden
gepflegt wird.

Das zweite Bild ist eine frueherere Kopf-Studie und nicht die Vorlage der
heutigen Marke -- andere Zeichnung, flacher, ohne Fell und Schulter. Sie wird
nirgends benutzt und liegt als Herkunft.

Vom Entwurfs-Paket ist der assets/-Ordner entfallen: er enthielt Kopien von
mark.png und lockup.png, die zwei Verzeichnisse weiter im Original liegen. Die
.dc.html-Referenzen zeigen jetzt auf ../brand/ und stellen das Logo weiter
dar, statt mit kaputten Bildern zurueckzubleiben. Der Kopf der README sagt
ausserdem, dass der Entwurf umgesetzt ist und wo bewusst abgewichen wurde --
sonst liest sich das Dokument in einem Jahr wie eine offene Aufgabe.

Dazu eine Zeile, die seit dem Redesign nicht mehr stimmte: build.py bezeichnet
(10, 10, 10) als "--bg der Marke". Das ist seit gestern #131316. Wirkt erst,
wenn jemand build.py laufen laesst -- die erzeugten Icons in frontend/public/
tragen bis dahin den alten Grund. Neu bauen konnte ich sie hier nicht, es gibt
weder Python noch Pillow auf dieser Maschine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:19:49 +02:00
D4rkst3randClaude Opus 5 25d36c7269 Webseiten-Redesign: Graphit + Orange, abgeschraegte Ecken
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Umsetzung des Entwurfs aus design_handoff_redesign/ -- beide Auftritte
(Produktseite und Community-Hub) plus Dashboard.

  Farbe    #0a0a0a + Neon-Gelb   ->  #131316 + Orange-Verlauf aus dem Logo
  Schrift  Bebas/Barlow/ShareTech ->  Chakra Petch + IBM Plex Mono
  Form     border-radius          ->  clip-path, oben links + unten rechts

Rauschen, Scanlines, Hintergrundschrift, Kopf-Raster und die Partikel sind
raus; der Schein von oben rechts ersetzt sie. Die Abschraegung ist ein
System und keine sechzig Einzelregeln: eine clip-path-Regel mit --bv, jede
Komponente setzt nur die Tiefe.

Der Farbdurchstich ging durch alle ~55 Abschnitte der style.css, nicht nur
durch die fuenf gezeichneten Seiten -- sonst waeren Galerie, Level,
Composer und Emoji-Auswahl im alten Gelb stehen geblieben. Von --neon ist
nichts uebrig.

Die Schriften kommen weiter self-hosted ueber @fontsource. Der Entwurf
schlaegt einen Google-Fonts-Link vor; das waere eine Fremdanfrage mehr, wo
der Aufbau bisher ohne auskam.

ZWEI FEHLER unterwegs gefunden, beide aelter als dieser Umbau:

1. .btn stand in der Datei NACH .btn-save und .btn-ghost. Bei gleicher
   Spezifitaet gewinnt die spaetere Regel, also hat ihr `background: none`
   jeden Verlauf ueberschrieben -- der Speichern-Knopf sah aus wie ein
   Ghost-Knopf, schon in der gelben Fassung. Die Grundform steht jetzt
   vor allen Abwandlungen.

2. .nav-logo ist ein Flex-Container, und `gap` legte sich zwischen die
   Textknoten "D4RKST" und <span>3R</span>. In der Leiste stand
   "D4RKST 3R". Der Abstand haengt jetzt am Bild statt am Container.

DREI ABWEICHUNGEN vom Entwurf, bewusst:

- Die Statuszeile im Hero zeigt nur "ONLINE", nicht "38 MS · UPTIME
  99,2 %". Antwortzeit und Erreichbarkeit des Bots liegen ausschliesslich
  hinter /api/settings und damit hinter der Anmeldung; oeffentlich gibt es
  sie nirgends. Dafuer braeuchte es einen neuen Endpunkt -- erfunden wird
  hier nichts.
- Keine Aura am Hauptknopf: clip-path schneidet den Schatten des eigenen
  Elements mit weg. Im Referenz-HTML stehen box-shadow und clip-path am
  selben Element, dort ist sie also ebenso wenig zu sehen. Tote
  Deklaration weggelassen statt mitgeschleppt.
- Die Config-Bereiche bleiben scharfkantig statt abgeschraegt. In ihnen
  klappen Auswahllisten aus dem Panel heraus, die clip-path abschneiden
  wuerde. Deckt sich mit der Vorgabe, dass die uebrigen Tabs nur die
  Token uebernehmen; die Dashboard-Panels selbst sind abgeschraegt.

Dazu: .content von 860 auf 1160 px, weil die dreispaltigen Raster es
brauchen -- die Devlog-Timeline behaelt 880 px Lesebreite, sonst waeren
die Zeilen zu lang. Der Punkt an den Timeline-Karten ist entfallen: er
sass ausserhalb der Karte und waere von der Abschraegung weggeschnitten
worden.

Welches Wort in einer Ueberschrift den Verlauf bekommt, steht jetzt im
Woerterbuch -- der Teil in eckigen Klammern ("Funk[tionen]", aber
"Fea[tures]"). So entscheidet jede Sprache selbst, wo sich das Wort
teilen laesst.

Mitgezogen: brand.js, das geteilte Design-System fuer die eingebundenen
Dienste. Neue Tokens und Abschraegungen, aber --neon/--neon2 bleiben als
Zweitname stehen -- fremde Apps benutzen sie in ihrem eigenen CSS.

Marken-Dateien nach frontend/public/brand/, verkleinert auf die Groessen,
in denen sie wirklich dargestellt werden: 2,4 MB -> 97 KB.

Geprueft: Build laeuft. Im Browser durchgesehen -- Landing, Funktionen,
Befehle, Hub-Start, Devlogs, Server, Roadmap, Dashboard.

Ungeprueft: das Dashboard mit echter Anmeldung. Eine Sitzung war hier
nicht moeglich, gesehen habe ich es ueber gestubbte API-Antworten. Ebenso
ungeprueft, wie sich die Schriftumstellung auf schmalen Schirmen macht.

NICHT angefasst: die Embed-Farben in runtime-settings.js stehen weiter auf
dem alten Gelb -- die wirken in Discord, nicht auf der Webseite. Und ist
brand_color in der Datenbank gesetzt, behalten die eingebundenen Dienste
ihre bisherige Farbe, unabhaengig von den neuen Vorgabewerten.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 15:15:37 +02:00
D4rkst3randClaude Opus 5 5394d5c0c6 Die zwei Sicherungs-Wachhunde abschalten
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Auf Ansage des Betreibers vom 18.08.2026. Gemeldet hatten sie:

    pruefeSicherung  -> "Die Sicherung ist fehlgeschlagen"
                        (zuletzt: MKCOL Sicherungen -> HTTP 000)
    pruefeZweitziel  -> "Bei dir zu Hause kommt keine Sicherung mehr an"

Beide bewachen Ziele, die es nicht mehr geben soll: Nextcloud wird nicht mehr
benutzt, und abgeholt wird auch nichts mehr. Die Sicherung bleibt auf dem
Server, darum kuemmert sich das Gameserver-System.

Ein Wachhund, der ein aufgegebenes Ziel bewacht, meldet jeden Tag denselben
Fehler -- und genau davon lernt man, Meldungen dieses Bots zu ueberlesen. Das
ist teurer als die Meldung wert war.

NICHT GELOESCHT, sondern abgeschaltet: beide Funktionen stehen unveraendert
im Modul. Wer Nextcloud oder die Abholung wieder einrichtet, macht eine Zeile
frei. Geprueft: node --check sauber, und ausser der abgeschalteten Zeile ruft
niemand mehr pruefeSicherung auf.

Die taegliche Sicherung selbst ist NICHT angefasst -- sie laeuft weiter, und
ihr eigener Fehlerfall meldet weiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 10:19:44 +02:00
D4rkst3r c81e862992 Wo der Bot nicht hinkommt, fragt das Panel -- Typ panel
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Bisher entschied `x.gamedig` darueber, ob aus einem Panel-Server ein
Monitor-Eintrag wird: "kennt unsere Bibliothek dieses Spiel?" Das hat
einen Fall ausgesperrt, den es wirklich gibt -- TeamSpeak.

UND ZWAR NICHT, WEIL GAMEDIG ES NICHT KOENNTE. Am 16.08.2026
nachgeschlagen, nachdem die Annahme "gamedig kennt TeamSpeak nicht"
sich als falsch erwies: gamedig 5.3.3 hat `teamspeak3`, DiscordGSM
fuehrt es ebenfalls. Der Grund ist ein anderer und ein besserer:

    node_modules/gamedig/protocols/teamspeak3.js
      queryPort = options.teamspeakQueryPort || 10011
      'use port=<voice>' -> 'serverinfo' -> 'clientlist'

Es braucht den SERVERQUERY-Port. Beim Betreiber liegt der hier:

    9987/udp  -> 0.0.0.0:9987      Sprache, oeffentlich
    10011/tcp -> 127.0.0.1:9988    ServerQuery, NUR localhost
    30033/tcp -> 0.0.0.0:9989      Dateien, oeffentlich

Der Bot laeuft im Container und erreicht 127.0.0.1 des Hosts nie. Zwei
Gegenproben vom Host aus, beide gescheitert (die zweite auch deshalb,
weil gamedig ohne `teamspeakQueryPort` stur 10011 nimmt -- den es dort
gar nicht gibt). Eine Kennung einzutragen brauechte also einen OFFENEN
Verwaltungsport, und das ist genau das, was die Regel "kein Port geht
ohne Eintrag ins Netz" verhindert.

Das Panel dagegen laeuft auf dem Host, darf hin und hat die
Zugangsdaten -- und schickt seine eigene Messung seit heute im
Statusbericht mit. Also:

  - `taugt` fragt `zeigbar` statt `gamedig`: "kann UEBERHAUPT JEMAND
    diesen Server abfragen -- wir oder das Panel fuer uns?" Mit
    Rueckfall auf `gamedig` fuer ein aelteres Panel; ohne den
    verschwaenden nach diesem Update alle Eintraege, und zwar lautlos.
  - Neuer eigener Typ `panel` neben `fivem` und `http`. Er ersetzt
    nichts: wo gamedig kann, bleibt es bei gamedig -- dessen Antwort
    bringt Karte, Fassung und Ping mit, die das Panel nicht liefert.
  - `spieler === null` heisst KEINE ANTWORT, nicht null Spieler. Das
    Panel sichert dazu einen Grund zu; wir werfen damit und landen im
    selben Zweig wie ein gescheiterter gamedig-Query, statt eine 0 zu
    erfinden. Und kein Ping: die Zahl ist eine gespeicherte Messung des
    Panels, keine Antwortzeit von hier.

EINE FALLE BEIM ZWEITEN PARAMETER: `servers.map(queryServer)` haette
`panelItems` den INDEX untergeschoben -- der erste Server saehe null,
alle weiteren eine Zahl, an der `panelEintrag` still scheitert. Also
`map((s) => queryServer(s, panelItems))`.

Probe erweitert und im neu gebauten Abbild gelaufen, alles gruen --
einschliesslich "null Spieler sind eine Antwort, kein Ausfall".
2026-08-16 12:01:24 +02:00
D4rkst3randClaude Opus 5 e39d4b33b6 Protokoll: melden, was Moderation ist -- nicht alles, was passiert
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Betreiber bekam "fuer alles einen Alert". Zu Recht: sechs verschiedene
Meldungen liefen in denselben Kanal, und dahinter stand ein einziger
Modulschalter -- entweder alles oder nichts.

  geloescht / bearbeitet / Nick+Rollen / beigetreten / gegangen / AutoMod

Zwei davon waren der eigentliche Laerm, und beide sind gar keine
Moderationsvorgaenge:

1. Jede selbst zurueckgenommene Nachricht wurde gemeldet. Wer seinen eigenen
   Tippfehler loescht, ist kein Vorfall.

2. Jede Rollenaenderung, die der Bot selbst ausgeloest hat -- Rollenmenue-Klick,
   Level-Aufstieg, Sticky-Restore nach Wiedereintritt, Playtester-Knopf. Der
   Bot hat sich selbst bei der Arbeit zugesehen und darueber Protokoll gefuehrt.

Beides wird jetzt gefiltert. Erkannt ueber Discords Audit-Log und nicht ueber
Merker an den vier Stellen, die Rollen vergeben -- so ist auch die fuenfte
erfasst, die jemand spaeter baut.

Dafuer musste werHatGeloescht() zuerst repariert werden. Sie gab fuer VIER
verschiedene Lagen dasselbe `null` zurueck: kein Server, kein Recht aufs
Audit-Log, Abfrage fehlgeschlagen, und "der Verfasser war es selbst". Zum
Anzeigen reichte das ("dann eben ohne Angabe"). Als Filter waere es die
schlimmste Sorte Fehler gewesen: fehlt dem Bot das Recht, saehe "selbst
geloescht" genauso aus wie "konnte nicht nachsehen" -- und das Protokoll
haette stillschweigend alles verschluckt, ohne dass es jemandem auffaellt.

Jetzt drei unterscheidbare Antworten: { wer } / { selbst } / { unklar, Grund }.
Verschwiegen wird nur, was sicher selbst geloescht wurde. Unklares wird
gemeldet, und der Grund steht im Embed, damit niemand die Zeile fuer "von
einem Mod geloescht" haelt. Fehlt das Recht, sagt das Log es einmal und dann
nicht wieder -- sonst waere die Meldung selbst wieder Rauschen.

Dazu vier Einzelschalter im Panel, weil "alles oder nichts" die Ursache war.
Vorgabe an: ein Protokoll, das nach einem Update stillschweigend aufhoert zu
protokollieren, waere die schlechtere Ueberraschung. Gelesen wird mit
!== '0', geschrieben werden sie einmalig als '1' -- das Panel liest
Feld-Schalter naemlich mit === '1', und ein Formular, das das Gegenteil
dessen zeigt, was gilt, ist schlimmer als gar keins.

Aufgepasst bei den Schaltern: an drei der Handler haengt mehr als nur die
Meldung. Begruessung und Auto-Rollen beim Beitritt, das Sichern der Rollen
beim Austritt, und das Nachfuehren des Nachrichten-Gedaechtnisses beim
Bearbeiten laufen weiter, auch wenn niemand die Meldung sehen will. Sonst
haette ein Protokoll-Schalter das Verhalten des Servers geaendert.

Geprueft mit gestellten Objekten, zwoelf Faelle: kein Server, kein Recht,
Abfrage wirft, kein Eintrag, Eintrag vom Autor selbst, Eintrag zu alt, Mod
war es -- und fuer die Rollen: Bot war es, Mensch war es, kein Eintrag, kein
Recht, Abfrage wirft. Im Zweifel wird immer gemeldet. Dazu die Schalter gegen
die Panel-Anzeige: beide sagen dasselbe.

Ungeprueft: der Lauf gegen ein echtes Audit-Log. Ob der Bot das Recht
"Audit-Log ansehen" hier ueberhaupt hat, steht nach dem Ausrollen im Log.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 17:16:14 +02:00
D4rkst3randClaude Opus 5 582d68580b Drei Befunde aus der Durchsicht: /health, Herunterfahren, ephemeral
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
1. /health konnte nicht fehlschlagen

   app.get('/health', async () => ({ status: 'ok' }));

Ein fester Wert, ohne irgendetwas anzusehen. Das meldet "sauber", solange
der HTTP-Faden laeuft -- und der laeuft auch dann noch, wenn die Verbindung
zu Discord weg ist. Ein Bot ohne Gateway ist fuer alle draussen genauso weg
wie ein abgestuerzter, nur merkt es niemand. NPM fragt die Adresse an, zwei
Hosts, alle paar Minuten; sie hat immer 200 gesagt.

Das Bittere: die Auskunft lag laengst vor. heartbeat.js schreibt alle zwei
Minuten client.isReady() mit (gemessen an der laufenden Datenbank: 9557
Zeilen, letzter Schlag ok=1, 104 ms). Der Herzschlag wusste es, /health hat
nicht gefragt.

Jetzt geprueft: steht die Verbindung, und antwortet die Datenbank. Sonst 503
mit Grund -- 503 und nicht 500, das ist kein Programmfehler, sondern ein
Zustand, der vorbeigeht. Der Grund steht dabei, sonst faengt das Raten wieder
von vorn an.

Dazu ein HEALTHCHECK im Dockerfile, denn sonst reagiert niemand darauf.
Gemessen: d4rkbot hatte keinen (<nil>), d4rk-media hat einen und steht auf
"healthy". curl und wget fehlen im slim-Abbild, also Node selbst; die
Exitcodes sind gegengeprueft (erreichbar -> 0, tote Adresse -> 1).

Fuer die Datenbankprobe eine echte Abfrage und nicht existsSync auf die
Datei: die Datei ist auch dann noch da, wenn das Dateisystem nur noch lesbar
ist.

2. Kein Handler fuers Beenden

Kein SIGTERM, kein SIGINT im ganzen Quelltext. Bei `docker stop` wird der
Prozess nach der Schonfrist abgeraeumt, und zwei Dinge geben danach falsche
Auskunft: der Bot steht in Discord noch eine Weile als online, und die
Statuszeile am Sprachkanal behauptet weiter, es liefe Musik -- die ueberlebt
den Container, der sie geschrieben hat.

Reihenfolge ist wichtig: erst das Radio, denn zum Abraeumen der Statuszeile
braucht es die Verbindung zu Discord noch. Dann Webserver, dann Discord.
Jeder Schritt einzeln abgesichert, sonst verhindert ein Fehler beim
Aufraeumen das restliche Aufraeumen. Nach acht Sekunden bricht es selbst ab,
weil Docker nach zehn schiesst.

radio_state bleibt dabei absichtlich stehen -- der Merker ist genau dafuer
da, dass der Bot nach einem Deploy von selbst zurueckkommt.

3. ephemeral: true

Einzige Stelle im ganzen Bot, ueberall sonst steht flags:
MessageFlags.Ephemeral. discord.js 14.27 wertet es noch aus
(InteractionResponses.js:118), warnt aber, und in v15 faellt es weg -- dann
waere ausgerechnet die Fehlermeldung ploetzlich oeffentlich.

Was die Durchsicht sonst ergab, gehoert genauso hierher: 125 Routen
durchgezaehlt, keine einzige aendernde ohne Wache. Keine Nutzereingabe in
SQL. Der Mod-Download prueft gegen die Modliste statt gegen ein Namensmuster.
Die Sicherung packt ihr Archiv wieder aus und laesst integrity_check laufen.
Keine Geheimnisse im Log. Daran war nichts zu verbessern.

Ungeprueft geblieben: 76 Stellen mit stillschweigend verschlucktem Fehler
(catch {}), das Frontend, und ob NPM schon drosselt -- eine
Ratenbegrenzung hat die API naemlich nicht. Entschaerft dadurch, dass alle
aendernden Routen hinter einer Wache liegen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 13:06:53 +02:00
D4rkst3randClaude Opus 5 1154e7a6a5 Radio: Lautstaerke im Panel -- und leise reinkommen statt erschrecken
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Anlass ist eine Beobachtung aus dem Betrieb: der Bot kam mit voller
Aussteuerung in den Kanal, und wer schon drin sass, ist erschrocken. Dass
sich jeder den Bot fuer sich leiser drehen kann, hilft erst *nachdem* es zu
laut war. Also liegt die Vorgabe jetzt bei 50 %, und lauter machen kostet
zwei Knopfdruecke.

Dafuer musste die Tonkette umgebaut werden. Bisher lief Opus unveraendert
durch (ffmpeg -> Ogg/Opus -> Discord). Das war eine Stufe sparsamer, aber an
fertigen Opus-Paketen laesst sich die Lautstaerke nicht drehen -- dafuer muss
man an die Abtastwerte. Jetzt: ffmpeg gibt PCM aus, die Sprachbibliothek
regelt und kodiert.

Der alte Einwand im Dateikopf ("sonst muesste @discordjs/opus mit ins Image,
und das will gebaut werden") stimmt weiterhin -- fuer @discordjs/opus.
Nachgemessen am 14.08.2026 in node:22-slim:

  @discordjs/opus  npm i bricht ab, kein Prebuild fuer node-v127/glibc-2.36,
                   node-gyp will bauen, im Image fehlen die Werkzeuge
  opusscript       reines JavaScript, installiert in 740 ms, kein Compiler
                   0,3 % eines Kerns fuers Kodieren eines Dauerstroms

Der Umweg ueber Opus-Dekodieren entfaellt dabei ganz, ffmpeg liefert ja schon
Abtastwerte. Deshalb 0,3 % und nicht 2,9 %, was Dekodieren und Kodieren
zusammen gekostet haetten.

Die Kette ist mit echtem ffmpeg gegen einen Sinuston geprueft: 200
Opus-Pakete fuer 4,0 s Ton, kein Versatz, und die Lautstaerke laesst sich
mitten im Strom aendern. Genau darum geht der Weg ueber PCM und nicht ueber
ffmpegs volume-Filter -- der haette bei jedem Klick einen Neustart des
Prozesses gebraucht, also eine hoerbare Luecke.

Am Panel zwei Knoepfe in der bestehenden Reihe, Schritte von 10, Bereich 10
bis 200. Am Anschlag graut der Knopf aus, statt sich druecken zu lassen und
nichts zu tun. Die Rueckmeldung ist die neu gezeichnete Tafel mit dem neuen
Prozentwert.

Der Wert steht in den Einstellungen, nicht in radio_state: der Merker dort
wird beim Stoppen geloescht, die Lautstaerke soll das ueberleben.

Dazu der Kanal-Status: was laeuft, steht jetzt auch in der Statuszeile des
Sprachkanals und wird mit dem Titel nachgefuehrt.

  ACHTUNG, das ist ein UNDOKUMENTIERTER Endpunkt.
  PUT /channels/{id}/voice-status -- Discord hat ihn nie in die API-Doku
  aufgenommen, die Pull Requests dazu liegen seit 2023 offen, und discord.js
  hat die Umsetzung als "not planned" geschlossen. Roher Aufruf ueber
  client.rest also, und er kann ohne Ankuendigung verschwinden.

Deshalb ist er als Beiwerk gebaut: faellt er aus, sagt er einmal warum und
schweigt dann -- ein Fehler alle 30 Sekunden waere kein Hinweis mehr,
sondern Rauschen. Das Radio laeuft in jedem Fall weiter. Der naechste
Sender-Start versucht es erneut, falls das Recht inzwischen erteilt wurde.

Das Recht heisst "Kanalstatus festlegen" und sitzt auf Bit 48. discord.js
kennt es nicht einmal als Konstante, deshalb steht die Zahl im Quelltext.
Ungeprueft, weil dafuer ein echter Sprachkanal noetig ist: ob das Recht dem
Bot hier tatsaechlich erteilt ist. Faellt es aus, steht der Grund im Log.

Beim Stoppen wird die Zeile geleert -- sonst behauptet sie stundenlang, es
liefe ein Lied, das laengst vorbei ist.

Nebenbei: prism-media wurde direkt importiert, stand aber nie in der
package.json. Der Import ist mit dem Ogg-Auspacker weggefallen.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:53:16 +02:00
D4rkst3randClaude Opus 5 f08a0e8b35 Radio: es war nie UDP -- Discord verlangt DAVE
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Mitschnitt von 12:33 hat es benannt:

  Netzteil: Websocket auf -> Anmeldung -> zu -> Websocket zu (4017) -> zu

4017 heisst DAVE. Discord erzwingt seit dem 01.03.2026 Ende-zu-Ende-
Verschluesselung in allen Nicht-Stage-Sprachkanaelen und weist Gegenstellen,
die das nicht koennen, sofort nach der Anmeldung ab -- lange bevor irgendein
UDP-Paket fliegt. Nachgeschlagen bei Discord, nicht gedeutet.

Gemessen, warum es uns traf: @discordjs/voice 0.18.0 kennt DAVE ueberhaupt
nicht (null Fundstellen im Bundle) und spricht Sprach-Gateway v4. Es meldet
deshalb max_dave_protocol_version: 0. 0.19.2 spricht v8, bringt
@snazzah/davey als feste Abhaengigkeit mit und hat 48 Fundstellen.

Also hochgezogen. Im gebauten Abbild gegengeprueft, weil das native Modul
plattformabhaengig ist und der Lock auf Windows entstanden ist:

  DAVE Libraries
  - @snazzah/davey: 0.1.12

@snazzah/davey-linux-x64-gnu steht im Lock, node:22-slim ist Debian/glibc,
das Modul laedt im Abbild. engines steht jetzt auf >=22.12.0 -- das verlangt
0.19.2, und ">=20" waere ab jetzt gelogen.

Und ein Fehler in der frischen Diagnose selbst, den erst der Ernstfall zeigte:
sie hat den Schritt nicht benannt, obwohl der Mitschnitt ihn enthielt. Das
Netzteil schliesst sich erst und meldet dann den Code, also endet jeder
Mitschnitt auf "zu" -- und genau den nahm die Auswertung als letzten Schritt.
Damit lief alles in "Ursache offen". Jetzt wird "zu" uebersprungen; die
Gegenprobe mit 4009 landet wieder beim UDP-Handschlag statt im Nichts.

Nebenbei stand "zu" zweimal im Mitschnitt, einmal vor und einmal nach dem
Schliesscode: die Entdopplung verglich mit dem letzten Eintrag statt mit dem
letzten Schritt.

Der Weg hierher gehoert zur Sache: erst hiess es "ausgehendes UDP dicht" --
geraten. Dann wurde gemessen, dass UDP rauskommt, und die Meldung sagte
ehrlich "Ursache offen". Diese eine ehrliche Meldung hat den Schliesscode
sichtbar gemacht, und der war die Antwort. Eine Pruefung, die nichts sehen
kann, meldet nicht "sauber" und auch nicht "UDP".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:39:52 +02:00
D4rkst3randClaude Opus 5 484f2edc62 Radio: den Schliesscode festhalten, statt UDP zu behaupten
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Die Meldung "ausgehendes UDP dicht" war geraten. Am 14.08.2026 im laufenden
Container gegengemessen, und sie stimmt nicht:

  UDP raus (DNS)              1.1.1.1:53                 4 ms
  UDP auf hohem Port (STUN)   stun.l.google.com:19302    9 ms
  TCP zum Sprachserver        c-fra13-...discord.media:2096  offen
  Verschluesselung            libsodium-wrappers 0.8.4 geladen

UDP kommt also raus, auch auf hohen Ports und mit Rueckweg durchs NAT. Der
Satz stand trotzdem da, weil "Weg enthaelt connecting" als Beweis genommen
wurde. Ist es nicht: in @discordjs/voice 0.18 faellt die Verbindung bei
*jedem* Websocket-Schluss ausser 4014 stumm nach "signalling" zurueck
(onNetworkingClose) und wirft den Schliesscode dabei weg. Ob der UDP-Teil je
begonnen hat, steht nirgends -- die Pruefung konnte den Unterschied nicht
sehen und hat trotzdem geurteilt.

Jetzt wird das Netzteil selbst mitgeschrieben: seine Schrittnummer
(NetworkingStatusCode, nachgeschlagen -- Websocket auf / Anmeldung /
UDP-Handschlag / Protokollwahl) und der Schliesscode. Nicht ueber `debug`,
denn das druckt Token und Sitzungsschluessel mit. Daraus folgt die Diagnose:

  stehen bei UDP-Handschlag  -> jetzt darf sie UDP sagen
  zu vor dem UDP-Teil        -> Sitzung (4006/4009), kein Netzproblem
  stehen bei Protokollwahl   -> Verschluesselung
  kein Mitschnitt            -> "Ursache offen". Nicht mehr "UDP".

Warum der UDP-Fall so aussieht, als haenge er: performIPDiscovery hat in
0.18 keine Zeitschranke, und die UDP-Keepalives werden nicht mehr
ausgewertet. Bleibt die Antwort aus, steht der Schritt, bis Discord den
Websocket zumacht.

Drei Sachen noch, die beim Nachsehen auffielen:

- Ein Fehler aus dem Netzteil hat den ganzen Prozess beendet. VoiceConnection
  reicht ihn als 'error' weiter, und ein EventEmitter ohne 'error'-Hoerer
  wirft. Genau dieser Weg ist im Fehlerfall offen -- die UDP-Erkennung meldet
  sich darueber. Jetzt gibt es einen Hoerer.
- schritte.endpunkt wurde bei jedem VOICE_SERVER_UPDATE ueberschrieben. Bei
  einem Rueckfall nennt Discord den Server erneut, also verdeckte die zweite
  Meldung den Endpunkt, an dem es scheiterte. Jetzt gesammelt, und fuer die
  Diagnose zaehlt der erste echte.
- tools/ landete nie im Abbild (COPY src ./src). Der Aufruf im Kopf von
  udp-pruefen.mjs -- "docker exec ... node tools/udp-pruefen.mjs" -- lief ins
  Leere. Jetzt kopiert das Dockerfile es mit.

Die neun Diagnose-Zweige sind gegen die exportierten Funktionen im Container
durchgespielt, nicht gegen eine Kopie.

Was weiter offen bleibt: eine Sperre speziell auf Zielports 50000-65535 ist
nicht widerlegt -- dafuer fehlt eine Gegenstelle, die dort antwortet. Der
naechste Fehlversuch sagt es von selbst.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-14 12:31:27 +02:00
D4rkst3randClaude Opus 5 228853febd Radio: die zweite Sackgasse von der ersten trennen — und UDP messbar machen
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Aus dem Betrieb: alle drei Beitrittsschritte durch, trotzdem "signalling".
Das ist kein Widerspruch. In @discordjs/voice gibt es zwei Wege dorthin:

  - `configureNetworking` steigt aus, wenn der Endpunkt leer ist. Discord
    schickt das bei einem Regionswechsel; kommt keine zweite Meldung, bleibt
    die Verbindung fuer immer stehen.
  - Scheitert der Austausch mit dem Sprachserver, wirft die Bibliothek die
    Verbindung nach "signalling" zurueck und faengt von vorn an. Von aussen
    sieht das genauso aus.

Deshalb schreibt der Adapter jetzt den Endpunkt mit, und die Verbindung
protokolliert ihren Weg. War sie einmal bei "connecting" und ist
zurueckgefallen, ist es der UDP-Austausch — dann steht der Endpunkt in der
Meldung, damit man ihn von Hand pruefen kann.

Dazu tools/udp-pruefen.mjs: im Container aufrufen und es sagt, ob UDP
ueberhaupt rauskommt und ob auch auf hohen Ports. Genau das unterscheidet
"Firewall zu" von "Firewall laesst nur die ueblichen Ports durch" — und
Discord-Sprache braucht die hohen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:15:14 +02:00
D4rkst3randClaude Opus 5 bbb56e1276 Radio: den Beitritt in drei Schritte zerlegen statt "signalling" zu melden
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
"signalling" sagt nur, dass etwas fehlt, nicht was. Ein Beitritt zum
Sprachkanal ist ein Dreischritt:

  1. Der Bot bittet Discord ueber die bestehende Verbindung um den Beitritt.
  2. Discord antwortet mit dem eigenen Sprachstatus (darin die Sitzungs-ID).
  3. Discord nennt den Sprachserver.

Der Adapter zwischen Verbindung und Sprachbibliothek haelt jetzt fest,
welcher davon stattgefunden hat. Aus den drei Haekchen wird ein Satz, der
sagt, was zu tun ist — fehlende Berechtigung, fehlender Intent, oder ein
Netz, das UDP nicht rauslaesst. Fuenf Faelle, fuenf verschiedene Antworten;
der Test besteht darauf, dass keine zwei gleich lauten.

Dazu eine Pruefung vorweg, die genau dasselbe Bild erzeugt haette: ein
voller Sprachkanal. Discord nimmt die Anfrage entgegen und antwortet dann
einfach nicht mehr — das sieht aus wie ein Rechteproblem. Wer Mitglieder
verschieben darf, kommt am Limit vorbei, sonst nicht.

Nachgesehen und ausgeschlossen: discord.js 14.27 verdrahtet beide
Sprachereignisse (VOICE_STATE_UPDATE ueber die Action, VOICE_SERVER_UPDATE
ueber den Handler), und GuildVoiceStates steht in den Intents.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 12:09:59 +02:00
D4rkst3randClaude Opus 5 e246dadbec 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>
2026-08-14 12:00:14 +02:00
D4rkst3randClaude Opus 5 4d71122820 Radio: Schalter dorthin, wo man ihn sucht — und Stille sagt jetzt warum
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Drei Sachen, alle vom selben Bericht ausgeloest.

1. Der Schalter war nur im Modul-Reiter zu finden, die Meldung schickte aber
   nach "Community". Beim Nachsehen kam heraus: Modul-Schalter haben in
   /api/settings gar keinen Weg — jeder stand von Hand in api.js, und fuenf
   fehlten dort (radio, automod, anti_raid, linked_roles, ls_farm). Die
   liessen sich nur ueber den Modulreiter umlegen. Jetzt kommen sie wie
   Kanaele, Rollen und Texte aus dem Register; jeder neue ist damit von
   allein speicherbar. Dazu steht der Haken jetzt oben im Radio-Abschnitt,
   wo auch die Sender stehen.

2. Der Bot sass im Sprachkanal und schwieg. Ursache im Code: nach
   joinVoiceChannel wurde sofort losgespielt. Der Aufruf kommt zurueck,
   sobald der Bot sichtbar im Kanal steht — der UDP-Weg wird danach erst
   ausgehandelt. Wer vorher sendet, sendet ins Leere. Jetzt wird auf
   VoiceConnectionStatus.Ready gewartet, und schlaegt das fehl, sagt der
   Befehl es statt still dazustehen.

3. Stille erklaert sich. Fehlt ffmpeg im Container, setzte sich der Bot
   bisher hin und schwieg — nichts stuerzte ab, nichts fehlte sichtbar.
   ffmpeg wird jetzt beim Start geprueft, /radio verweigert mit klarer
   Ansage, und kommt binnen acht Sekunden kein Ton, steht der Grund in der
   Tafel: fehlendes ffmpeg oder ein Sender, der nichts liefert.

Nebenbei: -analyzeduration stand hinter -i und wurde damit als
Ausgabe-Option gelesen, also ignoriert.

Geprueft: die Tonkette laeuft lokal gegen einen echten Sender durch bis zum
Abspieler (4,1 s Ton in 6 s Laufzeit, Status bleibt playing). Alle 16
Modul-Schalter gehen angemeldet durch /api/settings hin und zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:54:49 +02:00
D4rkst3randClaude Opus 5 89f6dfcd29 Radio: Webradio im Sprachkanal, 44 Sender zum Durchschalten
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
/radio holt den Bot in den Sprachkanal und postet eine Tafel: Auswahlmenue
mit allen Sendern, Stopp, und ein Embed mit dem laufenden Titel, das sich
alle 30 Sekunden selbst nachfuehrt.

Der Titel kommt aus dem Icecast-Strom selbst (Icy-MetaData). Bewusst so und
nicht ueber eine Sender-API: das funktioniert bei jedem Strom, den jemand
eintraegt, und nicht nur bei den drei, die ich kenne. Leere Titel-Bloecke
heissen "unveraendert", deshalb werden bis zu drei gelesen.

Die Tonkette braucht keine Opus-Bibliothek: ffmpeg holt das MP3 und gibt
direkt Ogg/Opus aus, prism-media packt nur noch aus. Gemessen: 600 Pakete in
fuenf Sekunden. Sonst muesste @discordjs/opus mit ins Image, und das will
gebaut werden. Neu im Image ist nur ffmpeg.

44 RauteMusik-Sender sind ab Werk eingetragen — dieselben, die im LS25 und
ETS als Bordradio laufen. Die Liste ist nicht abgeschrieben, sondern
gemessen: jede Adresse einzeln angefragt, der Name kommt aus icy-name.
Doppelgaenger sind raus (deutschrap-charts und wackenradio liefern denselben
Strom wie deutschrap und metal). Eingetragen wird einmal, solange die Liste
leer ist — wer loescht, behaelt es geloescht.

Ein Auswahlmenue fasst nur 25 Eintraege. Bei 44 Sendern haette ein Deckel
die letzten 19 stillschweigend verschluckt; stattdessen mehrere Menues,
benannt nach ihrem ersten und letzten Sender.

Der Bot geht raus, wenn der Kanal leer ist, und kommt nach einem Neustart in
den Kanal zurueck — sonst beendet jeder Deploy die Musik endgueltig.

Kein YouTube. Genau daran sind Groovy und Rythm gestorben, und was davon
uebrig ist, ist ein Dauerlauf gegen kaputte Extraktoren.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-14 11:40:15 +02:00
D4rkst3randClaude Opus 5 b7f54185f7 fix: ein schon von Hand eingetragener Server wird uebernommen, nicht uebergangen
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Betreiber hat im Panel den Schalter fuer atm11-test umgelegt und im
Discord passierte nichts. Er hat den Fehler bei sich gesucht -- ob er den
Namen falsch geschrieben habe. Hat er nicht: der Server stand schon von
Hand im Monitor, weil ich ihn vorhin selbst dort eingetragen hatte, und
der Abgleich uebersprang ihn mit `continue`, damit nicht zwei Embeds fuer
denselben Server im Kanal stehen.

Das Ueberspringen war richtig, das Schweigen nicht. Wer den Schalter
umlegt, sagt "dieser Server gehoert ans Panel" -- also bekommt der
bestehende Eintrag jetzt den Herkunftsvermerk, statt ignoriert zu werden.
Ein zweites Embed entsteht dabei nicht.

Der Preis gehoert benannt und wird mitgeprueft: ab der Uebernahme nimmt
ein Ausschalten den Eintrag mit, samt Bild und Links, die von Hand daran
haengen. Das ist der Sinn des Schalters, aber es ist eine Uebertragung
von Besitz.

Ueberschrieben wird weiterhin kein Feld -- die Uebernahme setzt nur
panel_name.

Und die Meldung im Protokoll unterscheidet jetzt "neu" von "uebernommen";
vorher hiess beides "uebernommen", was genau die Auskunft verwischt haette,
um die es hier geht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 19:13:58 +02:00
D4rkst3randClaude Opus 5 0982e8cd1b feat: Server aus dem Panel uebernehmen -- und nur die auch wieder loeschen
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Im d4rk_gameserver-Panel bekommt jeder Server einen Schalter "Im Discord
anzeigen". Das Panel schreibt dafuer NICHTS hierher: es setzt nur ein
Haekchen, und das reist in dem Statusbericht mit, den der Monitor ohnehin
alle 30 Sekunden abholt. Der Bot legt den Eintrag dann selbst an.

Der umgekehrte Weg -- Panel ruft unsere API -- braeuchte dort ein
Zeichen mit Schreibrecht. So bleibt es bei einem, das nur lesen kann.

ZWEI FALLEN, beide beim Bauen aufgefallen:

Der Abgleich lief zu spaet. In monitorTick stand `if (servers.length ===
0) return` VOR dem Panel-Abruf; beim allerersten Server haette der
Schalter also nichts getan, und zwar stillschweigend. Der Abruf steht
jetzt davor.

Loeschen braucht einen Besitzvermerk. Ohne den nimmt ein Haekchen im
Panel den handgepflegten ATM10 mit -- die einzige verfuegbare Grundlage
waere der Name, und beim ersten Server, der in beiden Werkzeugen gleich
heisst, waere der Eintrag samt Bild, Links und Zugangs-Code weg. Neue
Spalte `panel_name`, leer heisst "von Hand". Die UPDATE-Anweisung fasst
sie nicht an, damit ein Bearbeiten im Webinterface die Herkunft behaelt.

Und der Abgleich ueberschreibt nichts: angelegt und geloescht wird, mehr
nicht.

tools/panel-abgleich-pruefen.mjs prueft das auf einer EIGENEN Datenbank
in /tmp und bricht ab, wenn sie es nicht ist -- eine Probe, die loeschen
kann, darf nicht dort loeschen, wo es zaehlt. Alles gruen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 19:08:43 +02:00
D4rkst3randClaude Opus 5 21eab627b9 fix: die Panel-Anbindung steht jetzt dort, wo man sie sucht
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der Betreiber hat sie nicht gefunden, und das zu Recht: die zwei Felder
gab es nur auf der Modul-Detailseite unter Module -> Game-Server-Monitor.
Der Reiter "Game-Server" hat aber einen eigenen, handgeschriebenen Block
mit Status- und Alarm-Kanal -- und genau dort schaut nach, wer den
Monitor einrichtet.

Also stehen sie jetzt auch dort, direkt unter den Kanaelen, mit dem
Absatz, der erklaert wofuer das gut ist. Die Modulseite behaelt sie; es
sind dieselben Formularwerte, keine zweite Wahrheit -- genauso wie
status_channel_id schon an beiden Stellen steht.

Und ins Suchregister (setting-index.js), sonst findet die Suche in der
Seitenleiste das Feld nicht und man sucht es mit den Augen. Das Register
traegt selbst den Kommentar, dass ein vergessener Eintrag genau so
endet.

Geprueft, indem die Frontend-Stufe des Images gebaut und im Ergebnis
nachgesehen wurde -- "Panel-Adresse", "Panel-Zeichen" und
"Panel-Anbindung speichern" stehen im ausgelieferten Bundle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-13 18:38:52 +02:00
D4rkst3r d6124fc2cd feat: der Monitor fragt das Panel, wenn eine Abfrage nicht ausreicht
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
gamedig FRAGT einen Server. Ein startendes Modpack antwortet nicht, und
nach zwei Fehlversuchen stand im Alarm-Kanal eine Ausfallmeldung, obwohl
niemand etwas kaputtgemacht hat -- am ATM10 des Betreibers gemessen jedes
Mal 306 Sekunden Fehlalarm.

Das d4rk_gameserver-Panel SIEHT den Container, statt ihn zu fragen. Neu
ist deshalb `src/panel.js`: Zustand, Fertig-Merkmal, RAM und CPU kommen
als Zusatzauskunft dazu, einmal je Durchlauf abgerufen und 20 Sekunden
zwischengespeichert.

Was das im Alarm-Kanal aendert:

  startet gerade   kein Alarm, aber nur innerhalb einer Gnadenfrist von
                   15 Minuten. Ohne diese Grenze fraesse die Anbindung
                   genau den Alarm, fuer den es sie gibt -- `bereit`
                   wird nie von allein wahr, ein haengender Start bliebe
                   sonst fuer immer stumm.
  im Panel gestoppt  kein Alarm (Code 0/143/137)
  abgestuerzt      ALARM, mit Code im Embed
  OOM-Kill         ALARM, und beim Namen genannt statt als Absturz
                   getarnt -- bei Modpacks die haeufigste Ursache
  Container weg    ALARM

Der Zaehler wird beim Unterdruecken NICHT zurueckgesetzt: ein bereits
gemeldeter Ausfall bleibt gemeldet, sonst verschluckt ein Stopp im Panel
die spaetere "wieder online"-Entwarnung und im Kanal bliebe ein Alarm
ohne Aufloesung stehen.

ES IST DURCHGEHEND OPTIONAL. Adresse und Zeichen stehen im Webinterface
unter "Server" und NICHT in der .env; ohne Eintrag -- und ebenso, wenn
das Panel nicht antwortet -- verhaelt sich der Monitor exakt wie vorher.
Das Zeichen darf nur lesen, damit auch ein verlorenes niemandem einen
Server stoppen kann.

`tools/panel-pruefen.mjs` prueft die Logik ohne Discord und ohne
Datenbank; deshalb laedt panel.js die Einstellungen erst beim Aufruf.
2026-08-13 18:05:24 +02:00
D4rkst3randClaude Opus 5 bce75f8c65 docs: Befund 1 geschlossen -- die Sicherung verlaesst die Maschine
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
backup_channel_id ist gesetzt. Vorher nachgesehen, ob der Bot den Kanal auch
bedienen KANN -- ein eingetragener Kanal, in dem er nicht posten darf, sieht
von aussen genauso aus wie ein richtig eingetragener:

    Kanal  #backup-upload-kanal   Textkanal, richtige Gilde
    Bot    D4rk DevBot            VIEW_CHANNEL, SEND_MESSAGES, ATTACH_FILES

Dann von Hand ausgeloest, und der ganze Weg lief durch -- inklusive der heute
gebauten Gegenpruefung:

    [backup] d4rkbot-2026-08-12.db.gz erstellt (1021 KB),
             geprueft: 55 Tabellen, 89 Einstellungen
    last_backup 2026-08-12T13:54:44Z, last_backup_ok 1, kein Fehler

Der Zaehler der Einstellungen ist zwischen zwei Laeufen von 87 auf 89
gewachsen: das sind die Laeufe selbst, die last_backup schreiben. Ein huebscher
Beleg, dass wirklich das neue Archiv geoeffnet wurde und nicht ein altes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:55:41 +02:00
D4rkst3randClaude Opus 5 6057bdb0d7 docs: Befund 1 -- was gebaut wurde und was ausserhalb des Repos liegt
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Die Abholung laeuft taeglich um 03:30 -- nach der Sicherung des Bots (ab 03:00)
und vor der von d4rk_media (04:30), damit nicht zwei Vorgaenge gleichzeitig auf
derselben Platte arbeiten.

Ueber die geplante Aufgabe gemessen, nicht von Hand: Ergebnis 0, Archiv auf
Platte Nr. 1 statt Nr. 0, Pruefsumme identisch, 55 Tabellen und 86
Einstellungen im ausgepackten Archiv, last_zweitziel steht in der Datenbank.

Zwei Dinge liegen ausserhalb des Repos und stehen deshalb im Bericht, damit sie
nach einem Neuaufsetzen nicht fehlen: die Arbeitskopie unter
Desktop\d4rkbot (vorher gab es gar keine, der Bot lief nur aus Portainers
Checkout) und die Aufgabe "d4rkbot Sicherung abholen". Sie startet pwsh ueber
den stabilen Ausfuehrungsalias und nicht ueber den Pfad mit Versionsnummer --
genau daran hing bei d4rk_media eine Sicherung, die beim naechsten
PowerShell-Update aufgehoert haette zu laufen.

Offen bleibt: beide Kopien liegen im SELBEN RECHNER. Gegen einen Plattenausfall
hilft das jetzt, gegen Feuer oder eine verschluesselte Maschine nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:19:22 +02:00
D4rkst3randClaude Opus 5 720785c175 backup: die Sicherung verlaesst das Volume -- tools/abholen.ps1 plus Wachhund
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Befund 1 aus docs/befunde-2026-08-12.md, der wichtigste: Datenbank und alle
vierzehn Sicherungen lagen im selben Volume. Geht das Volume verloren, geht
beides zusammen verloren -- das ist keine Sicherung, das ist eine Kopie.

tools/abholen.ps1 SICHERT NICHT. Das Archiv baut der Bot selbst; hier wird nur
abgeholt, und zwar so, dass am Ende feststeht, dass die Kopie heil ist und
nicht nur, dass ein Kopierbefehl keinen Fehler geworfen hat:

  1. neuestes Archiv im Container finden (und meckern, wenn es aelter als
     24 Stunden ist -- ein Skript, das treu das Archiv von vorletzter Woche
     abholt und "fertig" sagt, ist schlimmer als eines, das gar nicht laeuft)
  2. docker cp heraus
  3. Pruefsumme IM CONTAINER gegen die Kopie hier -- nicht die Dateigroesse:
     eine abgebrochene Kopie auf eine volle Platte hat oft genau die richtige
     Laenge und trotzdem Nullen am Ende
  4. auspacken und die Datenbank darin oeffnen, mit dem Bot-Abbild, weil dort
     better-sqlite3 schon liegt; integrity_check plus Tabellen zaehlen
  5. Bericht als last_zweitziel zurueck in die Einstellungen
  6. ausduennen

Und es warnt, wenn es DIESELBE PLATTE ist. Verglichen wird die physische Platte
und nicht der Laufwerksbuchstabe: zwei Partitionen derselben NVMe sterben
zusammen.

Echter Lauf, kein Trockentest:

    d4rkbot-2026-08-12.db.gz   993.332 Bytes
    andere Platte: Nr. 1 statt Nr. 0
    identisch (ff08516d293d...)
    in der Sicherung: 55 Tabellen, 86 Einstellungen

DAZU EIN ZWEITER WACHHUND, pruefeZweitziel(). Ein Skript auf dem Wirt, das
still aufhoert zu laufen -- Aufgabe deaktiviert, Laufwerk weg, Pfad geaendert --
waere derselbe Schaden noch einmal, nur eine Ebene weiter aussen. Er meldet nur,
wenn die Abholung schon einmal lief: fehlt der Eintrag ganz, ist sie nicht
eingerichtet, und daraus taeglich eine Meldung zu machen waere Naergelei.

Gegen eine Kopie der Datenbank durchgespielt:

    40 h alt   -> "alt:17863965"
    nochmal    -> unveraendert          (keine Wiederholung)
    Fehlschlag -> "fehler:1786540570974"
    wieder gut -> ""                    (Entwarnung)

Nebenbei gelernt, zum zweiten Mal in diesem Projekt: die Kopie der Datenbank
ohne -wal war leer an der Stelle, die zaehlte -- last_zweitziel stand noch im
Write-Ahead-Log. Dieselbe Falle wie beim Zurueckspielen von d4rk_media.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:17:01 +02:00
D4rkst3randClaude Opus 5 40ede9c56a docs: Richtigstellung -- der Auto-Deploy funktioniert, ich war zu ungeduldig
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Zwei falsche Diagnosen hintereinander, beide aus einem Zwischenstand
geschlossen, waehrend der Vorgang noch lief. Was wirklich passiert ist:

    12:48:18   Push 93156e5
    12:48      Portainer hat die Dateien im Stack-Verzeichnis
    12:57      hier gemessen: Abbild 15 h alt, Container von gestern
               -> daraus "er baut nicht" geschlossen. FALSCH.
    13:01:02   Abbild neu gebaut
    13:01:14   Container neu gestartet
    13:02      pruefeSicherung im Container: 2, melden.js da, Panel HTTP 200

Zwischen Push und laufendem neuen Code liegen rund DREIZEHN MINUTEN -- Portainer
fragt in Intervallen nach, nicht sofort. Die Messung um 12:57 fiel mitten in
dieses Fenster: die Dateien waren schon da, das Abbild noch nicht neu. Aus dem
Schnappschuss habe ich einen Systemfehler gemacht.

Das ist genau der Fehler, gegen den der Bericht geschrieben ist -- eine
plausible Erklaerung, die zum Schnappschuss passt, behauptet, bevor der Vorgang
zu Ende war.

pull_policy: build bleibt drin, aber NICHT mehr aus dem Grund im Commit davor.
Es repariert nichts; es macht das Neubauen nur ausdruecklich, damit ein
handgetipptes `docker compose up -d` dasselbe tut wie die Automatik. Folgenlos
entfernbar.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:03:18 +02:00
D4rkst3randClaude Opus 5 b98f4e15c9 fix: pull_policy build -- der Auto-Deploy zog, baute aber nie
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Der erste Verdacht ("kein Runner") war die halbe Wahrheit. Portainers
GitOps-Weg funktioniert, nachgemessen in seinem eigenen Datenverzeichnis:

    /d/compose/1/src/melden.js     12:48   <- die Uhrzeit des Pushes
    /d/compose/1/stack.env         12:57   <- Portainer hat neu geschrieben
    docker images d4rkbot:latest   00:21   <- das Abbild 15 Stunden alt

Portainer holt den neuen Stand also brav ins Stack-Verzeichnis und ruft dann
docker compose up -d. UND DAS BAUT NUR, WENN DAS ABBILD FEHLT. d4rkbot:latest
gab es -- also kein Bau, kein neuer Container, und der Bot lief weiter mit dem
Code von vorgestern.

Kein Runner, kein Webhook, keine Fehlermeldung: es sah nach "nichts zu tun"
aus. Das ist die unangenehme Sorte Fehler -- man pusht, hakt es ab, und der
Fehler, den man gerade behoben hat, laeuft weiter.

pull_policy: build laesst compose bei jedem Ausrollen neu bauen. Geprueft mit
docker compose config (v5.3.1 nimmt es an und gibt es aufgeloest zurueck).

Einmal noch von Hand, denn die Zeile wirkt erst, wenn sie selbst ausgerollt
ist: Portainer -> Stack ecobot -> Pull and redeploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:00:51 +02:00
D4rkst3randClaude Opus 5 b63d4cae3b docs: der Auto-Deploy hat nicht ausgeloest -- gemessen, nicht vermutet
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Nach dem Push von 93156e5 nachgesehen, ob der Bot den neuen Stand bekommt:

    Push registriert          updated_at 12:48:18Z
    Workflow-Laeufe           "total_count": 0     <- keiner, nie
    Act-Runner auf dem Wirt   kein Container
    d4rkbot neu gestartet?    nein, laeuft seit 11.08. 22:21
    Code im Container         grep pruefeSicherung -> 0

has_actions ist am Repo AN, es gibt also nur niemanden, der die Laeufe
abarbeitet -- und der Webhook-Weg, der ohne Runner auskaeme, ist offenbar auch
nicht eingerichtet.

Pushen allein rollt also nichts aus. Das gehoert aufgeschrieben, weil ein
Deploy, von dem man GLAUBT dass er laeuft, schlimmer ist als gar keiner: man
pusht, hakt es ab, und der Fehler, den man gerade behoben hat, laeuft weiter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:57:41 +02:00
D4rkst3randClaude Opus 5 93156e5205 backup: gegenpruefen, nachholen, und endlich Bescheid sagen
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Befund 2, 3 und 4 aus docs/befunde-2026-08-12.md. Befund 1 bleibt offen, Befund
5 war falsch und ist zurueckgezogen.

DER BOT SAGT JETZT BESCHEID. src/melden.js ist neu und enthaelt, was vorher in
watchdog.js eingeschlossen war: dmAdmin. Genau deshalb hat alles andere im Bot
geschwiegen oder in die Konsole geschrieben -- und eine Konsolenzeile in einem
Container liest niemand.

Dazu dmAdminEinmalig: meldet nur bei ZUSTANDSWECHSEL. Sonst wuerde eine
Pruefung, die alle zehn Minuten laeuft, denselben Ausfall alle zehn Minuten
melden, und nach der dritten DM liest man sie nicht mehr. Der Merker steht in
den Einstellungen und nicht im Arbeitsspeicher -- ein Bot, der nach jedem
Update neu startet, haette sonst nach jedem Update wieder eine frische Meinung.
Gemessen gegen eine KOPIE der Datenbank, sechs Schritte, alle wie beabsichtigt.

DER WACHHUND AUF last_backup. pruefeSicherung() meldet, wenn die letzte
Sicherung aelter als 26 Stunden ist, wenn sie fehlgeschlagen ist, oder wenn es
nie eine gab. Im Fehlerfall steht NICHT last_backup als Zeitpunkt im Embed: der
wird nur bei Erfolg gesetzt, dort staende also der letzte GUTE Lauf, und das
liesse die Sicherung frischer aussehen als sie ist.

DER TAG FAELLT NICHT MEHR AUS. Vorher "if (now.getHours() !== 3) return" -- wer
waehrend dieser einen Stunde unten war, hatte den Tag verloren, und ein Bot,
der nach jedem Update neu startet, ist genau dieser Fall. Jetzt zaehlt nur: es
ist nach 03:00 und heute war noch keine.

DAS ARCHIV WIRD AUSGEPACKT UND GEZAEHLT. integrity_check plus Tabellenzahl
gegen die laufende Datenbank. Faellt das durch, wird das Archiv GELOESCHT (im
Sicherungsordner saehe es sonst aus wie eine Sicherung), die Rotation laeuft
nicht, und last_backup bleibt stehen.

An echten Archiven gemessen, mit zwei Gegenproben, damit die Pruefung nicht nur
"ja" sagen kann:

    2026-08-10  817 KB  54 Tabellen  86 ms
    2026-08-11  886 KB  54 Tabellen  64 ms
    2026-08-12  970 KB  55 Tabellen  78 ms   (laufend: 55)
    halbes gzip -> "unexpected end of file"
    Muell       -> "incorrect header check"

Die 54 gegen 55 sind kein Fehler, sondern eine gewachsene Tabelle zwischen dem
11. und dem 12. Verglichen wird zeitgleich, also stoert das nicht -- wissen
sollte man es, bevor jemand alte Archive gegen die heutige Zahl haelt.

BEFUND 5 WAR FALSCH. pruneMessageCache wird sehr wohl aufgerufen, taeglich, in
mod-tools.js:239 -- und stand schon zum Zeitpunkt der Durchsicht dort. Gezaehlt
wurden "Aufrufe ausserhalb von db.js", und die Sammel-Importzeile ist als
Import durchgegangen, waehrend der Aufruf zwanzig Zeilen tiefer nicht mitkam.
Das ist genau der Fehler, den zu vermeiden dieser Bericht dasteht; die anderen
vier sind deshalb einzeln nachgemessen worden und stimmen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 14:47:09 +02:00
D4rkst3randClaude Opus 5 1808b7c40b docs: Durchsicht -- die Sicherung liegt neben dem Original, und niemand merkt ihren Ausfall
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
Gesucht wurde nach denselben Fehlermustern, die beim Bau von d4rk_media
aufgefallen sind. 24105 Zeilen liest niemand am Stueck; gesucht wurde gezielt
nach nie aufgerufenen Aufraeumfunktionen, Wachen an '*', SQL mit eingesetzten
Zeichenketten, fetch auf fremde Adressen, ungelesenen Antwortkoerpern,
Schleifen mit Zeitlimit und Sicherungen ohne Gegenprobe.

AN DIESEM BOT WURDE NICHTS GEAENDERT. Die Entscheidungen gehoeren dem
Betreiber.

DER WICHTIGSTE BEFUND: Datenbank (5,0 MB) und alle vierzehn Sicherungen
(5,7 MB) liegen im SELBEN Volume ecobot_ecobot_data. Geht es verloren, geht
beides zusammen verloren -- das ist keine Sicherung, das ist eine Kopie. Der
eingebaute Ausweg (Upload in einen privaten Discord-Kanal) laeuft nie, weil
backup_channel_id nicht gesetzt ist.

DAZU: niemand prueft last_backup. Es wird geschrieben und im Panel ANGEZEIGT,
aber nichts vergleicht es mit heute. Und weil der Zeitplan auf getHours() === 3
steht, faellt der Tag still aus, wenn der Bot waehrend dieser Stunde unten ist
-- bei einem Bot, der nach jedem Update neu startet, kein Sonderfall. Die
Maschinerie dafuer ist vollstaendig da (dmAdmin, brandEmbed, Incidents) und
wird nicht benutzt.

WEITER: "zu gross fuer Discord" endet in einem console.warn, das niemand liest;
die Archive wachsen um rund 70 KB je Tag auf eine Grenze von 9 MB zu. Das
Archiv wird nie ausgepackt und gegengeprueft. Und pruneMessageCache wird
nirgends aufgerufen -- exakt dasselbe Muster wie pruneEvents in d4rk_media,
heute mit 18 Zeilen harmlos.

WAS GEPRUEFT WURDE UND IN ORDNUNG IST, damit es niemand ein zweites Mal prueft:
db.backup() statt Dateikopie ist richtig und WAL-sicher; das gefaehrlich
aussehende ORDER BY ${ord} ist eine feste Weissliste; die oeffentliche
Serverliste zaehlt ihre Felder einzeln auf; die zwei bekannten Geheimnisse
gehen nur als Ja/Nein hinaus; der Spielserver-Monitor nutzt bereits
Promise.all.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 01:26:36 +02:00
D4rkst3randClaude Opus 5 5ac079699c watchdog: sagen WARUM, nebeneinander pruefen, Gespenster aufraeumen
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
DER GRUND STEHT JETZT DABEI. Bisher landete im Ausfall-Embed nur "HTTP 503".
Das sagt, DASS etwas ist, nicht WAS -- und wer nachts eine DM bekommt, will
genau das wissen, bevor er sich einloggt.

Viele Dienste sagen es naemlich selbst. d4rk_media antwortet auf /status mit
einer 503 UND einer Begruendung, und die geht jetzt mit in die Meldung UND in
die Stoerung (startIncident nimmt einen Grund entgegen -- damit steht er in der
Historie der oeffentlichen Statusseite und nicht nur einmalig in einer DM):

    vorher   HTTP 503
    jetzt    HTTP 503 — sicherung: letzte vor 40 Stunden

Gemessen am laufenden Dienst, nicht ausgedacht.

Gelesen wird nur bei content-type JSON und hoechstens 64 KB; der Satz wird bei
240 Zeichen gekappt. Erkannt werden erst unsere eigene Form (eine Liste von
Pruefungen, die durchgefallenen werden genannt), dann die ueblichen Felder
error/message/reason/grund/detail, zuletzt ein blosser Zustand wie "degraded".
Wer nichts davon liefert, bekommt weiterhin "HTTP nnn" -- kein Fehler, nur
keine Zusatzauskunft. Zehn Faelle durchgeprueft, darunter kaputtes JSON, kein
Objekt und null.

DAS URTEIL BLEIBT DER STATUSCODE. Ein Dienst, der 200 mit "ok": false
antwortet, gilt weiterhin als erreichbar -- alles andere waere eine stille
Verhaltensaenderung fuer jede andere ueberwachte Adresse.

Nebenbei bekommt der Rumpf damit endlich eine Behandlung: er wird gelesen oder
verworfen. Ein fetch, dessen Antwort niemand anfasst, haelt die Verbindung, bis
der Aufraeumer sie holt.

NEBENEINANDER STATT NACHEINANDER. Die Schleife lief der Reihe nach, und jede
Adresse durfte bis zum Zeitlimit brauchen: bei fuenf Diensten und zehn Sekunden
Grenze konnte ein Durchlauf fast eine Minute dauern. Bei zwei Minuten Intervall
ist das knapp, bei zehn Diensten ueberholt sich der Waechter selbst. Geprueft
wird jetzt gleichzeitig, AUSGEWERTET weiter der Reihe nach -- die DMs sollen in
nachvollziehbarer Ordnung ankommen und nicht in der, in der zufaellig
geantwortet wurde.

GESPENSTER AUFRAEUMEN. `state` wurde nie aufgeraeumt: ein aus der Dienst-Tabelle
geloeschter Eintrag blieb bis zum Neustart im Speicher und damit in der
Uebersicht im Panel stehen. Was nicht mehr ueberwacht wird, fliegt jetzt am Ende
jedes Durchlaufs raus.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 00:19:58 +02:00
D4rkst3randClaude Opus 5 1ef06f747c Modliste: melden was sich geaendert hat, einzeln laden
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Bisher gab es fuer Mods nur einen Link auf das Sammelpaket des Hosters — vier
Gigabyte, auch wenn sich zwei Dateien geaendert haben. Jetzt liest der Bot die
Modliste aus der Statusabfrage, die er ohnehin holt, und meldet im Kanal, was
neu, aktualisiert oder entfernt wurde. Beim ersten Durchlauf bleibt es still,
sonst kaeme eine Meldung ueber 110 "neue" Mods.

Geaendert heisst: andere Version oder anderer Hash. Beides einzeln reicht
nicht — manche Modder bessern nach, ohne die Version zu erhoehen.

Dazu die Seite /mods/<server>: jeder Mod mit Version und Groesse, Suche ueber
Titel, Dateiname und Autor, und ein Download je Zeile. Der laeuft durch den
Bot, damit der Zugangs-Code des Spielservers nicht in einem Link landet — mit
dem Code liesse sich auch der ganze Spielstand lesen. Angefragt wird nur, was
wirklich in der Modliste steht; ueber den Dateinamen kommt man an nichts
anderes heran.

Groessen lernt der Bot beim Weiterleiten. Der Spielserver beantwortet kein
HEAD (501) und ignoriert Range, es gibt also keinen billigen Weg, sie vorher
zu erfahren — und 4 GB nur fuers Anzeigen zu holen waere keiner.

Nebenbei gefunden und behoben: 'server.mods' und 'commands.tag' gab es je
zweimal in den Sprachdateien. Der spaetere Eintrag gewinnt still, deshalb
stand auf dem neuen Knopf woertlich "%s Mods (110)" und auf der Befehlsseite
die Beschreibung von /tag statt der Kopfzeile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 11:22:55 +02:00
D4rkst3randClaude Opus 5 9b681754fa Ein Embed je Hof, das sich selbst aktualisiert
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Der Fuhrpark stand bisher nur als Gesamtzahl im Karten-Embed. Jetzt bekommt
jeder Hof eine eigene Nachricht im selben Kanal, die bei jedem Durchlauf
bearbeitet statt neu gepostet wird: Land, Fahrzeuge mit Wert und
Betriebsstunden, was in die Werkstatt muss, was gewaschen gehoert und was
geladen ist.

Die Fahrzeuge kommen aus vehicles.xml, blockweise gelesen — Verschleiss und
Schmutz stehen in Kindelementen und gehoeren sonst dem falschen Fahrzeug.
Gemietete Missionsfahrzeuge bleiben draussen, sie verfaelschen Anzahl und
Wert. Faellt die Datei einmal aus, bleiben die Embeds stehen, statt dass ein
Hof verschwindet und gleich darauf doppelt gepostet wird.

Damit tauchen auch Hoefe auf, die noch kein Land gekauft haben: die Liste der
Hoefe kommt aus Parzellen und Fuhrpark zusammen.

Dazu raus, was nur den Kanal vollgeschrieben hat: die Meldung zum
Monatswechsel und die zum Landkauf. Der Monat wird still weitergestellt — er
steht ohnehin im Embed. Damit faellt der Ereignis-Kanal weg und mit ihm die
gespeicherte Besitzliste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:53:02 +02:00
D4rkst3randClaude Opus 5 0875dff127 Server-Embed aufgeraeumt: volle Reihen statt halber
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Discord setzt drei Inline-Felder in eine Reihe. Es waren fuenf — Status,
Spieler, Map, Version, Mods —, also drei plus zwei, und die zweite Reihe stand
halb leer. Bei einem Server, der weniger meldet, stand ein einzelnes Feld
allein herum.

Neu sortiert nach dem, was man wissen will:

Der Zustand ist die Ueberschrift, kein Datenpunkt. Er steht jetzt in der
Beschreibung, eine Zeile mit Trennpunkten: online, Schloss, Ping. Das spart
eine ganze Feldreihe und liest sich als Satz. Der Ping ist damit aus der
Fusszeile raus, wo er zwischen Marke und Intervall stand.

Die Auslastung ist die Zahl, wegen der man hinschaut — sie bekommt die volle
Breite statt eines Drittels, und der Balken wurde von zehn auf sechzehn
Zeichen breiter.

Map, Version und Mods bilden genau eine Reihe. Fehlt eins davon, fuellen
unsichtbare Felder auf, damit die Reihe nicht schief steht.

Adresse und alles, was Discord nicht klickbar macht — fivem://, steam://,
ts3server:// —, standen als je eigener Codeblock untereinander. Das war eine
Wand aus Kaesten. Jetzt ein Block "Zum Kopieren" mit allem drin.

Nebenbei: fuer den Namen im Spiel hatte ich Discords Kleintext-Auszeichnung
genommen. Die rendert in Embeds nicht verlaesslich, und dann staende das
Steuerzeichen woertlich da. Kursiv tut es auch.

Geprueft: drei Zusammenstellungen im Klartext durchgespielt (LS mit allem,
FiveM mit wenig, offline), und die Auffuellung mit einem, zwei und drei
Feldern sowie ohne.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:25:58 +02:00
D4rkst3randClaude Opus 5 ea523b76b3 Servername aus dem Spiel und ein Schloss fuer Passwort oder Whitelist
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Der Name im Panel ist der, den DU vergibst — wie der Server sich im Spiel
nennt, stand nirgends. Steht jetzt darunter, aber nur wenn es etwas anderes
ist als der eingetragene Name; sonst haette dieselbe Zeile zweimal dagestanden.

Beim Schloss war die Frage, wie viel die Abfrage ueberhaupt hergibt. Nachgesehen
statt geraten: 31 der 358 gamedig-Protokolle melden ein Passwort. valve
gehoert dazu, deckt also CS2, Rust, ARK, Squad, 7DTD, Zomboid, Gmod und DayZ
ab. Minecraft nicht, Farming Simulator auch nicht — im LS-XML kommt das Wort
Passwort kein einziges Mal vor. Und eine Whitelist meldet grundsaetzlich kein
Spiel, die steht in einer Datei auf dem Server.

Also beides: automatisch, wo es geht, und eine Einstellung, die es schlaegt.
Wer "Whitelist" eintraegt, bekommt sie angezeigt, auch wenn der Server
schweigt.

Wichtig dabei ist der dritte Zustand. "Sagt nichts" ist nicht "offen" —
deshalb `null` und kein `false`. Ein Schloss, das bei jedem schweigenden
Server fehlt, waere eine Aussage, die wir nicht belegen koennen; umgekehrt
waere ein Schloss ohne Grundlage eine Luege. Bei unbekannt steht schlicht
nichts da.

Geprueft: Einstellung schlaegt Messung in allen vier Faellen, automatisch mit
true/false/null, fehlender Server, unbekannter Wert faellt auf automatisch
zurueck statt zu sperren, Schloss im Embed-Titel nur wenn belegt, Spielname
nur bei echtem Unterschied (auch mit Leerzeichen drumherum). Panel und
/server-Seite im laufenden Frontend angesehen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 14:12:33 +02:00
D4rkst3randClaude Opus 5 4ce3c10d76 Kein Monatswechsel mehr aus einer kaputten Antwort
Deploy / check (push) Has been cancelled
Deploy / deploy (push) Has been cancelled
Der Bot rief einen neuen Monat aus, obwohl im Spiel keiner war. Zwei Fehler,
die zusammen genau das ergeben:

statsLesen las die Uhrzeit als `Number(server.dayTime) || 0`. Eine Antwort
ohne dayTime — Server startet neu, Proxy schiebt eine Fehlerseite dazwischen,
XML halb geschrieben — wurde damit zu 0. Und tagUmgeschlagen prueft auf
`jetzt < vorher`. Null ist kleiner als jeder Vortagswert, also sah jede
kaputte Antwort wie Mitternacht aus.

Beides behoben: fehlt die Angabe, ist sie jetzt null und nicht 0 — "weiss
nicht" ist kein Tageswechsel. Und ein Wechsel muss die Form eines echten
Mitternachtssprungs haben: vom spaeten Abend in den fruehen Morgen, beide
Werte innerhalb eines Tages. Ein Zappeln um ein paar Minuten faellt jetzt
durch. Eine Antwort ohne Uhrzeit ueberschreibt ausserdem den letzten guten
Stand nicht mehr, sonst ginge der echte Sprung danach verloren.

Beim Nachmessen zeigte sich, dass meine Doku an der Stelle falsch war: dayTime
ist nicht live, sondern ein Schnappschuss. Fuenf Abrufe ueber zwei Minuten mit
einem Spieler online ergaben denselben Wert (37028620) — der Server schreibt
das XML im "Web API Interval" neu, hier alle 360 Sekunden. Steht jetzt richtig
in docs/ls-feed.md, samt dem verbleibenden Restrisiko: ein Serverneustart
setzt die Uhr auf den gespeicherten Stand zurueck und kann in seltenen Faellen
einen Monat zu viel zaehlen.

Geprueft: echter Sprung, normaler Tagesverlauf, Erstlauf, fehlende Angabe als
0/null/NaN, kleiner Ruecksprung, Ruecksprung am Vormittag, unsinnige Groessen,
und dass eine fehlende dayTime im XML als null ankommt statt als 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 13:05:21 +02:00