Files
d4rkbot/docs/befunde-2026-08-12.md
T
D4rkst3randClaude Opus 5 b63d4cae3b
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
docs: der Auto-Deploy hat nicht ausgeloest -- gemessen, nicht vermutet
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

13 KiB

Durchsicht am 12.08.2026

Gesucht wurde nach denselben Fehlermustern, die beim Bau von d4rk_media aufgefallen sind — nicht nach allem Denkbaren. 24 105 Zeilen in 108 Dateien liest niemand am Stück; gesucht wurde gezielt nach:

  • Aufräumfunktionen, die nie aufgerufen werden
  • Wachen, die an '*' hängen statt an den eigenen Pfaden
  • SQL, in das Zeichenketten eingesetzt werden
  • fetch auf Adressen, die von außen kommen
  • Antwortkörper, die niemand liest
  • Schleifen mit Zeitlimit, die nacheinander laufen
  • Sicherungen, die nie zurückgespielt wurden

An diesem Bot wurde nichts geändert. Was hier steht, ist ein Befund und kein Eingriff — die Entscheidungen darüber gehören dir.


1 · Die Sicherung hat keinen sicheren Ort

Das ist der wichtigste Befund. Gemessen:

docker inspect d4rkbot
  volume  ecobot_ecobot_data  ->  /app/data

/app/data/ecobot.db          5,0 MB
/app/data/backups/           5,7 MB   (14 Stück, täglich 03:0x)

Datenbank und alle vierzehn Sicherungen liegen im selben Volume. Geht das Volume verloren, geht beides zusammen verloren. Das ist keine Sicherung, das ist eine Kopie.

Vorgesehen ist ein Ausweg — der Upload in einen privaten Discord-Kanal:

const channelId = getSetting('backup_channel_id');
if (channelId && client) {  }

Der Kanal ist nicht gesetzt. Der Zweig läuft also nie, und der einzige vorgesehene Weg aus dem Volume heraus ist unbenutzt.

Was der Code richtig macht, damit das nicht untergeht: db.backup(rawFile) — die Online-Sicherungsschnittstelle von SQLite, WAL-sicher. Eine Dateikopie wäre still unvollständig gewesen. Das ist derselbe Griff wie in d4rk_media und er ist korrekt.

Zwei Wege:

  1. backup_channel_id setzen — der eingebaute Weg, kostet einen Klick.
  2. Wie bei d4rk_media: ein Skript auf dem Wirt, das das Archiv abholt und in die Nextcloud legt. Dort liegt schon eines (tools/sichern.ps1), das Muster ließe sich übernehmen.

2 · Niemand merkt, wenn die Sicherung ausbleibt

last_backup wird geschrieben und im Panel angezeigt — aber nirgends geprüft:

src/backup.js:57      setSetting('last_backup', …)      geschrieben
src/backup.js:70      … === today ? return              als Tagesbremse gelesen
src/web/api.js:1003   lastBackup: getSetting(…)         angezeigt

Niemand vergleicht ihn mit heute und beschwert sich. Dazu kommt das Zeitfenster:

if (now.getHours() !== 3) return;

Ist der Bot während der Stunde 03 unten, fällt der Tag aus — still. Bei einem Bot, der nach jedem Update neu startet, ist das kein Sonderfall.

Die Maschinerie dafür ist vollständig da und wird nicht benutzt: dmAdmin, brandEmbed, startIncident/endIncident, der Wächter mit seiner Fehlerschwelle. Es fehlt der eine Aufruf, der last_backup liest.

d4rk_media hat für genau das pruefeSicherung() — 26 Stunden Schwelle, meldet einmal je Zustand.


3 · Zu groß für Discord endet in der Konsole

} else {
    console.warn(`[backup] ${gzFile} zu groß für Discord-Upload (${sizeBytes} B)`);
}

Die Grenze steht bei 9 MB. Die Archive wachsen sichtbar:

08.08.  689 553 B
09.08.  762 371 B
10.08.  836 677 B
11.08.  906 916 B

Rund 70 KB je Tag. Bis 9 MB ist Luft für etwa drei Monate — und wenn sie reißt, hört die Kopie still auf. Eine Konsolenzeile in einem Container liest niemand.

Das ist derselbe Fehler wie ein Knopf ohne Rückmeldung, nur langsamer.


4 · Das Archiv wird nie gegengeprüft

Es wird geschrieben, gezippt, hochgeladen — und nie ausgepackt, nie geöffnet, nie gezählt. Ein defektes gzip oder eine halbe Datei fiele erst an dem Tag auf, an dem man sie braucht.

tools/sichern.ps1 in d4rk_media packt sein Archiv aus, öffnet die Datenbank darin und zählt die Zeilen — und tools/zurueckspielen.ps1 fährt daraus einen zweiten Dienst hoch und lässt ihn drei zufällige Dateien ausliefern. Genau diese Übung hat dort gestern einen echten Fehler gefunden (ein stehen gebliebenes media.db-wal, das die wiederhergestellte Datenbank leer aussehen ließ).

