163216a68e
Ampel / ampel (push) Successful in 31s
SAVEPOINT v3.20 mit den Messwerten: /jobs und /capabilities byteweise identisch ueber zwanzig Sekunden (es lud also nichts neu), 97 Antworten mit HTTP 429 im nginx-Log, 812 von 876 Anfragen scheinbar von einer IP, und die Rechnung, die zeigt warum: ein offener Tab braucht 123 Anfragen/min, erlaubt waren 100. Dazu drei neue Lehren in AGENTS.md: - Ein verpasster Abruf ist keine Nachricht ueber die Welt (`catch(() => [])`). - Eine eigene Schutzbremse gegen die eigene Last rechnen - und jedes Greifen protokollieren, sonst ist sie unsichtbar. - Eine geschluckte Warnung ist eine Falle (`|| echo` in einem 200-Zeilen-Log). - Und: wenn der Commander eine Korrelation nennt, ist das eine Spur, auch wenn seine vermutete Erklaerung daneben liegt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
173 lines
10 KiB
Markdown
173 lines
10 KiB
Markdown
# AGENTS.md — Arbeits-Konventionen für Rippy
|
|
|
|
## Grundregeln
|
|
|
|
**1. Plan vor Code.** Vor jeder Etappe: Klare Akzeptanzkriterien schreiben, dann bauen.
|
|
**2. Kleine Schritte.** Max. eine Feature pro Commit. Commit-Nachrichten beschreiben WAS und WARUM.
|
|
**3. Beweisen statt behaupten.** Testen, was gebaut wurde. Keine "es sollte funktionieren"-Commits.
|
|
**4. Deutsch-Nicht-Entwickler.** Der Commander liest alles — Variablennamen auf Englisch, aber Kommentare und Docs auf Deutsch.
|
|
|
|
## ⛔ HARTE Regeln (mechanisch geprüft — seit 22.07.2026)
|
|
|
|
**A. Die CI-Ampel muss GRÜN sein, bevor irgendetwas „fertig" heißt.**
|
|
`.gitea/workflows/ci.yml` läuft bei jedem Push auf dem Gitea-Runner (NICHT löschen, NICHT
|
|
abschwächen). Keine Tests = rot = nicht fertig. Der Commander liest die Ampel, nicht den Code.
|
|
|
|
**B. Abweichung vom KONZEPT = STOPP + fragen.** Anderes Werkzeug, andere Bibliothek,
|
|
gestrichenes Muss-Feature → erst den Commander fragen, NIE still ersetzen.
|
|
(Vorgefallen: Muss-Feature „MakeMKV lossless" wurde still durch lossy HandBrake ersetzt.)
|
|
|
|
**C. Single Source of Truth ist `main` — es gibt nur diesen einen Branch**
|
|
(seit 24.07.2026; der `stable`-Zwischenbranch ist abgeschafft). Die CI-Ampel
|
|
läuft bei jedem Push und PRÜFT nur (Ruff/pytest/Vite-Build) — sie befördert
|
|
nichts mehr. Deployt wird direkt aus `main`: auf der VM `git pull` bzw.
|
|
`./deploy.sh` (verifiziere vorher, dass die Ampel für den Commit GRÜN ist —
|
|
rot heißt: nicht deployen). Nie freihändig per SSH auf der VM bauen.
|
|
(Vorgefallen: Doppel-Anlage `Rippy` + `rippy` auf der VM durch Freihand-Deploys.)
|
|
|
|
**D. Externe Schnittstellen NIE aus dem Kopf.** Vor Nutzung fremder CLI-Flags oder
|
|
Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen.
|
|
(Vorgefallen: erfundene Celery-Methode `self.send_task`, erfundene abcde-Flags.)
|
|
|
|
## Workflow
|
|
|
|
1. **Read:** KONZEPT.md + ROADMAP.md lesen. Verstehen, welche Etappe dran ist.
|
|
2. **Plan:** Was genau soll diese Sitzung bauen? Eine Zeile.
|
|
3. **Build:** Code schreiben, testen, committen.
|
|
4. **Verify:** `docker compose up` — funktioniert das Ganze?
|
|
5. **Savepoint:** SAVEPOINT.md aktualisieren. Nächster Chat beginnt nicht von Null.
|
|
|
|
## Modelle je Aufgabe
|
|
|
|
- **Planung:** Heavy-Modell (Reasoning) für Architektur-Entscheidungen.
|
|
- **Bauen:** schnelles Coding-Modell.
|
|
- **Review:** Heavy nochmal für Code-Review vor Merge.
|
|
|
|
## Docker-Praxis
|
|
|
|
- Alles läuft in Containern: `docker compose up -d`
|
|
- Keine System-Pakete auf dem Host — alles im Container.
|
|
- Dockerfile immer multi-stage, kleinste Images.
|
|
|
|
## Was NICHT gebaut wird
|
|
|
|
- Kein Code direkt im Host-OS.
|
|
- Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker.
|
|
- Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf.
|
|
|
|
## Aktueller Stand (26.07.2026)
|
|
|
|
- ✅ **E2E bewiesen (v3.1):** BD-50 komplett durch die Kette (43 GB → 4,8 GB)
|
|
- ✅ **Etappe 13 (v3.2):** Universal-Komfort-Runde — Media-Server-Integration,
|
|
echte Benachrichtigungen, SMB-Klartext-Fehler, UHD-Arbeitsverzeichnis,
|
|
MakeMKV-Key via UI, Job-Detail-Popup, Toast-Feedback, Ampel-Blocker behoben
|
|
- ✅ **Etappe 17 (v3.10):** 4K-UHD-Disc-Schlüssel — persistentes
|
|
MakeMKV-Datenverzeichnis + `KEYDB.cfg` im UI
|
|
- ✅ **Etappe 18 (v3.11):** 4K-UHD gelöst — `makemkvcon` holt Schlüssel
|
|
unter Linux nie, unter Windows schon; Schlüsselspeicher übernehmbar.
|
|
Akira-UHD geht auf der VM auf (`TCOUNT:5`, bewiesen)
|
|
- ✅ **Etappe 19 (v3.14):** Durchsicht Frontend/Backend — vier Placebos weg
|
|
(Fortschritt log, Auswurf tat nichts, „Alle Tracks" konnte nichts, Encoder
|
|
wurden behauptet statt gemessen), Zombie-Erkennung gebaut, Pfad-Prüfung
|
|
gehärtet, und der Platten-Schutz aus `c065967` als **unwirksam** entlarvt
|
|
- ✅ **Etappe 20 (v3.15):** Aufräum-Runde — `/capabilities` 1,010 s → 0,003 s
|
|
(Ping im Hintergrund), Kompression je Disc-Typ abwählbar (4K verlustfrei),
|
|
Wizard empfiehlt nach gemessener CPU, vier tote Routen entfernt
|
|
- ✅ **Etappe 21 (v3.17):** Der Blocker externes Encoden ist zu —
|
|
`RIPPY_PATH_MAP` leitet Rippy aus seinen eigenen Mounts ab, der Installer holt
|
|
es selbst; Presets kommen vom Worker statt aus dem Quelltext; Dashboard ohne
|
|
Placebos, mit Restzeit. Dazu vier Bestandsfehler, alle live gemessen (u. a.
|
|
„Neu komprimieren" ging nie, und `os.path.isdir` hing im Kernel)
|
|
- ✅ **Etappe 22 (v3.18):** Auswurf wirkt endlich (MakeMKV verriegelt die Tür —
|
|
erst entriegeln, dann prüfen statt glauben), externer Worker meldet sein Log
|
|
nach Rippy, zeigt den laufenden Job und nimmt mehrere Aufträge an. Dazu **die
|
|
Mount-Ursache**: Die CIFS-Verbindung lebt in der Netz-Namespace des
|
|
api-Containers und stirbt mit ihm — eine Wache heilt das jetzt selbst
|
|
- ✅ **Etappe 23 (v3.19):** Sprachwahl vor dem Rip (Ton + Untertitel, Automatik
|
|
einstellbar), Auswurf wirkt wirklich (MakeMKV verriegelt die Tür), externer
|
|
Worker mit Verwaltungsfenster/Deinstaller/Slots, Schlüssel-Automatik für 4K,
|
|
Mount-Wiederanbindung 202 s → 8 s, Weitergabe an einem frischen Klon geprüft
|
|
- ✅ **Etappe 24 (v3.20):** Rippy bremste sich selbst aus — das Rate-Limit lag
|
|
unter der eigenen Last (100/min gegen 123/min), hinter dem Proxy teilten alle
|
|
Clients einen Eimer, und ein abgewiesener Abruf leerte das UI. Dazu: der
|
|
„Neu"-Knopf kennt jetzt die Phase (Rip oder Kompression) und fragt, wo er es
|
|
nicht weiß; zwei Fehler im Deploy-Weg behoben
|
|
- 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe
|
|
|
|
## Was diese Sitzungen wiederholt gekostet hat
|
|
|
|
**Nicht aus einem Zustandswert auf einen Mechanismus schließen.** Vorgefallen:
|
|
aus „kein Schlüssel da" → „Server abgeschaltet" (falsch), aus Status
|
|
`transcoding` → „Celery hat neu zugestellt" (falsch), aus `progress=99` →
|
|
„Altwert aus dem Absturz" (falsch — ein Bug), aus gleichem `st_dev` →
|
|
„`os.rename` funktioniert" (falsch — der Kernel vergleicht den Mount).
|
|
Jedes Mal hätte eine Messung von unter einer Minute gereicht.
|
|
|
|
**Und die Umkehrung gilt genauso:** gleiches `st_dev` heißt NICHT gleicher
|
|
Mount. Wo eine Eigenschaft ausprobierbar ist, probiere sie aus, statt sie
|
|
vorherzusagen.
|
|
|
|
**Ein Hintergrund-Prozess, der still scheitert, ist schlimmer als einer, der
|
|
laut scheitert** (26.07.2026). Ein `except Exception: pass` in einer
|
|
Vorrats-Schleife hat eine Stunde gekostet: Der Vorrat blieb leer, die Funktion
|
|
lief direkt aufgerufen einwandfrei, und der Grund stand nirgends. Gefunden erst
|
|
über die Thread-Zustände (`/proc/<pid>/task/*/stat`, Zustand `D` = im Kernel
|
|
blockiert). Jede Hintergrund-Schleife MELDET ihren Fehler, und wer einen Vorrat
|
|
anlegt, macht sein Alter abfragbar (`GET /health/vorraete`) — sonst ist am
|
|
Endpunkt selbst nichts zu sehen.
|
|
|
|
**Ein Rückgabewert ist kein Beweis, wo die Wirkung prüfbar ist** (26.07.2026,
|
|
zweimal am selben Abend). `CDROMEJECT` quittiert Erfolg auf einem verriegelten
|
|
Laufwerk und wirft nichts aus; `mount` quittiert Erfolg auf einer Verbindung, die
|
|
Sekunden später stirbt. Beide Fehler waren monatelang unsichtbar, weil der Code
|
|
dem Rückgabewert glaubte. Nach einer Aktion den ZUSTAND fragen — und wenn er
|
|
flattert, zweimal mit Abstand.
|
|
|
|
**Bei „zu langsam" die DAUER je Schritt messbar machen, nicht die plausibelste
|
|
Ursache beheben** (26.07.2026). Eine Mount-Wiederanbindung brauchte 150 s; die
|
|
erste, sehr plausible Erklärung war falsch, und die Änderung machte es langsamer
|
|
(202 s). Erst Zeitstempel im Log zeigten die Stelle: ein `os.makedirs` in Zeile
|
|
eins, das drei Minuten im Kernel hing. Danach 8 s. Wer eine Dauer nicht
|
|
aufschlüsselt, optimiert die falsche Stelle.
|
|
|
|
**Netz-Pfade nie ungebremst anfassen.** `os.path.isdir`/`open` auf einem toten
|
|
CIFS-Mount blockieren im Kernel und lassen sich aus Python NICHT abbrechen. Ein
|
|
Kind-Prozess lässt sich abbrechen: `timeout N ls -d <pfad>` (Muster in
|
|
`mounts.ist_erreichbar` und `rohdaten.verzeichnis_da`). Und „konnte nicht
|
|
nachsehen" ist etwas anderes als „ist nicht da" — beides zu vermischen erzeugt
|
|
falsche Aussagen im UI.
|
|
|
|
**Ein verpasster Abruf ist keine Nachricht über die Welt** (26.07.2026). Im UI
|
|
stand fünfmal `catch(() => [])`: Jeder fehlgeschlagene Abruf hieß damit „es gibt
|
|
keine Jobs, keine Laufwerke, keine Ablagen" — die Liste leerte sich für einen
|
|
Takt und füllte sich vier Sekunden später wieder. Der Commander meldete das als
|
|
„wird oft neu geladen", und die Ursache war unsichtbar, weil der Fehlerzweig
|
|
nichts protokollierte. Wer nichts Neues weiß, behält, was er wusste: bei
|
|
Fehlschlag `null` und den alten Stand stehen lassen — nie einen leeren Wert, der
|
|
als Aussage gelesen wird.
|
|
|
|
**Eine eigene Schutzbremse gegen die eigene Last rechnen** (26.07.2026). Das
|
|
Rate-Limit stand auf 100 Anfragen/min, während ein einziger offener Tab 111/min
|
|
verursacht (Dashboard 75 + Log-Kasten 24 + Laufwerke 12). Rippy bremste sich
|
|
also permanent selbst aus, und niemand sah es: Der 429 stand in keinem Log, und
|
|
das UI verbuchte ihn als Leermeldung. Dazu der zweite Fehler — hinter einem
|
|
Reverse-Proxy ist `request.client.host` IMMER der Proxy, also hatten Browser,
|
|
zweiter Tab und Windows-Tray EINEN gemeinsamen Eimer (812 von 876 Anfragen kamen
|
|
scheinbar von einer IP). Wer eine Grenze setzt, rechnet die eigene Grundlast vor,
|
|
schreibt sie als Kommentar dazu und lässt jedes Greifen protokollieren.
|
|
|
|
**Eine geschluckte Warnung ist eine Falle** (26.07.2026). `cp "$ENV_SRC" .env
|
|
2>/dev/null || echo "WARNUNG: …"` scheiterte auf der Ziel-VM bei JEDEM Deploy,
|
|
weil die .env dort anders lag. Die Zeile scrollte im Build-Rauschen vorbei,
|
|
gebaut wurde still mit einer zwei Tage alten Kopie — mit einem toten
|
|
Download-Notbehelf darin, an dem jeder worker-Build abbrach. Entweder abbrechen
|
|
oder so laut werden, dass es nicht zu übersehen ist (Dateidatum, Kandidatenliste);
|
|
ein `|| echo` in einem 200-Zeilen-Log ist keins von beidem.
|
|
|
|
**Wenn der Commander eine Korrelation nennt, ist das eine Spur.** „Wenn der
|
|
Worker installiert ist, wird der Bereich oft neu geladen" klang nach Bauchgefühl
|
|
und war exakt richtig: `tray.py` fragt `/api/jobs` über Port 80, landet damit im
|
|
Rate-Limit-Eimer des Browsers und drückt ihn über die Grenze. Dieselbe Lehre wie
|
|
bei „auf Windows ginge das sofort" (Disc-Schlüssel) — die Beobachtung ernst
|
|
nehmen, auch wenn die vermutete Erklärung („Celery-Ping?") daneben liegt.
|