# 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: ```js 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: ```js 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 ```js } 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`): ```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: ```js 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.