Ein Backup, das man nie zurückgespielt hat, ist keins.


5 · pruneMessageCache wird nie aufgerufen — FALSCH, zurückgezogen am 12.08.2026

Dieser Befund war falsch. Der Aufruf steht in src/bot/mod-tools.js:239, in registerModTools, mit eigenem Kommentar — und er stand schon zum Zeitpunkt der Durchsicht dort (nachgesehen in git show 1808b7c:src/bot/mod-tools.js):

const aufraeumen = () => {
    const weg = pruneMessageCache(Math.max(0, tuning('modlog_cache_days')));
    if (weg > 0) console.log(`[modlog] ${weg} alte Nachrichten vergessen`);
};
aufraeumen();
setInterval(aufraeumen, 86_400_000);

Täglich, mit Frist aus der Feineinstellung. Es ist nichts zu tun.

Wie es passieren konnte: gezählt wurden „Aufrufe außerhalb von db.js" — und mod-tools.js importiert die Funktion in einer Sammelzeile mit fünf anderen Namen. Beim Zählen ist die Zeile als Import durchgegangen und der Aufruf zwanzig Zeilen weiter unten nicht mitgezählt worden.

Das ist genau der Fehler, den zu vermeiden dieser Bericht dasteht: eine plausible Zahl, die niemand am Gegenstand nachgeprüft hat. Die anderen vier Befunde sind deshalb am 12.08.2026 alle noch einmal einzeln nachgemessen worden — sie stimmen:

Befund Nachgemessen
1 · Sicherung ohne sicheren Ort backup_channel_id ist leer — der Offsite-Zweig lief nie
2 · Ausbleiben fällt niemandem auf last_backup kommt an drei Stellen vor: schreiben, Tagesbremse, anzeigen. Nirgends ein Vergleich mit heute.
3 · Zu groß endet in der Konsole console.warn in backup.js, unverändert
4 · Archiv nie gegengeprüft kein Auspacken, kein Öffnen, kein Zählen im ganzen Modul

Geprüft und in Ordnung

Das gehört genauso hierher, damit es niemand ein zweites Mal prüft.

Was Ergebnis
db.backup() statt Dateikopie richtig, WAL-sicher — und im Kommentar begründet
ORDER BY ${ord} in db.js:414 sicher: SORTIERUNG ist ein festes Objekt mit drei Werten, die Anweisungen werden einmalig daraus vorbereitet. Der Aufrufer wählt nur einen Schlüssel.
Öffentliche Serverliste /api/servers sauber: die Felder sind einzeln aufgezählt, „bewusst ohne query_url/host/port"
Geheimnisse in /api/settings die zwei bekannten gehen als Ja/Nein hinaus (gitea_api_token_set, twitch_creds_set)
Spielserver-Monitor nutzt schon Promise.all — die Schleife läuft nebeneinander
Zugangscodes in der Datenbank genau einer (LS25, 16 Zeichen), und er geht nur an Panel-Nutzer mit server-Recht

Beobachtungen ohne Handlungsbedarf

Panel-Nutzer können den Bot beliebige Adressen abrufen lassen — Watchdog, Spielserver-Abfrage, LS-Feed nehmen alle eine URL aus der Datenbank. Bei d4rk_media war das eine echte Lücke, weil dort die Antwort als Datei zurückkommt. Hier kommt nur „erreichbar ja/nein" heraus, und wer das Panel hat, kann ohnehin mehr. Kein Handlungsbedarf — nur zu wissen, falls je ein Weg dazukommt, der den Antwortkörper weitergibt.

