diff --git a/docs/befunde-2026-08-12.md b/docs/befunde-2026-08-12.md new file mode 100644 index 0000000..5f63a44 --- /dev/null +++ b/docs/befunde-2026-08-12.md @@ -0,0 +1,191 @@ +# 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 + +``` +prunePlayerHistory 1 Aufruf ausserhalb db.js +pruneWatchdogHistory 1 +pruneHeartbeat 1 +pruneIncidents 1 +pruneMessageCache 0 <- +``` + +Exakt dasselbe Muster, das in `d4rk_media` bei `pruneEvents` gefunden wurde: die +Funktion ist da, die Aufbewahrungsfrist ist dokumentiert, der Aufruf fehlt. + +**Heute harmlos** — die Tabelle hat 18 Zeilen. Aber die Frist greift nicht, und +wenn der Nachrichten-Zwischenspeicher je stärker benutzt wird, wächst er ohne +Ende. + +--- + +## 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.