docs(savepoint): v3.17 - neun Punkte zu, vier Bestandsfehler dazu
Ampel / ampel (push) Successful in 27s

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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 13:55:41 +02:00
parent 87537bf3e5
commit 2587fe63af
2 changed files with 257 additions and 2 deletions
+22 -1
View File
@@ -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/<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.
**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.
+235 -1
View File
@@ -1,6 +1,240 @@
# SAVEPOINT — Rippy
## Aktueller Stand: v3.16ÜBERGABE: externes Encoden konnte nie funktionieren (26.07.2026)
## Aktueller Stand: v3.17der 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 <Name>` 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/<pid>/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, 24×
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/<pid>/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