currentSettings() gibt jeden von einem Modul angemeldeten Textschlüssel im Klartext aus (src/web/api.js:901). Heute steht dort nichts Heikles. Aber ein künftiges Modul mit einem Feld api_token ginge ungefragt mit hinaus, ohne dass jemand es bemerkt. Eine Namensregel („endet auf _secret/_token → nur als Ja/Nein") wäre eine Zeile und schlösse das für immer.


Wenn ich eins zuerst täte

Punkt 1 und 2 zusammen, denn sie sind dasselbe Problem von zwei Seiten: die Sicherung hat keinen zweiten Ort, und niemand würde merken, wenn sie ganz ausbliebe. Beides zusammen heißt: der Bot hat faktisch keine Sicherung, sondern eine Kopie neben dem Original, deren Ausfall unbemerkt bliebe.

Der billigste Schritt ist backup_channel_id zu setzen — ein Klick, und der vorgesehene Weg funktioniert. Der solide Schritt ist derselbe wie bei d4rk_media: abholen, prüfen, in die Nextcloud, und ein Wachhund darauf.


Was am 12.08.2026 daraus gebaut wurde

Befund 2, 3 und 4 sind erledigt — Befund 1 zur Hälfte (siehe unten).

Der Bot sagt jetzt Bescheid

src/melden.js ist neu und enthält das, was vorher in watchdog.js eingeschlossen war: dmAdmin. Deshalb hat alles andere im Bot geschwiegen oder in die Konsole geschrieben — eine Konsolenzeile in einem Container liest niemand.

Dazu dmAdminEinmalig(client, schluessel, zustand, …): meldet nur, wenn sich der Zustand geändert hat. Ohne das würde eine Prüfung, die alle zehn Minuten läuft, denselben Ausfall alle zehn Minuten melden — 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, hätte sonst nach jedem Update wieder eine frische Meinung.

Nachgemessen gegen eine Kopie der Datenbank, sechs Schritte:

1. Ausfall zum ersten Mal                 gebaut: 1   Merker: "alt:…T03"
2. derselbe Ausfall noch zweimal          gebaut: 0   Merker: "alt:…T03"
3. ANDERER Ausfall                        gebaut: 1   Merker: "fehler:…T09"
4. wieder in Ordnung (null)               gebaut: 0   Merker: ""
5. derselbe Ausfall danach erneut         gebaut: 1   Merker: "alt:…T03"

Der Wachhund auf last_backup

pruefeSicherung(client) läuft im selben Zehn-Minuten-Takt wie der Planer und meldet, wenn die letzte Sicherung älter als 26 Stunden ist, wenn die letzte fehlgeschlagen ist, oder wenn es noch nie eine gab.

Beim Fehlerfall steht nicht last_backup als Zeitpunkt im Embed: der wird nur bei Erfolg gesetzt, dort stünde also der letzte gute Lauf, und das ließe die Sicherung frischer aussehen, als sie ist. Der Zeitpunkt des Fehlschlags steht getrennt daneben (last_backup_fehler_at).

Der Tag fällt nicht mehr aus

Vorher:

if (now.getHours() !== 3) return;

Wer während dieser einen Stunde unten war, hatte den Tag verloren — und ein Bot, der nach jedem Update neu startet, ist genau dieser Fall. Jetzt zählt nur noch: es ist nach 03:00 und heute war noch keine. Der Lauf wird also nachgeholt.

Das Archiv wird ausgepackt und gezählt

Nach dem Zippen wird das Archiv wieder ausgepackt, die Datenbank darin geöffnet, PRAGMA integrity_check gefahren und die Tabellen gezählt — gegen die laufende Datenbank. Fällt das durch, wird das Archiv gelöscht (im Sicherungsordner sähe es sonst aus wie eine Sicherung), die Rotation läuft nicht, und last_backup bleibt stehen.

An den echten Archiven gemessen, und mit zwei Gegenproben, damit die Prüfung nicht nur „ja" sagen kann:

laufende Datenbank: 55 Tabellen

2026-08-10   817 KB   54 Tabellen, 86 Einstellungen   86 ms
2026-08-11   886 KB   54 Tabellen, 86 Einstellungen   64 ms
2026-08-12   970 KB   55 Tabellen, 86 Einstellungen   78 ms

halbes gzip  -> abgelehnt: "unexpected end of file"
Müll         -> abgelehnt: "incorrect header check"

Die 54 gegen 55 sind übrigens kein Fehler, sondern eine gewachsene Tabelle zwischen dem 11. und dem 12. Verglichen wird zeitgleich — Archiv gegen die Datenbank, aus der es gerade entstanden ist —, also stört das nicht. Wissen sollte man es trotzdem, bevor jemand alte Archive gegen die heutige Zahl hält.

Was offen bleibt: Befund 1

Die Sicherung liegt weiterhin im selben Volume wie die Datenbank. Der eingebaute Ausweg (backup_channel_id) braucht eine Kanal-ID, die nur du geben kannst — ein privater Kanal, in dem der Bot Dateien anhängen darf. Ein Klick in Setup → Einstellungen, und der Zweig läuft.


⚠️ Nachtrag: der Auto-Deploy hat nicht ausgelöst

docs/auto-deploy.md beschreibt zwei Wege — Portainer-Webhook oder Gitea Actions. Am 12.08.2026 hat keiner von beiden gegriffen. Gemessen nach dem Push von 93156e5:

Push registriert          git.d4rkst3r.de sagt updated_at 12:48:18Z
Workflow-Läufe            "total_count": 0            <- keiner, nie
Act-Runner auf dem Wirt   kein Container
d4rkbot neu gestartet?    nein, läuft seit 11.08. 22:21
Code im Container         grep pruefeSicherung -> 0   <- der alte Stand

has_actions ist am Repo an, es gibt also nur niemanden, der die Läufe abarbeitet — und der Webhook-Weg (der ohne Runner auskäme) ist offenbar auch nicht eingerichtet.

Das heißt: pushen allein rollt nichts aus. Bis einer der beiden Wege steht, gilt weiterhin der Weg von Hand — Portainer → Stack ecobotPull and redeploy.

Das ist keine Kleinigkeit für die Zukunft: ein Deploy, von dem man glaubt, dass er automatisch läuft, ist schlimmer als gar keiner. Man pusht, hakt es ab, und der Fehler, den man gerade behoben hat, läuft weiter.