Files
d4rkbot/docs/befunde-2026-08-12.md
T
D4rkst3randClaude Opus 5 b98f4e15c9
Deploy / check (push) Canceled after 0s
Deploy / deploy (push) Canceled after 0s
fix: pull_policy build -- der Auto-Deploy zog, baute aber nie
Der erste Verdacht ("kein Runner") war die halbe Wahrheit. Portainers
GitOps-Weg funktioniert, nachgemessen in seinem eigenen Datenverzeichnis:

    /d/compose/1/src/melden.js     12:48   <- die Uhrzeit des Pushes
    /d/compose/1/stack.env         12:57   <- Portainer hat neu geschrieben
    docker images d4rkbot:latest   00:21   <- das Abbild 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, keine Fehlermeldung: es sah nach "nichts zu tun"
aus. Das ist die unangenehme Sorte Fehler -- man pusht, hakt es ab, und der
Fehler, den man gerade behoben hat, laeuft weiter.

pull_policy: build laesst compose bei jedem Ausrollen neu bauen. Geprueft mit
docker compose config (v5.3.1 nimmt es an und gibt es aufgeloest zurueck).

Einmal noch von Hand, denn die Zeile wirkt erst, wenn sie selbst ausgerollt
ist: Portainer -> Stack ecobot -> Pull and redeploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 15:00:51 +02:00

351 lines
14 KiB
Markdown

# 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.
---
## ⚠️ 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.
### 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.