From 2587fe63afc99b680b87c8b5cf7a07e446ff36b9 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 26 Jul 2026 13:55:41 +0200 Subject: [PATCH] docs(savepoint): v3.17 - neun Punkte zu, vier Bestandsfehler dazu Der Savepoint trennt wie immer gemessen von vermutet - und stellt zwei Behauptungen aus v3.16 richtig, die sich als falsch erwiesen haben: 1. "Die Namen der Hardware-Presets sind auf der Rippy-VM nicht ermittelbar." Falsch - `--preset-list` nennt sie vollstaendig, auch ohne Hardware-Encoder. Der vermeintliche Blocker fuer Punkt 1 existierte nicht. 2. "Rohschnitt intakt -> 'Neu komprimieren' genuegt." Falsch - can_retry war false, den Knopf gab es nicht. AGENTS.md bekommt zwei neue Lehren aus dieser Sitzung: Hintergrund-Schleifen muessen ihre Fehler melden (ein stiller except hat eine Stunde gekostet), und Netz-Pfade nie ungebremst anfassen - os.path.isdir haengt auf einem toten CIFS im Kernel und laesst sich aus Python nicht abbrechen. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 23 ++++- SAVEPOINT.md | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 257 insertions(+), 2 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 7cf3e0a..4ff3c41 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen. - Kein Proxmox-LXC-Nesting-Workaround — das Projekt lebt bewusst in normalem Docker. - Kein ARM-Fork — Rippy ist ein Eigenbau von Grund auf. -## Aktueller Stand (25.07.2026) +## 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, @@ -73,6 +73,11 @@ Bibliotheks-APIs: `--help`/Doku prüfen und die Fundstelle im Commit nennen. - ✅ **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) - 📝 **Details immer in SAVEPOINT.md** — diese Sektion nennt nur die Etappe ## Was diese Sitzungen wiederholt gekostet hat @@ -87,3 +92,19 @@ 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//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. + +**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 ` (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. diff --git a/SAVEPOINT.md b/SAVEPOINT.md index c720a34..3bc6c76 100644 --- a/SAVEPOINT.md +++ b/SAVEPOINT.md @@ -1,6 +1,240 @@ # SAVEPOINT — Rippy -## Aktueller Stand: v3.16 — ÜBERGABE: externes Encoden konnte nie funktionieren (26.07.2026) +## Aktueller Stand: v3.17 — der Blocker ist zu, neun Punkte abgearbeitet (26.07.2026) + +> **Deployt und live gegengeprüft.** Auftrag war „lies den Savepoint und lass uns +> das Projekt endlich beenden". Die neun Commander-Punkte aus v3.16 sind +> abgearbeitet; unterwegs kamen vier Bestandsfehler dazu, die vorher niemand +> gesehen hat. Was offen bleibt, steht ganz unten — und es ist wenig. + +### ZUSTAND, gemessen + +| | | +|---|---| +| Repo + VM | `87537bf`, Ampel **grün**, deployt | +| Container | alle 5 `Up`, api/postgres/redis `healthy` | +| Tests | **211** grün (vorher 130) | +| Job `95afdc89` | `failed`, aber **`can_retry = true`** — die 74,1 GB sind erstmals über das UI erreichbar | +| Rohschnitt | `/app/media/rippy/95afdc89-…/title_t00.mkv`, 79.604.951.639 Bytes, intakt | +| Platte VM | 148 GB, 107 GB frei | +| NAS-Freigabe | eingehängt und antwortend — **aber siehe „NOCH OFFEN" Punkt 1** | + +### DER BLOCKER IST ZU — und er brauchte keine Eingabe vom Nutzer + +`RIPPY_PATH_MAP` wurde von niemandem gesetzt; deshalb konnte externes Encoden +**nie** funktionieren. Die Lösung musste nichts erfinden: **Rippy hat die +Freigabe selbst eingehängt und kennt ihre Quelle.** Mountpunkt plus Quelle IST +das Mapping. + +``` +GET /worker-setup/pfad-map + → /app/media/rippy=\\192.168.178.62\rippy +``` + +Live auf der VM abgefragt, Antwortzeit 0,010 s. Beide Windows-Installer holen +diesen Wert selbst, prüfen mit `Test-Path`, ob **dieser PC** die Freigabe +erreicht, und schreiben ihn in `start-tray.bat` und `start-worker.bat`. Der +Commander muss nichts über Container-Pfade wissen — das Feld bleibt leer. + +Ist keine Freigabe eingehängt, sagt der Endpunkt das im Klartext samt Abhilfe, +statt ein leeres Mapping zu liefern. + +**Was der Commander jetzt tun kann:** `RippyWorkerSetup.exe` neu holen +(Einstellungen → Worker) und laufen lassen. Danach steht bei „Neu komprimieren" +sein Ryzen mit avx512f und der RX 9070 XT zur Wahl, und die 74 GB gehen durch, +ohne die Disc noch einmal einzulegen. + +### DIE NEUN PUNKTE — Stand jetzt + +| Sein Punkt | Stand | +|---|---| +| 1. „Bei den Presets soll IMMER das Beste ausgewählt werden" | **erledigt** — Knopf „Bestes wählen", Begründung unter jeder Liste | +| 2. „Hier steht noch meine Settings drin" | erledigt (v3.16) | +| 3. „Vektorbefehle müssen ausgelesen werden" | erledigt (v3.16) | +| 3b. „…wenn der Worker AV1 oder besseres kann, immer diesem empfehlen" | **erledigt** — Hardware-Presets nach Familie, AV1 vor H.265 | +| 3c. „…Warnung verschwindet / orange zu grün" | erledigt (v3.16) | +| 4. „JIKAN ist drin — wird das genutzt?" | **beantwortet: ja** — und ab jetzt an der richtigen Stelle in der Kette | +| 5. „Was wenn der Unterbau nicht Debian ist?" | **erledigt** — Paketwerkzeug wird erkannt | +| 6. „Mein PC hat ne dicke Grafikkarte" | **erledigt** — Erkennung (v3.16) + Presets + Pfad-Mapping, die Kette ist vollständig | +| 7. „Warum wird der Datei-Browser noch angezeigt" | **erledigt** — eingeklappt | +| 8. „Server Status muss dringend überarbeitet werden" | **erledigt** — zwei Placebos entfernt | +| 9. „ETA im externen Worker und im Dashboard" | **erledigt** — Restzeit aus gemessenem Fortschritt | + +### GEBAUT — was das Raten beendet + +**Presets kommen vom Worker, nicht aus dem Quelltext.** `caps.py` meldet +`HandBrakeCLI --preset-list` (auf der VM: **90 Namen**), `api/presets.py` +staffelt daraus die Empfehlung. Kein Punktesystem: für jede Lage eine feste +Reihenfolge echter Namen, genommen wird der erste, den der Worker kennt. + +> ⚠️ **Richtigstellung zu v3.16:** Dort stand, die Namen der Hardware-Presets +> seien auf der Rippy-VM „nicht ermittelbar", weil deren HandBrake keinen +> Hardware-Encoder hat. **Gemessen ist das falsch** — die Kategorie `Hardware/` +> steht vollständig in der Liste (`H.265 VCN 2160p 4K`, `AV1 QSV 2160p 4K`, +> NVENC, MF). HandBrake trennt zwei Fragen: `--preset-list` nennt alle Presets, +> `--help` nur die nutzbaren Encoder. Zwei Fragen, zwei Quellen. Der vermeintliche +> Blocker existierte nicht. + +Feinheiten, die zählen: Der Encoder heißt `vce`, das Preset heißt **VCN** — ohne +diese Zuordnung liefe die AMD-Karte unter einem Intel-Namen. `vaapi` bekommt +bewusst **kein** Hardware-Preset (HandBrake 1.6.1 liefert keines mit), `MF` wird +nie empfohlen (Nutzbarkeit nicht ablesbar), und für DVD-Auflösungen gibt es +keine Hardware-Presets. Kennt ein Worker keinen der bekannten Namen, sagt das UI +„selbst wählen" statt etwas Falsches einzustellen. Und HandBrakes +`Invalid preset ` wird jetzt übersetzt — vorher stand da „Code 3". + +**Dashboard: zwei Placebos raus.** Die Karte hieß „Echte Live-Daten" und log an +zwei von drei Stellen. „Auslastung: 0 % (Aktiv)" war nicht die CPU-Last, sondern +der Job-Fortschritt (bzw. eine feste 15 bzw. 5). Die Verlaufskurve daneben war +`Math.random()`. Und „N Worker Online" zählte registrierte Einträge, auch Leichen +alter Rebuilds. Jetzt: laufende Phase mit **Restzeit**, jeder erreichbare Worker +mit Kernen/Vektorbefehlen/extern, freier Platz mit Warnung, wenn er nicht mehr +für eine Disc reicht. + +**Restzeit ehrlich.** Messreihe je Phase (Rip und Kompression haben nichts +miteinander zu tun), Stillstand verlängert die Schätzung, unter zwei Messwerten +oder 60 Sekunden Spanne gibt es **keine** Aussage — dann steht „Restzeit wird +gemessen". Mit den echten 25.07.-Werten gegengerechnet: 1,44 % in 29 min ergibt +„noch ca. 1 Tag 23 h". + +**Warnung VOR dem Start.** Wählt man einen externen Encoder, der Quelle oder Ziel +nicht erreicht, steht das im Rip-Dialog — nicht erst nach einer Stunde. Bei +„Automatisch" wird genannt, welcher Worker die Aufgabe kaputtmachen könnte. + +**Metadaten: exakt schlägt unscharf.** Das Gate 0,55 war nachrechenbar, weil +`titel_aehnlichkeit` rein ist: + +``` +"Alien" vs. "Alien 9" → 0,83 TRIFFT +"Inception" vs. "Deception" → 0,78 TRIFFT +"Hero" vs. "Heroman" → 0,73 TRIFFT +"The Dark Knight" vs. "Dark Knight Rises" → 0,69 TRIFFT +``` + +Ein Kinofilm, den TMDB verpasst, bekam damit Anime-Metadaten, und OMDb wurde nie +gefragt. **Und die Vermutung „das Gate ist zu großzügig" ist trotzdem falsch:** +Für den Fall, für den Jikan eingebaut wurde, ist es zu **streng** — +„Evangelion 2.22" gegen den MAL-Titel ergibt 0,51 und fällt durch. Eine einzelne +Zahl kann beides nicht leisten. Jetzt zwei Schwellen: nur ein praktisch exakter +Titel (≥ 0,9) beendet die Kette, ein unscharfer Treffer wird gemerkt, dann kommt +OMDb, und erst wenn OMDb nichts hat, gilt er als **Vorschlag** (0,6). + +### VIER BESTANDSFEHLER, die beim Gegenprüfen auffielen + +Alle vier durch **Messen an der laufenden Instanz** gefunden, nicht durch Lesen. + +**1. „Neu komprimieren" ging überhaupt nicht.** Der Savepoint v3.16 schrieb: +„Rohschnitt 79,6 GB intakt → ‚Neu komprimieren' genügt". Gemessen war +`can_retry = false` — **den Knopf gab es nicht.** Gesucht wurde nur in +`/app/temp/raw` und unter dem *aktuellen* `workDir`; der Rip lag aber auf der NAS, +weil beim Start eine Wahl **nur für diesen Rip** getroffen worden war, und die +Einstellung stand auf leer. Dasselbe in `retry_transcode`: es hätte am falschen +Ort gesucht. Neues Modul `rohdaten.py` sieht jetzt an allen Orten nach, die +überhaupt in Frage kommen. **Wieder derselbe Fehler:** aus einem Zustandswert +(der heutigen Einstellung) auf einen Mechanismus (wohin damals gerippt wurde) +geschlossen. + +**2. `os.path.isdir` hing im KERNEL und tötete eine Hintergrund-Schleife.** +Zwei Threads des API-Prozesses standen im Zustand **D** (uninterruptible sleep). +Die Rohdaten-Schleife startete ihren ersten Durchlauf, während Rippy die +CIFS-Freigabe neu einhängte — `asyncio.to_thread` kam nie zurück, die Schleife +erreichte ihr `sleep` nie und war **für immer tot**. Ein Timeout um den Aufruf +hätte nichts geholfen: ein im Kernel hängender Thread lässt sich aus Python nicht +abbrechen. Ein Kind-**Prozess** lässt sich abbrechen — geprüft wird jetzt mit +`timeout 4 ls -d`, dasselbe Werkzeug, das `mounts.ist_erreichbar` seit dem +24.07. benutzt. + +**3. „Konnte nicht nachsehen" wurde als „ist weg" gewertet.** Nach einem +Container-Neustart stallt der erste Zugriff auf die Freigabe; in diesem Fenster +verschwand der Knopf „Neu komprimieren", obwohl 74 GB dalagen. Jetzt drei +Antworten statt zwei (`da` / `weg` / `unklar` — 124 ist der Rückgabewert von +`timeout`), und bei „unklar" wird die letzte bekannte Antwort gehalten. + +**4. Der NAS-Mount kam nach einem Rebuild nicht zurück.** `mounten()` glaubte +`os.path.ismount` — und der Mountpunkt existiert im neuen Container weiter +(Bind-Mount vom Host), also brach das Wiederherstellen genau dort ab. Dazu öffnet +`schreibtest()` eine Datei **ohne Zeitgrenze**: auf einem toten CIFS blockiert das +im Kernel. Jetzt entscheidet `ist_erreichbar()` zuerst, und nach dem Mount wird +geprüft statt geglaubt (zwei Versuche, dann ehrlicher Fehler). + +### DIE FALLE, DIE DIE MEISTE ZEIT KOSTETE + +**Ein `except Exception: pass` in einer Hintergrund-Schleife.** Der Vorrat blieb +leer, die Funktion lief direkt aufgerufen einwandfrei, und **warum** sie im +Server nichts tat, war nirgends zu sehen. Erst ein Blick auf die Thread-Zustände +in `/proc//task/*/stat` brachte das D. Konsequenzen, beide gebaut: Die +Schleife **meldet** ihren Fehler jetzt, und es gibt `GET /health/vorraete` — sagt +für Ping- und Rohdaten-Vorrat, wie alt der letzte Durchlauf ist. Bleibt so ein +Vorrat leer, ist am Endpunkt selbst nämlich **nichts** zu sehen; er antwortet nur +dauerhaft „nichts gefunden". + +Merksatz für die nächste Sitzung: Ein Hintergrund-Prozess, der still scheitert, +ist schlimmer als einer, der laut scheitert. + +### NOCH OFFEN + +**1. Die NAS-Freigabe antwortet nach einem Container-Neustart oft erst beim +zweiten Anlauf — Ursache liegt beim NAS, nicht bei Rippy.** Gemessen: `mount` +meldet Rückgabewert 0, `/proc/mounts` zeigt genau eine korrekt aussehende Schicht +mit den richtigen Optionen, `ist_erreichbar` bekommt direkt danach eine Antwort — +und Sekunden später läuft jeder Zugriff in die Zeitgrenze. +`POST /storage-mounts/rippy/repair` stellt sie dann in ~30 s her, +reproduzierbar. Das NAS ist per Ping bei 0,47 ms erreichbar. Rippy geht damit +jetzt sauber um (harte Zeitgrenzen, zwei Versuche, ehrliche Meldung, +Selbstheilung binnen 30 s), aber **die eigentliche Ursache ist nicht gefunden** — +Verdacht: das NAS mag sieben Mount-Zyklen in zwanzig Minuten nicht. Zu prüfen, +wenn es im Alltag wieder auftritt: SMB-Protokollversion und Sitzungsgrenzen am +NAS, und ob ein `repair`-Aufruf beim API-Start als Automatik sinnvoll ist. + +**2. Der externe Worker des Commanders läuft noch mit altem Code.** Er meldet +`simd=unbekannt`, `presets: 0`, `pfad_map: None` und stand bei der Prüfung auf +`online=false`. Alles, was diese Sitzung gebaut hat, wirkt für ihn erst nach +einem Lauf des neuen Installers. + +**3. Live-Gegenprobe der Metadaten-Kette steht aus.** MyAnimeList war während +der ganzen Sitzung weg (Jikan antwortete durchweg HTTP 504). Die Arithmetik des +Gates ist davon unberührt und in `test_jikan_helpers.py` festgehalten; ein echter +Anime-Rip muss es noch bestätigen. + +**4. Aus dem ARM-Vergleich, unverändert nicht gebaut:** ISO-Sicherung für +Datenträger, die weder Film noch Audio-CD sind, und **mehrere Laufwerke +gleichzeitig** (Rippy sieht sie, ob parallel gerippt wird, ist unbewiesen). + +**5. Ältere Punkte, unverändert gültig:** +- Discord-Webhook und MakeMKV-Key liegen im **Klartext** in der settings-Tabelle. + Für Heimnetz erwartbar, aber der Webhook ist eine echte Zugangsberechtigung. +- **VM-CPU-Typ steht auf `qemu64`** → `host` würde AVX2 freischalten, 2–4× + schneller. Anleitung in der README („Rippy schneller machen"). Ein + VM-Neustart, nicht gemacht — das ist eine Entscheidung des Commanders. +- **`install.sh` ist nie als root durchgelaufen.** Verzeichnisse anlegen, + `mount --make-rshared` und die systemd-Unit bleiben ungetestet; sie laufen erst + bei einer echten Neuinstallation. +- Es gibt **kein TypeScript-Typcheck** im Projekt (`typescript` ist nicht + installiert, `npm run build` transpiliert nur mit esbuild). Typfehler im UI + fallen nirgends auf. Bewusst nicht geändert: `tsc` in die Ampel zu nehmen färbt + sie womöglich wegen Bestandscode rot, und das ist eine Entscheidung. + +### FALLEN DIESER SITZUNG (für die nächste) + +- **Ein Prozess-Zustand `D` in `/proc//task/*/stat` heißt: im Kernel + blockiert.** Das ist der schnellste Weg, einen hängenden Netz-Zugriff zu + finden — schneller als jedes Log. +- **`os.path.join` für Container-Pfade ist unter Windows falsch** + (`/app/media\rippy`). Dritter Fund derselben Falle; diesmal hat ein Test sie + gefunden, nicht die Produktion. Immer `posixpath` für Pfade, die in Container + gehen. +- **Ein Umlaut-Werkzeug hat `install.ps1` von CRLF auf LF gestellt** — 470 + Zeilen Diff-Rauschen für eine Änderung von 56. Nach automatisierten + Textänderungen an `.ps1` immer `file` gegenprüfen (BOM **und** CRLF). +- **Eigene Testerwartungen sind auch Behauptungen.** Zweimal lag der Test falsch + und der Code richtig (Confidence 0,3 statt 0,0; „1 Tag 23 h" statt „mehr als 2 + Tage"). Beide Male hat die Ampel bzw. der lokale Lauf es gefangen. +- **Ein Test, der Quelltext-Positionen vergleicht, findet seine eigene Erwähnung + im Kommentar.** Verhalten prüfen, nicht Zeichenketten. + +--- + +## Vorheriger Stand: v3.16 — ÜBERGABE: externes Encoden konnte nie funktionieren (26.07.2026) > **Zuerst lesen.** Diese Sitzung endete wegen vollem Kontext. Der Auftrag war > Commander-Feedback aus der ersten Selbst-Einrichtung plus die Diagnose eines