Files
rippy/AGENTS.md
T
HitonabiandClaude Opus 5 6dfff5c99d fix(windows): vier Befunde aus dem ersten echten Installationslauf
Alle vom Commander gemeldet, alle nachgestellt.

## 1. Der Installer brach ab: No module named 'db'

    Fehlgeschlagen: ModuleNotFoundError: No module named 'db'

Zu Recht: **docker/api/db.py gibt es nicht.** Seit der Zusammenlegung in V2-1
ist der Store `rippy.store`; main.py schreibt seitdem
`from rippy import store as db`. Ich habe mir aus einem ALIAS ein Modul
zusammengereimt und importiert -- genau die erfundene Schnittstelle, vor der
AGENTS.md Regel D warnt, nur diesmal im eigenen Code. Der Store wird jetzt
direkt benutzt; der Umweg ueber den API-Suchpfad war ohnehin ueberfluessig.

## 2. Der Ersteinrichtungs-Assistent fehlte nach der Installation

In App.tsx stand:

    .catch(() => setSetupDone(true))  // API/Setup nicht erreichbar -> nicht blockieren

Das Fenster geht unmittelbar nach dem Setup auf, der Dienst braucht ein bis
zwei Sekunden bis zur ersten Antwort -- der erste Abruf scheitert also fast
immer. Aus "ich konnte nicht nachsehen" wurde "ist schon erledigt", und der
Assistent war fuer immer weg.

Dasselbe Prinzip steht nebenan in useEventStream.tsx: Ein Verbindungsabriss
ist KEINE Aussage ueber die Welt. Jetzt wird nachgefragt, bis der Server
ANTWORTET (zwei Minuten lang alle zwei Sekunden), und solange steht
"Rippy startet ..." im Fenster statt einer leeren Flaeche.

## 3. + 4. Als Administrator keine Netzlaufwerke, Netzlaufwerk = "keine Rechte"

Beides Windows-Verhalten, kein Rippy-Fehler -- aber Rippy hat ihn
hineinlaufen lassen. Auf dem Rechner nachgemessen:

    X:, Y:, Z:                 Netzlaufwerke
    EnableLinkedConnections    nicht gesetzt (Standard)
    C:\Program Files (x86)     ohne Administrator nicht beschreibbar

Ein erhoehter Prozess bekommt ein anderes Zugriffstoken; eingebundene
Netzlaufwerke haengen am Token der Sitzung und existieren dort schlicht
nicht. Daraus wird ein Teufelskreis:

    Program Files    verlangt Administrator
    Administrator    versteckt die Netzlaufwerke
    Netzlaufwerk     wirkt dann wie "keine Schreibrechte"

Rippy braucht ueberhaupt keine Administratorrechte (Vorgabeordner unter
%LOCALAPPDATA%, Autostart unter HKCU). Der Assistent sagt das jetzt:

* eine eigene Pruefung "Rechte", die bei erhoehtem Start warnt und die
  unsichtbaren Laufwerke beim Namen nennt
* ein fehlender Laufwerksbuchstabe wird als solcher gemeldet, nicht als
  fehlendes Schreibrecht ("Laufwerk Q: ist hier nicht vorhanden")
* ein Ziel unter "Programme" erklaert den Kreis und verweist auf den
  Vorgabeordner

## Dazu: zwei Schreibfallen in AGENTS.md

Deutsche Anfuehrungszeichen in doppelt gequoteten Zeichenketten (vier Mal an
einem Tag) und Bash-Heredocs, die Backslashes halbieren (fuenf Mal, zuletzt
beim Schreiben genau dieses Absatzes). Ein Test dafuer waere Rauschen gewesen
-- Python faengt beides bereits beim Import, nur mit verwirrender Meldung.
Die Regel gehoert in die Konventionen, nicht in eine Pruefung.

Ampel lokal: 703 gruen, ruff sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 14:36:56 +02:00

196 lines
11 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.)
### Zwei Schreibfallen, die am 28.08.2026 mehrfach gekostet haben
**1. Deutsche Anführungszeichen in doppelt gequoteten Zeichenketten.**
Das öffnende `„` ist U+201E und harmlos. Das schließende ist das **gerade
ASCII-Zeichen** `"` — es beendet die Python-Zeichenkette. Der Fehler erscheint
dann am Gedankenstrich dahinter, also an einer Stelle, an der nichts falsch
ist. An einem Tag vier Mal passiert.
falsch: "einen Ordner unter „Benutzer" wählen"
richtig: 'einen Ordner unter „Benutzer" wählen'
**Regel: Steht ein `„` in der Zeichenkette, gehört sie in EINFACHE
Anführungszeichen.**
**2. Bash-Heredocs mit Windows-Pfaden.** `<<'EOF'` sollte literal sein,
halbiert in dieser Umgebung aber Backslashes — aus `C:\\Users` wird `C:\Users`,
und das ist eine ungültige Escape-Sequenz. An einem Tag fünf Mal passiert,
zuletzt beim Schreiben genau dieses Absatzes.
**Regel: Dateien mit Windows-Pfaden oder Umlauten mit dem Write-Werkzeug
schreiben, nicht per Heredoc.** Wo es doch sein muss: Backslashes über
`chr(92)` zusammensetzen.
## 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.