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>
17 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
fetchauf 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:
backup_channel_idsetzen — der eingebaute Weg, kostet einen Klick.- 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
pruneMessageCache wird nie aufgerufenDieser 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.
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:
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:
backup_channel_idsetzen — 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.- In die Nextcloud, wie
d4rk_mediaes tut.tools/sichern.ps1dort enthält den WebDAV-Teil fertig (curl,MKCOL,PUT, Prüfsumme zurückgeholt,PROPFINDzum Ausdünnen) — er ließe sich übernehmen.