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>
351 lines
14 KiB
Markdown
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.
|