# 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. ### Befund 1 — siehe unten Der ist am selben Tag noch angegangen worden. --- ## ~~Nachtrag: der Auto-Deploy hat nicht ausgelöst~~ — zwei falsche Diagnosen, siehe unten `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. ### Nachgefasst: er zieht, aber er baut nicht Der erste Verdacht („kein Runner") war nur die halbe Wahrheit. Portainers GitOps-Weg **funktioniert** — nachgemessen in seinem eigenen Datenverzeichnis: ``` /d/compose/1/src/melden.js 12:48 <- die Uhrzeit meines Pushes /d/compose/1/stack.env 12:57 <- Portainer hat neu geschrieben docker images d4rkbot:latest 00:21 <- das Abbild ist 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 und keine Fehlermeldung; es sah nach „nichts zu tun" aus. **Die Zeile, die gefehlt hat**, steht jetzt in `docker-compose.yml`: ```yaml pull_policy: build ``` Damit baut `docker compose up -d` das Abbild bei jedem Ausrollen neu. Geprüft mit `docker compose config` (Compose v5.3.1 nimmt es an und gibt es aufgelöst zurück). **Einmal noch von Hand**, denn die Zeile wirkt erst, wenn sie selbst ausgerollt ist: Portainer → Stack `ecobot` → *Pull and redeploy*. Danach greift der Auto-Deploy von allein. 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. ### Richtigstellung: der Auto-Deploy funktioniert. Er ist nur langsamer als meine Geduld **Der Abschnitt oben war falsch, und zwar zweimal.** Erst hieß es „kein Runner, also läuft gar nichts", dann „er zieht, aber er baut nicht". Beides war aus einem Zwischenstand geschlossen, während 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. Meine Messung um 12:57 fiel mitten in dieses Fenster: die Dateien waren schon da, das Abbild noch nicht neu. Aus diesem Schnappschuss habe ich einen Systemfehler gemacht. **Das ist genau der Fehler, gegen den dieser Bericht geschrieben ist** — eine plausible Erklärung, die zum Schnappschuss passt, behauptet, bevor der Vorgang zu Ende war. Der Unterschied zu den anderen Befunden hier: bei denen wurde gewartet, bis etwas fertig war. ### Was mit `pull_policy: build` passiert Die Zeile steht weiter in `docker-compose.yml`, aber **nicht mehr aus dem Grund, der im Commit davor stand**. Sie repariert nichts — Portainer baut auch ohne sie neu. Sie macht es nur ausdrücklich: bei einem Stack mit `build:` ist „immer neu bauen" das, was man beim Ausrollen will, und wer später von Hand `docker compose up -d` tippt, bekommt dann dasselbe Ergebnis wie die Automatik. Wer sie nicht will, kann sie folgenlos entfernen. --- ## Befund 1: die Sicherung verlässt jetzt das Volume `tools/abholen.ps1`, täglich um **03:30** — nach der Sicherung des Bots (ab 03:00) und vor der von `d4rk_media` (04:30), damit nicht zwei Vorgänge gleichzeitig auf derselben Platte arbeiten. Es **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: Prüfsumme im Container gegen die Kopie draußen, dann auspacken und die Datenbank darin öffnen. Echter Lauf über die geplante Aufgabe: ``` Zustand: Ready Ergebnis: 0 Naechster Lauf: 13.08.2026 03:30 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 last_zweitziel = { ok: true, ziel: "D:\backup\d4rkbot", … } ``` ### Was außerhalb des Repos liegt Zwei Dinge, die ein `git clone` nicht mitbringt — sie stehen hier, damit sie nach einem Neuaufsetzen nicht fehlen: | Was | Wo | |---|---| | Arbeitskopie | `C:\Users\Darkster\Desktop\d4rkbot` — vorher gab es keine, der Bot lief nur aus Portainers Checkout | | Geplante Aufgabe | `d4rkbot Sicherung abholen`, täglich 03:30, als `Darkster`/Interactive | Die Aufgabe startet `pwsh.exe` über den **stabilen Ausführungsalias** unter `LOCALAPPDATA` und nicht über den Pfad mit Versionsnummer. Genau daran hing bei `d4rk_media` eine Sicherung, die beim nächsten PowerShell-Update aufgehört hätte zu laufen. ### Und was weiter offen ist Beide Kopien liegen **im selben Rechner**. Gegen einen Plattenausfall hilft das jetzt, gegen Feuer, Diebstahl oder eine verschlüsselte Maschine nicht. Dafür gibt es zwei Wege, und sie schließen sich nicht aus: 1. **`backup_channel_id`** setzen — der eingebaute Weg, ein privater Kanal, in dem der Bot Dateien anhängen darf. Kostet einen Klick. Grenze: 9 MB je Datei, heute bei 993 KB, hochgerechnet gegen Ende Oktober bei rund 6 MB. 2. **In die Nextcloud**, wie `d4rk_media` es tut. `tools/sichern.ps1` dort enthält den WebDAV-Teil fertig (`curl`, `MKCOL`, `PUT`, Prüfsumme zurückgeholt, `PROPFIND` zum Ausdünnen) — er ließe sich übernehmen.