60c0f93237f60ca7b6479062192bfa117ee16147
100
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
60c0f93237 |
fix(windows): leere Symbole und der verschwundene Eintrag in "Installierte Apps"
Ampel / ampel (push) Successful in 54s
Beides vom Commander gemeldet, beides gemessen statt vermutet.
## 1. Weisses leeres Blatt statt Symbol
In deploy/worker-windows/rippy.ico steckte GENAU EIN Bild:
Bilder = 1
256x256 32 bit 5657 Bytes PNG
Windows holt sich fuer jede Stelle die passende Groesse -- 16x16 fuer die
Taskleiste, 32/48 fuer Desktop und Startmenue. Fehlt sie, skaliert die Shell
nicht zuverlaessig selbst, sondern zeigt das leere Blatt. Bei einer Datei mit
nur einem PNG-komprimierten 256er passiert das regelmaessig.
packaging/windows/icon.py baut die Datei jetzt mit neun Groessen
(16/20/24/32/40/48/64/128/256, LANCZOS beim Verkleinern). Nachgemessen an
der neuen EXE: 16x16 mit 138 Farben, 32x32 mit 264 -- kein leeres Blatt mehr.
build.py laesst ein unvollstaendiges Symbol gar nicht mehr durch, und
test_icon.py haelt die Pflichtgroessen fest (mit dem alten Stand rot gesehen).
## 2. Rippy stand nicht in "Installierte Apps"
Die Ursache war MEINE Testsuite. Gemessen:
Rippy.exe --nicht-starten -> Eintrag: DA (Rippy 2.0.0)
pytest -q -> Eintrag: WEG
test_installation_legt_die_dateien_an_... schrieb in den ECHTEN
Uninstall-Schluessel und loeschte ihn im finally wieder -- also auch den des
Commanders. Der Autostart-Eintrag ging denselben Weg.
Der Schluesselname stand als VORGABEWERT in jeder Signatur, und Vorgabewerte
wertet Python zur Definitionszeit aus: Ein Test konnte ihn gar nicht umbiegen.
Jetzt steht dort None, aufgeloest zur Laufzeit -- damit wirkt ein
monkeypatch.setattr(reg, "SCHLUESSEL", ...) ueberall.
Dazu ein Waechter (test_die_testsuite_ruehrt_den_ECHTEN_eintrag_nicht_an):
Er merkt sich den echten Zustand vorher, laesst eine vollstaendige
Test-Installation samt Autostart laufen und prueft danach, dass sich am
echten Eintrag nichts geaendert hat.
Das ist derselbe Fehler wie vorhin bei den Verknuepfungen, nur an anderer
Stelle. Die Regel gehoert in den Code, nicht in meinen Kopf: **Ein Test
raeumt nur weg, was er selbst angelegt hat.**
Ampel lokal: 590 gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e1984dc543 |
docs: SAVEPOINT — Ampel wieder gruen, und warum sie fuenf Laeufe rot war
Ampel / ampel (push) Successful in 54s
Nachgetragen: die 28 Linux-Fehler, der schwerste davon (der API-Container waere auf der VM gar nicht hochgekommen), und die Lehre daraus — ein Test, der nur auf einer Plattform greift, ist eine halbe Zusage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ca212ff835 |
fix(test): die detection-Attrappe muss auch das PAKET-Attribut ersetzen
Ampel / ampel (push) Successful in 54s
Ampel-Lauf 186: Die 28 echten Linux-Fehler waren weg, uebrig blieben fuenf
— alle in meinem neuen Test:
TypeError: drive_status() missing 1 required positional argument
`from rippy.drives import detection` sieht zuerst im PAKET nach
(`_handle_fromlist` prueft `hasattr`) und greift erst dann auf sys.modules
zurueck. Auf Linux ist das echte Modul dort laengst gesetzt, weil es
importierbar ist — die Attrappe blieb wirkungslos, und der Test lief in die
echten Funktionen.
Unter Windows fiel das nicht auf: Dort gibt es das Attribut mangels fcntl
gar nicht, also griff sys.modules. Derselbe Test, zwei Plattformen, zwei
Ergebnisse — genau die Sorte Luecke, die dieser Test eigentlich schliessen
soll.
Jetzt werden beide ersetzt. Gegengeprueft: mit kaputter Durchreiche fuenf
Fehlschlaege, danach wieder gruen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
bfc0cada62 |
fix(windows): kein CMD-Fenster mehr — die EXE ist ein Fenster-Programm
Ampel / ampel (push) Failing after 55s
Commander: "Unterbinde das CMD Fenster was sich beim oeffnen mitoeffnet."
Mein erster Anlauf wollte die Konsole VERSTECKEN und war falsch, aus zwei
Gruenden:
* Eine Onefile-EXE ist ZWEI Prozesse (gemessen: PID 33172 mit 8 MB, PID
33580 mit 103 MB, beide Rippy.exe). Die Pruefung "haengt genau EIN Prozess
an der Konsole" war damit nie wahr, das Fenster blieb einfach stehen.
* Selbst wenn sie ginge, waere das Fenster erst nach ein bis zwei Sekunden
weg. Ein Aufblitzen ist kein "unterbunden".
Jetzt wird die Konsole gar nicht erst erzeugt: --windowed statt --console.
Nachgemessen am fertigen Bau -- PE-Subsystem 2 (GUI) statt 3 (Konsole), und
beim Klick auf die Desktop-Verknuepfung ist genau EIN sichtbares Fenster da:
die WinForms-Oberflaeche. Kein ConsoleWindowClass-Fenster von Rippy, weder
sichtbar noch unsichtbar.
Was dazugehoert, damit daraus kein stiller Fehlschlag wird:
* Ohne Konsole ist sys.stdout None. uvicorn schreibt auf stderr und waere
mitten im Start gestorben, ohne dass irgendwo etwas stuende. einstieg.py
haengt sich deshalb an die Konsole des Aufrufers (--status in PowerShell
schreibt weiter dorthin) oder leitet nach %LOCALAPPDATA%\Rippy\rippy.log um.
* Der Installer meldet sein Ergebnis in einem Fenster, wenn keine Konsole da
ist -- sonst saehe ein gescheiterter Doppelklick aus wie einer, der nichts
tut. Bei --still und mit Konsole bleibt es bei der Textzeile.
Gegengeprueft am laufenden Programm: /api/health, / und /api/capabilities
antworten mit 200, und rippy.log faengt auf, was vorher ins Leere ging.
fix(drives): linux.py reicht die detection-Funktionen wieder durch
Die Ampel war seit Lauf 181 rot -- vier Commits lang, und ich hatte es
uebersehen. Ursache:
docker/api/main.py:54
AttributeError: module 'rippy.drives.linux' has no attribute 'drive_status'
main.py holt sich beim Import device_discovery.drive_status. Der
Windows-Treiber hat die Funktion; linux.py hatte sie nicht mehr, seit
detection (und damit fcntl) bewusst nicht mehr oben importiert wird -- sonst
waere main.py unter Windows nicht ladbar. Unter Windows lief also alles, auf
Linux waere der API-Container gar nicht hochgekommen.
Behoben mit PEP 562 (__getattr__), demselben Kniff wie in store/__init__.py:
Der Import bleibt ohne fcntl moeglich, wer die Funktionen BENUTZT bekommt sie.
Dazu drei weitere Fehler, die nur auf Linux auftraten:
* katalog.py baute Windows-Pfade mit os.path.join. Auf dem Linux-Runner wurde
daraus C:\Program Files (x86)\MakeMKV/makemkvcon64.exe. Jetzt entscheidet
der Pfad selbst ueber den Trenner, nicht die rechnende Maschine.
* verknuepfungen.py nahm os.path.dirname fuer den Arbeitsordner einer .lnk.
Eine .lnk zeigt IMMER auf einen Windows-Pfad -- also ntpath.
* waechter.py las 0.0 als Zeitpunkt statt als "noch nie geprueft". Auf einer
frisch gestarteten Maschine ist time.monotonic() klein, also fiel die erste
Laufwerks-Abfrage aus. None statt 0.0.
Alle vier haben jetzt Tests, die auf JEDER Plattform laufen -- der
Treiber-Test schiebt eine detection-Attrappe unter, der Waechter-Test stellt
eine Uhr bei 0,5 s nach, die Pfad-Tests pruefen beide Zweige. Beim
Wegnehmen der Durchreiche rot gesehen (5 Fehlschlaege), danach wieder gruen.
Damit faengt die Ampel so etwas kuenftig, ohne dass es eine Linux-Umgebung
auf dem Entwicklungsrechner braucht.
Ampel lokal: 582 gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e1577a4f47 |
docs: SAVEPOINT v4.0-rc — Windows ist ein echtes Programm
Ampel / ampel (push) Failing after 55s
Der SAVEPOINT stand seit V2-3 still, waehrend elf Commits dazukamen. Nachgetragen mit den gemessenen Zahlen: 562 gruen, RippySetup.exe 31,7 MB, WebView2 151.0.4129.107, 5 Encoder auf diesem PC. Zwei Dinge stehen bewusst gross drin, weil sie sonst verloren gehen: * Der Dienst, der /api/health mit 200 beantwortete und / mit 404 -- und warum ein %TEMP%-Verzeichnis kein Ort fuer eine Oberflaeche ist. * Der Test, der den PC des Commanders getroffen hat. Ein Test, der Spuren ausserhalb von tmp_path hinterlaesst, ist kein Test, sondern ein Eingriff. Und was NICHT bewiesen ist, steht als solches da: Ein echter Rip unter Windows ist nie gelaufen, die VM haengt elf Commits zurueck. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c01ef1081c |
feat(windows): echtes Fenster statt Browser-Tab (WebView2 statt Electron)
Auf die Frage des Commanders: "Warum nutzen wir fuer Windows weiterhin
einen Browser? Warum nutzen wir kein Electron oder sowas und machen daraus
einen echten Client."
Die erste Haelfte trifft zu und ist umgesetzt. Die zweite ist abgelehnt --
gemessen, nicht geschaetzt:
pywebview + WebView2 8,0 MB in der Bau-Umgebung, 2,7 MB in der EXE
Electron 150-210 MB, dazu eine zweite Laufzeitumgebung
Windows 11 bringt die WebView2-Laufzeit mit (hier 151.0.4129.107).
Nachgemessen statt angenommen, was sie rendert:
navigator.userAgent ...Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0
fetch ja EventSource ja CSS Grid ja Pfeilfunktionen ja
Dasselbe Chromium, das auch in Electron steckt -- nur ohne es ein zweites
Mal mitzuschleppen. RippySetup.exe waechst von 29,0 auf 31,7 MB.
Drei Dinge gehoeren dazu, nicht als Beiwerk:
* Kein stiller Rueckfall auf MSHTML. Der alte IE-Renderer stellt die
React-Oberflaeche nicht dar. Fehlt WebView2, oeffnet Rippy den Browser
und SAGT warum -- ein leeres Fenster waere schlimmer als ein Tab.
* Fenster und Tray sind getrennte Prozesse. pystray belegt unter Windows
den Haupt-Thread, das Fenster braucht ihn genauso.
* Die eigene Konsole wird versteckt, eine geerbte nie. GetConsoleProcessList
unterscheidet beides: genau ein Prozess an der Konsole heisst, sie
gehoert uns. Ohne diese Unterscheidung haette "Rippy.exe --status" im
Terminal das Terminal des Nutzers verschwinden lassen.
fix(windows): Oberflaeche neben das Programm legen statt aus %TEMP% bedienen
Beim Nachsehen im laufenden Betrieb gefunden: Ein Dienst lief eine Stunde,
/api/health gab HTTP 200, / gab 404. Im Fenster stand {"detail":"Not Found"}.
Der Server war in Ordnung. Sein Entpack-Verzeichnis war es nicht:
_MEI000074b02 31 Eintraege, 5 Ordner -- kein ui, kein api
_MEI000082e82 44 Eintraege, 16 Ordner -- vollstaendig
Eine PyInstaller-Onefile-EXE liest bei JEDER Anfrage aus %TEMP%\_MEIxxxxx.
Ein Temp-Verzeichnis ist kein Ort fuer etwas, das eine Woche liegen bleibt.
Und der Ausfall ist der schlimmstmoegliche: Die API antwortet weiter, der
Dienst gilt als gesund, nur die Oberflaeche ist weg.
Genau davor warnte ROADMAP.md beim Bau-Verfahren ("one-dir statt one-file").
Die Abweichung bleibt, die Luecke wird geschlossen: Der Installer legt die
Oberflaeche neben das Programm, daemon._ui_pfad() nimmt diese Kopie zuerst.
Zeigt sich derselbe Ausfall an den API-Modulen, ist one-dir die Antwort.
Ampel: 562 gruen, ruff sauber. Fenster mit der echten Oberflaeche im
Bildschirmfoto nachgewiesen, nicht nur der Titel geprueft.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
069fc46f44 |
fix(test): der Installations-Test hat die ECHTEN Verknuepfungen ueberschrieben
Ampel / ampel (push) Failing after 47s
WAS: `test_installation_legt_die_dateien_an_...` installiert in ein
tmp_path — legt aber seit dem Verknuepfungs-Einbau auch Symbole auf dem
ECHTEN Desktop und im ECHTEN Startmenue an. Die kennen kein tmp_path.
DIE FOLGE, vom Commander gemeldet: Er klickte auf das Desktop-Symbol und
bekam von Windows "Diese App kann auf dem PC nicht ausgefuehrt werden".
Nachgemessen zeigte die Verknuepfung auf
...\Temp\pytest-of-TobisPC\pytest-142\test_installation_..._0\installiert\Rippy.exe
— und dort liegt die 2-KB-ATTRAPPE aus dem Test, kein Programm. Windows
hatte voellig recht.
Der `finally`-Block raeumte nur Registry und Autostart auf; die
Verknuepfungen nicht, weil es sie beim Schreiben des Tests noch nicht gab.
Als sie dazukamen, wuchs der Test stillschweigend ueber sein tmp_path
hinaus. Ein Test, der Spuren ausserhalb seines Temp-Ordners hinterlaesst,
ist kein Test, sondern ein Eingriff.
BEHOBEN mit ZWEI Sicherungen statt einer:
1. verknuepfen=False — die Funktion legt gar keine an.
2. vk.alle_anlegen ist trotzdem ersetzt. Wer den Schalter spaeter
umdreht oder die Vorgabe aendert, kann damit trotzdem nichts am
echten System anrichten.
Dazu ein Waechter: test_installation_ruehrt_die_echten_verknuepfungen_
nicht_an prueft, dass die Anlege-Funktionen bei verknuepfen=False GAR
NICHT gerufen werden. Waere er frueher dagewesen, haette der Commander
keine kaputte Verknuepfung bekommen.
DER RECHNER IST REPARIERT: neu installiert, beide Verknuepfungen zeigen
wieder auf C:\Users\...\AppData\Local\Rippy\Rippy.exe (29 MB, existiert),
Programme+Features-Eintrag ist zurueck. Start ueber die Verknuepfung:
HTTP 200 nach 2 s, Worker TobisNicerPC mit cpu-x264/x265/av1 + vce/vce-av1,
MakeMKV 1.18.4 und HandBrake 1.11.2 gefunden.
Nebenbei ausgeschlossen, bevor die Ursache klar war: Die EXE selbst ist in
Ordnung (gueltiges x64-PE, MZ/PE-Kopf, 29 MB, Hash identisch mit dem
Installat), kein Mark-of-the-Web, kein Defender-Fund, Smart App Control
aus. Der Fehler lag nicht an der Datei, sondern daran, WORAUF gezeigt wurde.
GEMESSEN: ruff sauber, 522 Tests gruen + 15 uebersprungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
99c05865af |
feat(windows): Desktop-Symbol und Startmenue-Eintrag — plus --oeffnen
Ampel / ampel (push) Failing after 47s
WAS: Das Setup legt jetzt eine Verknuepfung auf dem Desktop UND im
Startmenue an, die Deinstallation nimmt beide zurueck, und `--status`
zeigt, ob sie da sind. Abwaehlbar mit --keine-verknuepfungen.
WARUM (Commander-Meldung): "es gibt kein icon aufm desktop und keinen
startmenü eintrag". Zu Recht — ein Hintergrundprogramm ohne Symbol ist
praktisch unauffindbar: Es laeuft, aber es gibt keinen Weg hinein ausser
der Adresse im Kopf.
DIE VERKNUEPFUNG ZEIGT AUF --oeffnen, NICHT AUF --dienst. Ein Doppelklick
soll Rippy OEFFNEN; ob der Dienst laeuft, ist dem Nutzer egal und er kann
es auch nicht wissen. Zeigte das Symbol auf --dienst, startete jeder Klick
einen ZWEITEN Server, der am belegten Port scheitert — das Fenster blitzt
auf, im Browser passiert nichts. Ein Symbol, das beim zweiten Klick nichts
tut, ist schlimmer als keins.
`--oeffnen` sieht deshalb erst NACH, ob jemand antwortet, startet nur wenn
noetig, und wartet dann, bis der Dienst wirklich da ist — statt sofort
einen Browser auf eine tote Adresse zu schicken. Gemessen: erster Aufruf
2 s bis HTTP 200, zweiter Aufruf keine zusaetzlichen Prozesse.
DIE ORDNER KOMMEN AUS DER REGISTRY, nicht aus %USERPROFILE%. Wer OneDrive
benutzt oder seine Ordner verschoben hat, dessen Desktop liegt woanders —
die Verknuepfung landete dann in einem Ordner, den niemand sieht, und der
Nutzer suchte ein Symbol, das es nirgends gibt.
UEBER POWERSHELL statt pywin32: Eine .lnk ist ein binaeres Shell-Objekt.
pywin32 koennte es, waere aber 20 MB fuer vier Zeilen. Denselben Weg geht
der alte Worker-Installer schon ("nativ via PowerShell").
ZWEI FEHLER, DIE DIE TESTS GEFUNDEN HABEN:
- os.makedirs stand AUSSERHALB des try. Auf einem Laufwerk, das es nicht
gibt, wirft es — aus "Verknuepfung konnte nicht angelegt werden" waere
ein Abbruch der ganzen Installation geworden.
- Der Test dafuer nahm fest Z: als "gibt es nicht". Auf dem Commander-PC
ist Z: ein NETZLAUFWERK (X, Y, Z sind belegt). Ein fest verdrahteter
Laufwerksbuchstabe ist eine Annahme ueber eine fremde Maschine; jetzt
wird zur Laufzeit ein freier gesucht.
Und der Apostroph: In PowerShell wird er innerhalb einfacher
Anfuehrungszeichen durch VERDOPPELN maskiert. Ohne das braeche ein Pfad wie
C:\Users\O'Brien\... das Skript entzwei, und der Fehler saehe aus wie ein
Syntaxfehler in Rippy.
GEMESSEN, nach echter Installation nach %LOCALAPPDATA%\Rippy:
Desktop-Symbol -> Rippy.exe --oeffnen, mit Symbol und Beschreibung
Startmenue -> ebenso
Programme+Feat. -> Rippy 2.0.0, 100 MB, Deinstallieren funktioniert
Autostart -> "...\Rippy.exe" --dienst
Start ueber das Symbol: HTTP 200 nach 2 s
Worker: TobisNicerPC, Encoder cpu-x264/x265/av1, vce, vce-av1
GEMESSEN: ruff sauber, 521 Tests gruen + 15 uebersprungen (vorher 512).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f4a8d77598 |
feat(windows): Rippy arbeitet eigenstaendig — Rippen, Werkzeuge, Fähigkeiten
Ampel / ampel (push) Failing after 48s
WAS: Der Ablauf ist aus tasks.py heraus (ablauf.py, ohne Celery), die
LocalQueue wird bedient, ein Laeufer arbeitet Auftraege im selben Prozess
ab, und Rippy meldet sich mit gemessenen Faehigkeiten selbst als Arbeiter.
WARUM: "Rippy fuer Windows soll standalone funktionieren" (Commander). Bis
hierher konnte die Windows-App alles ANZEIGEN und nichts TUN — ein Rip waere
eingereiht worden und fuer immer liegengeblieben, weil niemand ihn holt.
DIE TRENNUNG: rip_disc hing an GENAU DREI Celery-Stellen in 234 Zeilen —
self.update_state, _transcode_queue, transcode_files.apply_async. Alle drei
sind Fragen der ZUSTELLUNG, nicht des Ablaufs. Sie sind jetzt Rueckrufe:
tasks.py reicht die Celery-Fassung herein, standalone.py die lokale. OHNE
Rueckruf komprimiert derselbe Prozess weiter — genau das, was ein
Ein-Prozess-Rippy braucht. Der Ablauf selbst ist Zeile fuer Zeile derselbe;
der Docker-Betrieb merkt vom Umbau nichts (Task-Namen, Argumente, Queues
unveraendert).
EINE ZUSTELL-STELLE statt drei: celery_client.abschicken() bedient alle
Auftragsarten. Vorher rief jede Stelle send_task selbst auf — der
Standalone-Betrieb haette an drei Stellen umgebogen werden muessen, beim
naechsten Auftragstyp an einer vierten.
WEITERER BLOCKER GEFUNDEN: ablauf.py holte detect_disc_type fest aus dem
LINUX-Treiber, in einem try/except. Unter Windows waere es damit IMMER None
gewesen und Rippen "hart verriegelt" — Rippy haette alles angezeigt und
nichts gerippt, ohne dass irgendwo ein Fehler stuende. Jetzt fragt es den
Treiber-Port.
WERKZEUGE: ripping.py und caps.py suchten nur im PATH. Auf dem Commander-PC
gemessen, vorher/nachher:
vorher check_makemkv_installed() -> False (obwohl installiert)
erkenne_encoder() -> nur CPU
nachher MakeMKV 1.18.4 C:\Program Files (x86)\MakeMKV\makemkvcon64.exe
HandBrake 1.11.2 ueber die API geholt, in 2,7 s
Encoder cpu-x264, cpu-x265, cpu-av1, VCE, VCE-AV1
107 Presets, Ryzen 7 9700X, 16 Kerne, avx512f
VCE ist die Hardwarebeschleunigung der Radeon — die hat Rippy auf diesem
Rechner vorher nie gesehen, weil es HandBrake gar nicht fand.
NEUE ROUTEN: GET /system/werkzeuge (was liegt wo, in welcher Fassung, gibt
es Neueres) und POST /system/werkzeuge/{name}/holen. HandBrake kommt
vollautomatisch von GitHub. MakeMKV wird NICHT mitgeliefert — Rippy laedt
die offizielle Datei und startet sie (Black-Box-Trennung, KONZEPT.md § 6).
makemkv.com antwortete beim Bauen mit HTTP 525; das wird im Klartext
gemeldet, und eine selbst geholte Datei bleibt moeglich.
HERZSCHLAG: /capabilities las die workers-Tabelle, die bisher nur der
Celery-Herzschlag fuellte. Im Standalone-Betrieb stand dort "0 Worker" und
die Encoder-Auswahl im UI blieb LEER — auf einem Rechner, der alles kann.
Jetzt meldet sich der Prozess selbst, mit dem, was caps.py MISST.
GEMESSEN, aus der fertigen EXE (29,0 MB):
bereit nach 1 s, keine Fehler im Log
Werkzeuge: beide gefunden, mit Version und Pfad
Worker: 1 (TobisNicerPC), 5 Encoder, 107 Presets
Die ganze Kette ist als Test festgehalten (test_kette.py): zustellen ->
einreihen -> Laeufer -> ablauf -> Job endet in einem EHRLICHEN Zustand.
Ohne Laufwerk geprueft, und das ist der wichtigere Fall: Ein Rip auf ein
totes Geraet muss zuegig scheitern, nicht auf "pending" haengenbleiben.
GEMESSEN: ruff sauber, 512 Tests gruen + 15 uebersprungen (vorher 489).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
288f9eeb8d |
feat(tools): Werkzeuge finden, holen und aktuell halten — Grundlage fuer Windows
Ampel / ampel (push) Failing after 47s
WAS: rippy/tools/ mit katalog.py (finden + Version) und beschaffen.py
(herunterladen, installieren, Update-Stand). ripping.py und caps.py fragen
den Katalog statt shutil.which.
DER BEFUND, DER DAS NOETIG MACHT: Der Bestand suchte AUSSCHLIESSLICH mit
shutil.which(). Im Container stimmt das — dort liegen beide Werkzeuge in
/usr/local/bin. Unter Windows findet es NICHTS, auch wenn alles installiert
ist: Windows-Programme liegen in Program Files, nicht im PATH. Am
28.08.2026 auf dem Commander-PC nachgesehen — MakeMKV lag unter
"C:\Program Files (x86)\MakeMKV\makemkvcon64.exe", which sah es nicht.
Fuer einen Rippy, der unter Windows eigenstaendig arbeiten soll, ist das der
Unterschied zwischen "laeuft" und "laeuft nicht". Vorher/nachher gemessen:
vorher check_makemkv_installed() -> False (obwohl installiert)
nachher check_makemkv_installed() -> True
build_makemkv_cmd()[0] -> C:\Program Files (x86)\MakeMKV\makemkvcon64.exe
SUCHREIHENFOLGE, mit Begruendung im Modul: eingestellt > Rippys eigener
Werkzeug-Ordner > PATH > bekannte Installationsorte > Uninstall-Zweig der
Registry. Rippys eigener Ordner steht VOR dem System, damit eine selbst
gepflegte Fassung eine alte Systeminstallation schlaegt.
VERSION: HandBrakeCLI kann --version. makemkvcon KANN DAS NICHT (usage.txt
kennt keinen solchen Schalter) — die Version kommt aus dem Uninstall-Zweig.
Dort steht bei MakeMKV zwar eine Version, aber InstallLocation ist LEER
(nachgesehen); deshalb wird der Ordner ersatzweise aus DisplayIcon bzw.
UninstallString abgeleitet. Der Test dafuer hat sofort einen Fehler
gefunden: '"C:\...\uninstall.exe" /S' liess sich mit einem blossen
strip('"') nicht aufloesen.
BESCHAFFUNG, an echter Hardware gemessen:
HandBrake GitHub-Release-API -> 1.11.2, HandBrakeCLI-1.11.2-win-x86_64.zip
geladen, entpackt, gestartet: meldet sich als 1.11.2. 2,8 s.
MakeMKV makemkv.com antwortete mit HTTP 525 (Cloudflare) — auch mit
Browser-Kennung. Dieselbe Sperre, die im Projekt schon den
Docker-Bau lahmlegt. Deshalb: Abruf wird versucht, ein
Fehlschlag wird im Klartext gemeldet, und eine selbst geholte
Datei laesst sich weiterhin verwenden. Ein Update-Server, der
heute antwortet, darf keine Startbedingung fuer morgen sein.
DREI FALLEN, GEGEN DIE ES TESTS GIBT:
- Die .sig-Datei liegt im Release direkt neben dem ZIP und ist 566 Bytes
gross. Wer nur nach "win" filtert, laedt sie.
- "1.9.2" ist als Zeichenkette GROESSER als "1.11.2". Ein Update-Hinweis, der
ab Version zehn dauerhaft in die Irre fuehrt — und genau dort ist
HandBrake gerade.
- Eine Fehlerseite kommt oft mit HTTP 200. Sie darf keine funktionierende
Installation ersetzen: erst laden, pruefen, dann tauschen.
Und: Ist die neueste Version unbekannt (Quelle tot), steht NICHT "Update
verfuegbar" da. Eine Nichtauskunft ist keine Aussage.
GEMESSEN: ruff sauber, 489 Tests gruen + 15 uebersprungen (vorher 456).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
059183c651 |
feat(windows): V2-4 fertig — RippySetup.exe, Taskmanager-Eintrag, Deinstallation
Ampel / ampel (push) Failing after 46s
WAS: Eine 28,6-MB-Datei, drei Betriebsarten. Doppelklick installiert,
`--dienst` laeuft im Hintergrund, `--deinstallieren` raeumt sich weg.
Dazu packaging/windows/build.py, das sie baut.
DIE ZWEI COMMANDER-WUENSCHE, GEMESSEN:
1. EINTRAG IM TASKMANAGER
ProcessName Rippy
Beschreibung Rippy — automatisches Ripping
Produkt Rippy
Version 2.0.0
Der Prozessname kommt vom Dateinamen (bei der Installation kopiert sich
die Datei als Rippy.exe), die Beschreibung aus einer eingebetteten
Versions-Ressource. Ohne die steht dort nichts, und Windows SmartScreen
sieht eine EXE ohne Herausgeberangabe genauso misstrauisch wie ein Mensch.
2. EINTRAG IN PROGRAMME UND FEATURES
Aus der Registry zurueckgelesen, nicht behauptet:
DisplayName Rippy
DisplayVersion 2.0.0
Publisher Rippy
InstallLocation <Zielordner>
UninstallString "<pfad>\Rippy.exe" --deinstallieren
EstimatedSize 29241 (KiB)
NoModify/NoRepair 1
HKCU statt HKLM: Der Eintrag erscheint genauso in "Apps & Features", die
Installation laeuft aber ohne UAC-Abfrage durch.
VOLLER KREISLAUF GEMESSEN: installieren -> Rippy.exe + rippy.ico liegen da,
Registry-Eintrag da, Autostart da; Dienst starten -> nach 1 s bereit, alle
sieben Endpunkte HTTP 200, Oberflaeche kommt; deinstallieren -> nach ~4 s
sind Datei, Ordner, Registry-Eintrag, Autostart UND das Aufraeum-Skript weg.
VIER FEHLER, DIE NUR DIE FERTIGE EXE ZEIGT:
1. rippy/store baute die Engine BEIM IMPORT, mit PostgreSQL als Vorgabe.
Im Windows-Paket gibt es psycopg2 bewusst nicht -> die EXE starb sofort.
Die Engine entsteht jetzt beim ersten Zugriff. Und der Fehler war
grundsaetzlicher als der fehlende Treiber: Ein Modul, das beim Import
schon eine Verbindung aufbaut, laesst sich gar nicht mehr umstellen —
store.verbinden() kaeme immer zu spaet.
2. celery_client baute den Broker-Client beim Import. Dasselbe Muster, und
`from celery_client import celery_client` loeste es sogar dann aus, wenn
nie ein Rip angestossen wird. Jetzt traege, mit KeinBroker als klarer
Ansage statt eines Importfehlers.
3. Die Selbstloeschung beim Deinstallieren ging als EIN Argument an cmd.
Python maskiert dabei die inneren Anfuehrungszeichen — cmd suchte einen
Dateinamen mit Backslashes davor, fand nichts und meldete nichts (>nul).
Registry und Autostart waren weg, die 28-MB-Datei blieb liegen: ein still
scheiternder Hintergrundprozess, wie ihn AGENTS.md beschreibt. Jetzt ein
Aufraeum-Skript, das 15-mal versucht (die Onefile-EXE laeuft als ZWEI
Prozesse; der Starter haelt die Datei nach dem Ende noch offen).
4. deinstallieren() raeumte den STANDARD-Ordner auf statt des tatsaechlichen.
Wer nach --ziel D:\Rippy installiert hatte, dessen Dateien waeren
geblieben, waehrend ein fremder Ordner angefasst worden waere. Der Pfad
steht in der Registry (InstallLocation) und wird jetzt dort gelesen.
WAS NOCH NICHT GEHT, ausdruecklich: Ein RIP laesst sich unter Windows noch
nicht anstossen — die Zustellung laeuft weiter ueber Celery. Die Umstellung
auf die LocalQueue ist V2-5. Oberflaeche, Laufwerks-Erkennung, Auswurf und
alles Lesende laufen.
GEMESSEN: ruff sauber, 456 Tests gruen + 15 uebersprungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
e5254c23f0 |
feat(daemon): V2-4 (Teil 3) — rippyd laeuft unter Windows, ohne Docker
Ampel / ampel (push) Failing after 46s
WAS: `rippyd` startet Rippy als EINEN Prozess — SQLite statt Postgres, LocalQueue statt Celery/Redis, In-Process-Bus statt Redis Pub/Sub, UI von FastAPI statt von nginx. Dieselbe API, dasselbe UI, derselbe Rip-Code. GEMESSEN (Windows 11, frische Python-3.12-Umgebung, ohne Docker/Postgres/Redis): bereit nach 1 s /api/health 200 /api/settings 200 /api/devices 200 /api/logs 200 /api/jobs 200 /api/capabilities 200 /api/health/vorraete 200 / (Weboberflaeche) 200 Ereignis-Waechter gesund: true, alter_sekunden: 0.5 SSE-Strom liefert `event: snapshot` mit vollem Zustand SQLite angelegt, WAL aktiv, alle Tabellen da ZWEI FEHLER, DIE NUR DER ECHTE LAUF ZEIGT: 1. STARLETTE REICHT LIFESPAN NICHT AN MOUNT-UNTERANWENDUNGEN WEITER. Das UI ruft /api/..., im Docker-Betrieb entfernt der nginx das Praefix. Ohne nginx haengt die API als Unteranwendung unter /api — und ihr startup_event lief nie. Folge: db.init_db() nicht ausgefuehrt, "no such table: settings", HTTP 500 auf /jobs, /settings, /logs, /capabilities, und der SSE-Strom blieb stumm. /api/health antwortete trotzdem mit 200, weil die Route keine Datenbank braucht — der Fehlschlag sah also aus wie "laeuft". Behoben ueber einen eigenen lifespan, der den der Unteranwendung mit ausloest. 2. EIN print() HAT DEN GANZEN START UMGEBRACHT. UnicodeEncodeError: 'charmap' codec can't encode characters -> ERROR: Application startup failed. Exiting. Windows-Konsolen arbeiten mit cp1252; Rippys Meldungen sind deutsch und voller Umlaute und Warnzeichen. Das Bittere: Es war nicht die WARNUNG, die den Start verhindert hat, sondern der VERSUCH, sie auszugeben. Behoben mit encoding="utf-8" UND errors="replace" — eine Ausgabe darf unter keinen Umstaenden etwas abbrechen koennen. BEFUND ZUR HARDWARE (kein Codefehler): Nach dem Auswurf-Test hat sich das USB-Laufwerk vom Bus getrennt. Windows kennt es nur noch als Karteileiche (Get-PnpDevice -Class CDROM -> Status "Unknown", Win32_CDROMDrive leer), weder ein Laufwerksbuchstabe noch \.\CdRom0..4 sind da. Die leere Geraeteliste ist damit die RICHTIGE Antwort. Bei bus-versorgten USB-Laufwerken ist das bekannt — der Auswurf zieht kurz mehr Strom. GEMESSEN: ruff sauber, 440 Tests gruen + 15 uebersprungen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9d95c95d79 |
feat(drives): Windows-Treiber an echter Disc BEWIESEN
Ampel / ampel (push) Failing after 46s
WAS: Die Hardware-Schicht des Windows-Treibers ist nicht mehr nur gebaut,
sondern gemessen. Messwerte stehen im Modul-Docstring, der gemessene
BD-50-Wert als Testfall in test_cdrom.py.
GEMESSEN (28.08.2026, LG BU40N extern, Laufwerk G:, echte BD-50):
drive_status() -> 4 (CDS_DISC_OK), sofort
IOCTL_CDROM_DISK_TYPE -> DATA_TRACK
IOCTL_DISK_GET_LENGTH_INFO -> 48 149 364 736 Bytes = 44,84 GiB
disc_status() -> 101 (CDS_DATA_1)
detect_disc_type() -> 'bluray'
device_info()['status'] -> 'ready'
device_info()['type'] -> 'bluray'
auswerfen_versuchen() -> True, 2,41 s
drive_status() davor / danach -> 4 / 1
DIE LETZTE ZEILE IST DER EIGENTLICHE BEWEIS. Der Auswurf haengt nicht am
Rueckgabewert des Steuercodes, sondern am ZUSTAND davor und danach: Die
Disc war drin (4) und ist danach draussen (1). Genau das fehlte in v1
monatelang — dort quittierte das ioctl Erfolg, die Schublade blieb zu, und
im Log stand "Disc ausgeworfen".
Die 44,84 GiB pruefen die Schwelle gleich mit: Eine BD-50 liegt knapp unter
den 55 GiB, ab denen classify() auf 'uhd' geht, und wird korrekt als
'bluray' eingeordnet. Wer die Schwelle senkt, macht aus jeder BD-50 eine
UHD — und Rippy waehlte dann das falsche Kompressions-Preset. Der Testfall
haelt den gemessenen Wert fest.
Offen bleibt allein die Ereignis-Erkennung (WM_DEVICECHANGE) — die wird
erst gebaut.
Nebenbei: Beim Eintragen der Messwerte sind drei echte NUL-Bytes in die
Datei geraten (eine Escape-Ebene im Schreib-Skript verschluckt). Python
lehnt so eine Datei rundheraus ab ("source code string cannot contain null
bytes") — im Binaermodus repariert und gegengeprueft.
GEMESSEN: ruff sauber, 430 Tests gruen + 14 uebersprungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3d7f3b1680 |
feat(drives): V2-4 (Teil 2) — main.py laedt unter Windows, Treiber gemessen
Ampel / ampel (push) Failing after 46s
WAS: rippy/drives/__init__.py waehlt den Treiber fuer DIESE Maschine.
main.py und prescan.py fragen ihn, statt fest den Linux-Treiber zu
importieren. Damit ist der Blocker fuer den nativen Windows-Betrieb weg.
GEMESSEN, vorher (frische Python-3.12-Umgebung unter Windows):
>>> import main
ModuleNotFoundError: No module named 'fcntl'
GEMESSEN, nachher:
main.py laedt unter Windows: JA
Treiber: rippy.drives.windows
Routen: 61
Laufwerke: ['\\.\G:']
Der Import stand in rippy/drives/linux.py -> detection.py -> fcntl. Genau
dafuer gibt es den Port: Der Aufrufer sagt WAS, nicht WIE. test_treiberwahl.py
haelt ausserdem fest, dass BEIDE Treiber denselben Satz Funktionen anbieten —
sonst faellt eine fehlende erst im Betrieb als AttributeError auf, und zwar
auf der anderen Plattform.
DER TREIBER AN ECHTER HARDWARE (LG BU40N extern, Laufwerk G:, LEER):
GetDriveTypeW("G:\\") -> 5 (DRIVE_CDROM)
list_optical_devices() -> ['\\.\G:']
CreateFileW(r"\.\G:") -> Handle 380
CHECK_VERIFY2 bei leerem Laufwerk -> ERROR_NOT_READY (21)
drive_status() -> 1 (CDS_NO_DISC)
detect_disc_type() -> 'no_disc'
device_info()['status'] -> 'empty'
MEDIA_REMOVAL (entriegeln) -> Erfolg, 0,1 ms
EJECT_MEDIA -> Erfolg, 664,8 ms
Die 664,8 ms sind der Beleg, dass wirklich etwas passiert ist: Ein
abgelehnter Steuercode kommt in rund 2 ms zurueck.
BEFUND, DER EIN PLACEBO VERHINDERT HAT: IOCTL_STORAGE_LOAD_MEDIA (Schublade
einziehen) antwortet auf diesem Laufwerk mit ERROR_INVALID_FUNCTION. Bei
flachen und externen Laufwerken ist das die Regel. Deshalb gibt es bewusst
KEINE einziehen()-Funktion — ein Knopf "Schublade schliessen", der auf der
Haelfte aller Laufwerke stillschweigend nichts tut, waere genau die Sorte
Placebo, die in Etappe 19 aufgeraeumt wurde.
Dazu eine DeprecationWarning behoben: '\G' im Docstring ist keine gueltige
Escape-Sequenz und waere in einer kuenftigen Python-Version ein harter Fehler.
NOCH NICHT gemessen: das Verhalten MIT eingelegter Disc (Typ-Erkennung ueber
die Groesse, und ein Auswurf, bei dem wirklich etwas drin ist).
GEMESSEN: ruff sauber, 429 Tests gruen + 13 uebersprungen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b723fee3a2 |
feat(drives): V2-4 (Teil 1) — Windows-Laufwerkstreiber, ohne Hardware geprueft
Ampel / ampel (push) Successful in 45s
WAS: rippy/drives/windows.py mit denselben zwei Vertraegen wie die Linux-Fassung (auswerfen_versuchen gibt False zurueck, eject wirft). Dazu win_ioctl.py: die Win32-Steuercodes, HERGELEITET statt abgeschrieben. WARUM ctypes und nicht pywin32: 20 MB zusaetzliche Abhaengigkeit, die in jedes PyInstaller-Paket muss und bei jedem Python-Wechsel neu passen will. Gebraucht werden fuenf Funktionen aus kernel32 — die spricht ctypes direkt an. DIE STEUERCODES WERDEN AUSGERECHNET, NICHT ABGETIPPT: win_ioctl.py enthaelt das CTL_CODE-Makro aus winioctl.h als Funktion; jede Konstante wird daraus gebildet. test_win_ioctl.py rechnet das Ergebnis gegen die in der Microsoft-Dokumentation stehenden Zahlen. Grund: Ein falscher IOCTL-Code meldet kein "unbekannter Befehl", sondern ERROR_INVALID_FUNCTION — und das liest sich wie "dieses Laufwerk kann das nicht". Man sucht dann am Geraet statt an einer Zahl. AGENTS Regel D, ernst genommen. DIE ZWEI PORT-REGELN GELTEN AUCH HIER: - Auswerfen heisst entriegeln, auswerfen, NACHSEHEN. Unter Linux wurde am 26.07.2026 gemessen, dass ein verriegeltes Laufwerk den Auswurf mit ERFOLG quittiert und nichts tut. IOCTL_STORAGE_EJECT_MEDIA meldet ebenfalls nur die Annahme des Befehls — also wird auch hier nachgesehen (CHECK_VERIFY2 muss ERROR_NOT_READY liefern). - Ein unzugaengliches Laufwerk ist UNBEKANNT, nicht leer. device_info kennt deshalb drei Zustaende, nicht zwei. EIN FEHLER, DEN DIE TESTS SOFORT GEFANGEN HABEN: Win32Fehler setzte self.winerror VOR super().__init__(). Nachgemessen: Ein zweiargumentiges OSError.__init__(code, text) setzt winerror wieder auf None. Damit erkannte _medium_da ERROR_NOT_READY nicht mehr, hielt jedes leere Laufwerk fuer einen echten Fehler und meldete "unbekannt" statt "leer". Vier Tests rot, Ursache in einer Minute gemessen statt geraten. ⚠️ WAS HIER NICHT BEWIESEN IST: dass echte Hardware sich so verhaelt. Das Laufwerk ist derzeit nirgends angeschlossen. Der Ablauf, die Fehlerunterscheidung und die Typ-Zuordnung sind gegen ein nachgebautes Laufwerk geprueft (51 Tests, laufen auf jeder Plattform) — die Hardware- Schicht gilt bis zu einer Messung als GEBAUT, nicht als BEWIESEN. GEMESSEN: ruff sauber, 400 Tests gruen + 4 uebersprungen (vorher 378). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4337253797 |
docs: SAVEPOINT v4.0-beta — V2-0 bis V2-3 stehen, Laufwerk fehlt an der VM
Ampel / ampel (push) Successful in 41s
Vier Etappen gebaut und deployt, Polling ist weg (121 Anfragen/min je Tab -> ~2 einmalige Abrufe), SSE-Strom live belegt. Wichtig fuer die naechste Sitzung: Das optische Laufwerk haengt NICHT mehr an der VM. Rippy laeuft als reine Komprimier-Maschine. Und der Vorfall, der das aufgedeckt hat, ist dokumentiert — ein devices:-Eintrag in compose ist eine Startbedingung, und die hat api, worker und ui stillgelegt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
05ab655116 |
fix: ein fehlendes Laufwerk darf Rippy nicht am Starten hindern
Ampel / ampel (push) Successful in 40s
WAS: Die `devices:`-Eintraege fliegen aus docker-compose.yml. Was dieser Host wirklich hat, ermittelt deploy/geraete-override.sh und schreibt es in docker-compose.override.yml — die zieht Compose von allein dazu, der Befehl bleibt `docker compose up -d`. WARUM (Vorfall heute, 28.08.2026): Das USB-Laufwerk haengt nicht mehr an der VM. Ein ganz normaler Deploy legte daraufhin api, worker UND ui still: Error response from daemon: error gathering device information while adding custom device "/dev/sr0": no such file or directory Ein `devices:`-Eintrag ist eine STARTBEDINGUNG — und keiner der drei Container braucht zum STARTEN ein Laufwerk. Rippy war unten, und die Meldung stand mitten im Build-Rauschen (dieselbe Klasse wie das geschluckte `|| echo` beim .env-Kopieren am 26.07.). DER WIDERSPRUCH WAR AELTER ALS DER VORFALL: install.sh sagt bei fehlendem Laufwerk ausdruecklich "die Installation laeuft trotzdem durch; diese Maschine dient dann als reine KOMPRIMIER-Maschine" — die Compose-Datei sah das anders. Eine Maschine ohne Laufwerk ist ein VORGESEHENER Betriebsfall: der Windows-Worker und der GPU-Encoder sind genau das. Das Skript uebernimmt die Erkennung aus install.sh unveraendert: sr- und sg-Knoten ueber die SCSI-Adresse in /sys abgeglichen, nicht geraten. Und es schreibt die Override AUCH dann, wenn kein Laufwerk da ist — sonst bliebe eine alte Datei mit /dev/sr0 liegen und der Fehler waere derselbe, nur schwerer zu finden, weil sie gitignored ist und in keinem Diff auftaucht. deploy.sh ruft es vor dem Start auf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
95705c8d88 |
feat(ui): V2-3 (Teil 2) — das UI haengt am Ereignis-Strom, Taktgeber raus
Ampel / ampel (push) Successful in 47s
WAS: EventStreamProvider haelt EINE SSE-Verbindung fuer die ganze
Anwendung. Dashboard, Log-Kasten, Log-Seite, Laufwerksliste und
Worker-Liste beziehen ihren Zustand daraus. Sieben von neun setInterval
sind weg.
GEMESSEN AN DEN TAKTGEBERN, je offenem Tab:
Dashboard 5 Endpunkte / 4 s 75/min -> 0
Log-Kasten 2 Endpunkte / 5 s 24/min -> 0
Laufwerke 1 Endpunkt / 5 s 12/min -> 0
Log-Seite 1 Endpunkt /10 s 6/min -> 0 (+1 Abruf beim Oeffnen)
Worker-Liste 1 Endpunkt /15 s 4/min -> 0 (+1 Abruf beim Oeffnen)
------
121/min -> ~2 einmalige Abrufe
Zwei Taktgeber bleiben bewusst: FirstRunWizard (laeuft nur VOR der
Einrichtung) und RipTargetModal (nur solange der Dialog offen ist).
DIE REGEL IST UMGEZOGEN, NICHT VERSCHWUNDEN: Ein Abriss ist keine
Aussage ueber die Welt. Der Provider BEHAELT bei einem Fehler den letzten
Stand und setzt nur `verbunden` auf false; es wird nie eine Liste geleert.
Jede Komponente uebernimmt einen Wert nur, wenn er wirklich da ist —
`devices === null` heisst "konnte nicht nachsehen", nicht "keine
Laufwerke". Das war der Fehler hinter "wird oft neu geladen".
EIN PLACEBO WENIGER: Oben rechts stand ein fest verdrahtetes "ONLINE" mit
pulsierendem Punkt — es leuchtete gruen, auch wenn die API tot war. Jetzt
zeigt es LIVE oder VERBINDUNG WEG, und im Tooltip steht, wann die letzte
Meldung kam.
DER SERVER SCHIEBT JETZT AUCH DEN SERVER-ZUSTAND: Neuer Ereignistyp
system.status (Hardware, Worker, Ablageziele) im 15-Sekunden-Takt des
Waechters — EINMAL im Server statt 15/min je Tab. Nur mitgeschickte
Schluessel werden uebernommen; ein fehlender heisst "behalte deinen Stand".
DAZU EIN FORMATFEHLER GEFUNDEN UND BEHOBEN: /logs bildet ts -> timestamp
ab, mein Snapshot lieferte die rohe DB-Zeile. Das UI haette "Invalid Date"
gezeigt — und zwar NUR im Live-Betrieb, nicht beim manuellen Neuladen.
Jetzt gibt es _log_zeile() einmal, benutzt von beiden.
UND EINEN ZWEITEN: system_lesen lief per asyncio.get_event_loop() in einem
Worker-Thread — dort ist das NICHT die laufende Schleife. Die Coroutine
waere nie gelaufen. Die Schleife wird jetzt im Startup festgehalten.
GEMESSEN: ruff sauber, 378 Tests + 3 uebersprungen, `npm run build`
durch (1650 Module). Der Live-Beweis steht noch aus — er kommt mit dem
Deploy.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f097a0b59d |
feat(bus): Waechter — Worker-Aenderungen werden zu Ereignissen
Ampel / ampel (push) Successful in 43s
WAS: rippy/bus/waechter.py sieht im Takt von 1 s in der Datenbank nach und meldet Aenderungen an den Bus: job.created / job.phase / job.progress / job.finished und log.line. 12 Tests, ohne Datenbank lauffaehig. WARUM ES IHN BRAUCHT: Der Bus verteilt innerhalb EINES Prozesses. Der Worker ist aber ein eigener Prozess, im Docker-Betrieb ein eigener Container, im verteilten Betrieb ein anderer Rechner. Ohne Bruecke wuesste die API nichts vom Fortschritt eines Rips. WARUM UEBER DIE DATENBANK und nicht per HTTP-Rueckruf oder Redis: - HTTP-Rueckruf: jeder Fortschrittswert haengt an einer Verbindung, ein kurzer API-Neustart liesse Ereignisse verschwinden, und der Worker braeuchte zusaetzliche Konfiguration. - Redis Pub/Sub: sauber, kommt im verteilten Betrieb (V2-5) — aber der Standalone-Betrieb hat bewusst KEIN Redis, das war der Punkt von V2-2. - Die Datenbank ist ohnehin die Wahrheit ueber den Job-Zustand, ist in JEDEM Modus da, und der Worker schreibt dort sowieso hin. "Ist das nicht wieder Polling?" Doch — aber einmal, lokal und billig: vorher N Tabs x 9 Endpunkte alle 4-5 s ueber HTTP durch nginx durch das Rate-Limit; jetzt EINE Abfrage pro Sekunde von vier Spalten. Das Ziel war nie "nirgendwo nachfragen", sondern: der Browser fragt nicht mehr. DER TEST HAT EINEN ECHTEN FEHLER GEFUNDEN: Der Docstring versprach "erst neue Jobs, dann Aenderungen", die Schleife lieferte aber die Reihenfolge des dicts — ein Client haette ein job.progress fuer einen Job bekommen koennen, den er noch gar nicht kennt. Jetzt drei getrennte Durchlaeufe. Das Versprechen war richtig, der Code nicht. Und er stirbt nicht still: Fehler gehen ins Log UND als system.notice an den Bus (erster Fehlschlag, danach jeder zehnte), und lebt_seit_sekunden() macht sein Alter abfragbar — AGENTS.md, "wer einen Vorrat anlegt, macht sein Alter abfragbar". GEMESSEN: ruff sauber, 371 Tests gruen + 3 uebersprungen (vorher 359). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
1fbe2cfc55 |
fix(test): Snapshot-Tests brauchten eine Datenbank — die Ampel hat keine
Ampel / ampel (push) Successful in 40s
WAS: test_snapshot_* ersetzen den Store per monkeypatch, statt
db.list_jobs() wirklich aufzurufen.
WARUM ROT (Lauf 170): Die Ampel startet KEINE Postgres. Mein neuer Test
war der erste im ganzen Repo, der eine Datenbank angefasst hat — alle
anderen in test_api_smoke.py pruefen nur Importe und Routen. Auf der VM
und lokal waere es nie aufgefallen: dort ist eine DB da bzw. der ganze
Test wird uebersprungen (kein fcntl unter Windows).
Geprueft wird die Snapshot-LOGIK ("konnte nicht nachsehen" ergibt None,
nicht []), nicht die Datenbank. Also gehoert die Datenbank da nicht rein.
Dazu ein zweiter Test: der Snapshot muss alle vier Bereiche liefern
(jobs/workers/logs/devices) — ein Client, dem einer fehlt, muesste den
Rest raten und fiele auf Polling zurueck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
55db9eb13f |
feat(api): V2-3 (Teil 1) — Ereignis-Bus und SSE-Endpunkt /api/v2/events
Ampel / ampel (push) Failing after 41s
WAS: rippy/bus mit Ereignis-Schema, In-Process-Treiber und Ringpuffer.
Dazu der SSE-Strom in der API: erst ein Snapshot, danach nur Deltas,
mit lueckenloser Wiederaufnahme ueber Last-Event-ID.
WARUM: Ein offener Tab plus ein Worker verursachen heute rund 133
Anfragen pro Minute (Dashboard 75 + Log-Kasten 24 + Laufwerke 12 +
Worker-Liste 4 + Log-Seite 6 + Tray 12). Das Rate-Limit stand einmal
UNTER dieser Zahl — daher die sich leerende Job-Liste im Sekundentakt.
Mit einer offenen Verbindung sind es null.
DREI EIGENSCHAFTEN, ALLE AUS v1-FEHLERN:
1. Der Bus traegt NUR Nachrichten ueber Aenderungen, nie den Zustand.
Wer den Zustand will, fragt den Store. Damit kann ein verpasstes
Ereignis auch keinen Zustand loeschen — anders als beim fuenffachen
`catch(() => [])` im alten UI, wo jeder fehlgeschlagene Abruf
"es gibt keine Jobs" bedeutete.
2. Eine zu grosse Luecke wird ANGESAGT, nicht verschluckt. nachliefern()
gibt None ("hol dir ein ganzes Bild") statt [] ("nichts verpasst") —
dieselbe Unterscheidung wie timeout-Rueckgabe 124 bei den Netzpfaden.
Stillschweigend weiterzumachen waere schlimmer: Das UI hielte sich
fuer aktuell und waere es nicht.
3. Der Snapshot meldet unlesbare Laufwerke als None, nicht als leere
Liste. "Konnte nicht nachsehen" ist etwas anderes als "gibt es nicht".
Ein unbekannter Ereignistyp fliegt beim Senden HOCH statt durchzugehen.
Ein Tippfehler waere sonst der stillste aller Fehlschlaege: Nachricht
raus, kein Empfaenger, nirgends ein Hinweis.
Ein langsamer Zuhoerer (Tab im Hintergrund, lahmes Handy) bremst den
Sender nicht — er wird markiert und bekommt beim naechsten Mal einen
Snapshot. Ein Rip darf nicht auf einen Browser warten.
GEMESSEN: ruff sauber, 359 Tests gruen + 3 uebersprungen (vorher 346).
13 neue Bus-Tests laufen auf jeder Plattform; die vier SSE-Tests haengen
an main.py und laufen damit in der Ampel.
NOCH OFFEN in V2-3: das UI auf den Strom umstellen (neun setInterval)
und die Ereignisse an den Zustandsaenderungen ausloesen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
d2645f87e8 |
feat(core): V2-2 — SQLite hinter demselben Port, Auftrags-Queue mit Lease
Ampel / ampel (push) Successful in 39s
WAS: Der Store spricht jetzt beide Datenbanken (PostgreSQL wie bisher, SQLite fuer den Standalone-Betrieb). Dazu die LocalQueue: Auftraege als Tabelle, Vergabe per bedingtem UPDATE, Lease statt Zombie-Jagd. Und die Konfigurationsschicht mit EINER Praezedenz fuer alle drei Betriebsarten. WARUM: Ohne SQLite und ohne broker-lose Queue gibt es keinen Standalone- Betrieb — und ohne den keine Windows-App und keine Headless-Variante. Beides haengt an dieser Etappe. DER GRUNDSATZ (KONZEPT-V2.md §3.1): Die Datenbank ist die Wahrheit ueber den Job-Zustand, der Broker ist nur der Wecker. Daraus folgt die ganze Queue: Ein Auftrag ist eine Zeile mit claimed_by und lease_until, ein Knoten uebernimmt ihn per bedingtem UPDATE (es gewinnt genau EINER, auch wenn zehn gleichzeitig fragen), und er haelt ihn per Lease am Leben. Laeuft die Lease ab, ist der Auftrag frei — egal ob Absturz, Netzausfall oder gezogener Stecker. Damit gibt es den Zustand "laeuft, aber niemand arbeitet daran" nicht mehr, den v1 mit zombies.py (206 Zeilen + 263 Zeilen Tests) nachtraeglich einsammeln musste. Er kann hoechstens 60 Sekunden bestehen und heilt sich dann selbst. Beides ist geprueft: zwei Knoten bekommen NICHT denselben Auftrag, und eine abgelaufene Lease gibt ihn wirklich wieder her. KEINE QUEUE-BIBLIOTHEK (Taskiq/ARQ/Celery-lite), Begruendung im Modul-Docstring: Der teure Teil ist kein Task, sondern ein 30-90-Minuten- Subprozess mit Fortschritts-Parsing. Was die Bibliotheken loesen, ist nicht das Problem; was das Problem ist, muss man ohnehin selbst bauen. ABWEICHUNG VOM ENTWURF — KEIN ALEMBIC (in KONZEPT-V2.md §3.2 vermerkt): Die Datenbank auf der VM gibt es schon, Alembic muesste sie erst stempeln — ein Handgriff auf einer laufenden Installation, den beim naechsten Deploy jemand vergisst. Und es gibt hier nichts zu versionieren: drei nachgetragene Spalten. store.migrieren() fragt stattdessen per inspect() nach und legt nur Fehlendes an; das laeuft auf beiden Dialekten. Der alte Weg (ADD COLUMN IF NOT EXISTS) war korrekte Postgres-Syntax und waere auf SQLite gebrochen. EIN KONSTRUKTIONSFEHLER, SELBST GEFUNDEN: lokal.py hatte anfangs `from rippy.store import engine` — ein Import bindet den Wert EINMAL, ein spaeteres store.verbinden() waere nie angekommen, und das Modul haette weiter mit der alten Datenbank gesprochen. Jetzt wird store.engine zur Aufrufzeit gelesen; die Warnung steht an beiden Stellen im Code. GEMESSEN: ruff sauber, 346 Tests gruen + 3 uebersprungen (vorher 301). Neu: 30 Tests gegen ECHTES SQLite (kein Fake) — inklusive der Gegenprobe, dass WAL und busy_timeout wirklich gesetzt sind, und dass migrieren() eine weggenommene Spalte zurueckholt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
7558be4854 |
fix: CRLF in gui.py wiederherstellen (Zeilenenden-Falle in V2-1)
Ampel / ampel (push) Successful in 39s
WAS: docker/worker/gui.py war CRLF und ist beim Umstellen des db-Imports komplett auf LF gekippt — 1520 Zeilen Diff-Rauschen fuer EINE geaenderte Zeile. Zeilenenden zurueckgedreht, der Inhalt bleibt. WARUM ES PASSIERT IST: Mein Umschreib-Helfer las mit io.open(p, encoding="utf-8") — im Textmodus wandelt Python CRLF beim LESEN zu LF — und schrieb mit newline="" zurueck, also LF. Betroffen war nur gui.py; die uebrigen angefassten Dateien sind ohnehin LF. Richtig waere newline="" AUCH beim Lesen gewesen (dann bleiben die Zeilenenden im String stehen) oder gleich der Binaermodus. Das steht so schon in den Projekt-Notizen und ist mir trotzdem passiert, weil der Helfer generisch war und die Datei nicht danach aussah. Gegenprobe: `file docker/worker/gui.py` sagt wieder "with CRLF line terminators", und `git diff HEAD~2 -- gui.py` zeigt genau eine Zeile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
dd1d0b7365 |
refactor(core): V2-1 — die vier Ports, ein Store, ein Laufwerks-Treiber
Ampel / ampel (push) Successful in 40s
WAS: rippy/ports.py beschreibt Store/Queue/Bus/Drives als Protocol. Zwei
weitere Doppelungen sind zusammengelegt: db.py (API+Worker) wird
rippy/store, und der Auswurf (api/devices.py + ripping.wirf_disc_aus)
wird rippy/drives/linux. Verhalten unveraendert.
WARUM: Drei Betriebsarten tragen nur, wenn ein Modus die Auswahl der
Treiber hinter vier Nahtstellen ist statt ein eigener Codestand
(KONZEPT-V2.md §1). Diese Etappe zieht die Nahtstellen ein, ohne schon
einen zweiten Treiber zu haben — die kommen in V2-2 (SQLite/LocalQueue)
und V2-4 (Windows).
DIE UNANGENEHMERE DOPPELUNG WAR DER AUSWURF: Er stand zweimal da, mit
UNTERSCHIEDLICHEN Vertraegen — devices.eject wirft OSError, ripping.
wirf_disc_aus gibt False zurueck und wirft nie. Beides ist richtig fuer
seine Seite (Browser-Meldung gegen "ein Rip stirbt nicht an einer
klemmenden Schublade"). Jetzt liegt EINE Mechanik darunter
(auswerfen_mit_grund) und beide Vertraege unveraendert darueber.
Die API-Fassung war ausserdem NIE getestet — jetzt schon, inklusive
"reicht ENOENT/EPERM unveraendert weiter".
get_settings hatte den einzigen echten Verhaltensunterschied der beiden
db.py: die Worker-Fassung schluckte jeden Fehler und gab {} zurueck.
Nicht still entschieden, sondern sichtbar gemacht — der Parameter
bei_fehler_leer steht jetzt in der Signatur, mit der offenen Frage im
Docstring. {} heisst fuer den Aufrufer "nichts gesetzt", nicht "konnte
nicht nachsehen"; das ist dieselbe Klasse wie catch(() => []) im alten
UI. Zu entscheiden in V2-2.
ZWEITER BEINAHE-FEHLER DIESER ETAPPE: linux.py importierte detection
auf Modulebene — und das zieht fcntl. Damit waere ripping.py und ueber
es der NATIVE WINDOWS-WORKER nicht mehr ladbar gewesen. Diesmal haben
die Tests es sofort gefangen (4 Sammelfehler). Behoben an der Wurzel:
Konstanten und die reine classify() leben jetzt in drives/cdrom.py,
ganz ohne fcntl. Nebengewinn — die classify-Tests liefen bisher NUR in
der Ampel ("erst nach dem Push bewiesen") und laufen jetzt ueberall.
Ausserdem: .dockerignore-Testmuster brauchen **, sonst greifen sie nur
in der obersten Ebene. Im laufenden Container nachgezaehlt: 30 test_*.py
lagen in den Images.
GEMESSEN: ruff sauber, 301 Tests gruen + 1 uebersprungen (vorher 290;
+4 neue eject-Tests, +7 classify-Tests die jetzt lokal laufen). Kein
Modul liegt mehr doppelt im Repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
edfe0d4313 |
docs: SAVEPOINT fuer v4.0-alpha — v2-Konzept entschieden, Etappe V2-0 gruen
Ampel / ampel (push) Successful in 39s
WAS: Neuer Kopfeintrag mit dem gemessenen Zustand (
|
||
|
|
b98dc5eef5 |
refactor(core): V2-0 — gemeinsames Paket rippy statt drei Zwillingsdateien
Ampel / ampel (push) Successful in 41s
WAS: Neues Paket src/rippy (core/drives/rip). detection.py,
makemkv_daten.py und notify.py lagen je ZWEIMAL im Repo — unter
docker/api UND docker/worker, byte-identisch. Jetzt gibt es sie einmal;
beide Container importieren dieselbe Datei. Verhalten unveraendert.
WARUM: Es gab kein geteiltes Paket zwischen den Containern, deshalb die
Kopien. docker/api/db.py sagt es im Kopfkommentar selbst: "Wer die
Struktur aendert, aendert BEIDE Dateien." Ein Waechter-Test
(test_zwillinge_sind_byteweise_identisch) hat das mechanisch
abgesichert — er war noetig, weil die Konstruktion falsch war. Erste
Etappe des v2-Plans (KONZEPT-V2.md §8.3).
IM EINZELNEN:
- src/rippy/{core/notify, drives/detection, rip/makemkv_daten}.py
- conftest.py in der Wurzel legt src/ auf den sys.path (die Ampel ruft
pytest dort auf).
- Beide Dockerfiles kopieren src/rippy nach /app/rippy — /app ist
Arbeitsverzeichnis und uvicorn-App-Dir, also ohne PYTHONPATH findbar.
- worker_setup_paket packt das Paket ausdruecklich mit ins Zip: die
Schleife sah nur die oberste Ebene, ein Unterordner waere nie
mitgekommen und der Windows-Worker beim Start gestorben.
- Die zwei Test-Dateien fuer makemkv_daten sind zu einer verschmolzen
(die Faelle aus beiden). Damit entfaellt auch der importlib-Umweg im
Worker-Test: der war noetig, weil bei "pytest -q" aus der Wurzel
docker/api zuerst eingesammelt wird und jeder weitere Import nur noch
den sys.modules-Cache trifft — die Worker-Kopie wurde also nie
angefasst. Netto -13 doppelte Tests, +2 neue (siehe unten).
EIN FEHLER, DEN DER UMBAU FAST AUSGELIEFERT HAETTE: In tasks.py steht
der detection-Import in einem "try/except ImportError" — absichtlich,
denn der native Windows-Worker hat kein fcntl und soll trotzdem
starten. Das except verschluckt aber JEDEN ImportError, auch einen
falschen Modulpfad. Auf Linux haette der Worker ab sofort still
detect_disc_type=None gesetzt und jeden Rip verweigert, ohne dass
irgendwo ein Fehler stuende — genau die Klasse "still scheiternder
Hintergrund-Prozess" aus AGENTS.md. Gefunden ueber eine zweite Suche
mit eingerueckten Treffern.
Waechter dagegen: src/rippy/test_paket.py prueft mit
importlib.util.find_spec (fuehrt nichts aus, laeuft also auch auf
Windows ohne fcntl), dass es die drei Modulpfade wirklich gibt. Beim
Wegnehmen von detection.py gegengeprueft: Test wird rot, nach
Wiederherstellen gruen.
GEMESSEN: ruff sauber, 290 Tests gruen + 1 uebersprungen (lokal, ohne
die zwei Linux-only-Module). Kein Modul liegt noch doppelt im Repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
20675a98fa |
docs: Konzept fuer Rippy v2 + drei Commander-Entscheide (28.08.2026)
WAS: KONZEPT-V2.md neu — Systemarchitektur, Daten-/Queue-Strategie, die drei Betriebsmodi, API-/Event-Design, Migrationsplan V2-0 bis V2-7. KONZEPT.md §10 und ROADMAP.md ziehen nach. WARUM: Rippy soll drei Betriebsarten bekommen statt einer — Docker, native Windows-App ohne Docker, Headless-Linux-Dienst; alle mit demselben Webinterface. Das traegt technisch nur, wenn es EINEN Kern gibt, dessen Betriebsmodus nur die Auswahl der Treiber hinter vier Ports ist (Store, Queue, Bus, Drives). Drei Entscheide des Commanders sind eingearbeitet: - Reihenfolge: Echtzeit (SSE statt Polling) VOR der Windows-App. Die Windows-App braucht SQLite und die lokale Queue ohnehin. - Disc-Schluessel: automatischer Abruf MIT Rueckfallebene, als Kette in drei Stufen (eigener Bestand -> Abruf -> Import von Hand). Das verschiebt die Grenze aus KONZEPT.md §10 vom 25.07.2026 bewusst — deshalb steht sie dort jetzt ausdruecklich fortgeschrieben statt still ersetzt (AGENTS Regel B). Bezugsadresse leer vorbelegt, Fehlschlag laut, Bestand wird nie still ueberschrieben. - Speicherziele: der Host mountet, Rippy prueft und erzeugt die kopierbare Zeile. Raeumt die URSACHE der CIFS-Ausfaelle ab (Befund 26.07.2026: die Verbindung lebt in der Netz-Namespace des api-Containers und stirbt mit ihm) statt weiter das Symptom zu heilen. SYS_ADMIN, DAC_READ_SEARCH, apparmor:unconfined und die Mount-Wache fallen damit weg. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a14449ef85 |
fix(worker-ui): fix syntax error in gui.py and ruff error in test_verwaltung.py
Ampel / ampel (push) Successful in 39s
|
||
|
|
4343b0901d |
feat(worker-ui): adjust thumbnails, log view, and button logic to match web dashboard
Ampel / ampel (push) Failing after 40s
|
||
|
|
c3e77399ad |
fix(worker): mock ntpath.dirname in test_erreichbarkeit so it passes on Linux CI runner
Ampel / ampel (push) Failing after 39s
|
||
|
|
75426224d1 |
fix(worker): ensure sys.path in test_verwaltung for pytest importlib mode
Ampel / ampel (push) Failing after 39s
|
||
|
|
e5ff5373bb |
fix(worker): restore pure logic module verwaltung.py and its tests (SoC)
Ampel / ampel (push) Failing after 39s
|
||
|
|
9b30c0a490 |
chore: remove obsolete test_verwaltung.py
Ampel / ampel (push) Failing after 40s
|
||
|
|
061f79ecda |
fix: ruff linter errors (multiple statements, bare except, unused vars)
Ampel / ampel (push) Failing after 38s
|
||
|
|
a32a1bb075 |
fix: bare except in main.py
Ampel / ampel (push) Failing after 38s
|
||
|
|
32979d7bde |
docs: SAVEPOINT update fuer Etappe 25 (Windows Worker in Flet)
Ampel / ampel (push) Failing after 40s
|
||
|
|
d15f4b2ab6 | feat: Thumbnails, bunte Logs und Button-Status im Worker UI | ||
|
|
a221a161f4 | feat: Tray-Icon fuer Worker GUI (pystray) — Fenster schliessen minimiert ins Tray | ||
|
|
264b2c77f0 | chore: finale exe mit venv --clear fix neu gebaut | ||
|
|
16ea54e35d | fix: venv --clear bei Reinstall, verhindert veraltete Paketversionen | ||
|
|
81ba6bd1c6 | fix: don't override pinned flet==0.23.2 with unpinned pip install | ||
|
|
06c1718a52 | fix: installer buttons (Durchsuchen/Freigabe holen), Worker-Start via os.startfile, Shortcut via PowerShell | ||
|
|
de500f9890 | feat: complete rewrite of worker installer in Flet (standalone exe) | ||
|
|
9a151f3d8b | feat: Worker V2 Flet GUI mit Feature-Parität und neuem Installer | ||
|
|
e165e2a0d3 |
feat: Komplettierung der aktuellen Rippy-Etappe
Ampel / ampel (push) Failing after 29s
- Versionsanzeige für den Windows-Worker im UI inkl. Prüfung - Absicherung der install.sh gegen fehlendes systemd - Serien-Episoden-Erkennung und Laufzeitabgleich anhand TMDB-Daten (Heuristik) - Umstellung der Windows-Worker-Installation auf SchTasks (Dienst-Ersatz) - Kodi-Bibliotheks-Refresh über JSON-RPC integriert |
||
|
|
d15900d1eb |
fix(audio): nur die beste Tonspur pro Sprache verlustfrei kopieren
Ampel / ampel (push) Failing after 29s
|
||
|
|
4cb1fab416 |
docs(savepoint): die Kette ist zum ersten Mal ganz durchgelaufen
Ampel / ampel (push) Failing after 30s
40,9 GB Rip -> Auswurf -> Kompression auf dem Windows-PC -> 12,29 GB abgelegt, Roh-Verzeichnis danach automatisch aufgeraeumt. Die Sprachwahl am ERGEBNIS nachgeprueft (HandBrakeCLI --scan auf die abgelegte Datei): nur noch deutsche Ton- und Untertitelspuren, kein Japanisch, keine unbenannten - vorher meldete der Disc-Scan deu 4x, jpn 2x, und 4x. Ein Fund aus dem Ergebnis, bewusst NICHT still repariert: Der Ton ist fuenfmal MP3 2.0 mit 160 kbps. Das Preset "HQ 1080p30 Surround" schreibt laut --preset-export zwei Regeln vor (av_aac stereo 160 / Surround 640); angewandt wurde nur die erste, auf jede behaltene Spur. Der Surround-Ton eines Presets namens "Surround" faellt damit still weg. Warum der Encoder MP3 statt av_aac wurde, ist ungeklaert - Rippy uebergibt keinen Audio-Schalter. Als offener Punkt notiert statt geraten; die Preset-Wahl ist eine Entscheidung des Commanders. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4915bba0b3 |
docs(savepoint): der volle Durchlauf - vier Belege und ein gefundener Fehler
Ampel / ampel (push) Failing after 30s
Belegt: Auswurf im automatischen Weg (bisher nur der UI-Knopf), Phasen-Marke
springt bei der Uebergabe auf true, Sprachwahl kommt beim externen Encoder an
("Ton: deu, Untertitel: deu"), Mount-Wache heilt in 4,2 s.
Gefunden: Jeder erste Film in einer neuen Ablage war unerreichbar - die Pruefung
liess nur eine fehlende Ordner-Ebene durch, obwohl makedirs die ganze Kette
anlegt. Der Fehler wartete seit v3.17 darauf, dass jemand eine frische Ablage
benutzt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
96c400f2f2 |
fix(worker): erster Film in einer neuen Ablage war immer unerreichbar
Ampel / ampel (push) Failing after 30s
Im ersten vollstaendigen Durchlauf aufgefallen: Der Rip lief sauber durch (40 GB Akira, Marke rip_fertig sprang korrekt auf true), die Kompression brach 0,2 s spaeter ab mit "Dieser Worker erreicht die Ziel (fertige Datei) nicht: \\192.168.178.62\rippy\movies\Akira (1988)". Die Freigabe war erreichbar. Es fehlten zwei noch nie angelegte Ordner - movies und der Filmordner darin. Die Pruefung liess aber nur EINE fehlende Ebene durch (sie sah nach dirname), obwohl gleich darauf os.makedirs die ganze Kette anlegt. Damit war jeder ERSTE Film in einer neuen Ablage systematisch unerreichbar. Jetzt zaehlt, ob irgendein Vorfahre existiert (erster_vorhandener_ordner, begrenzt auf 12 Stufen, damit auf einer toten Freigabe nicht endlos geklopft wird). Zweiter Fehler in derselben Meldung: Sie behauptete "RIPPY_PATH_MAP ist gesetzt, deckt diesen Pfad aber nicht ab" - und nannte im selben Satz den korrekt uebersetzten UNC-Pfad. Wer dem folgte, suchte in der Karte statt in der Freigabe. Die Karte wird nur noch beschuldigt, wenn sie den Pfad wirklich nicht angefasst hat; sonst steht da, was zutrifft: uebersetzt, aber gerade nicht erreichbar. Dazu der Artikel-Fehler "erreicht die Ziel" behoben. Diese Pruefung hatte KEINEN einzigen Test - deshalb kam beides durch. Jetzt 12 (301 gruen). Beim Schreiben ist noch ein dritter Fehler aufgefallen: `isdir=os.path.isdir` als Vorgabewert bindet die Funktion beim IMPORT, ein Ersetzen geht danach ins Leere. Aufloesung jetzt beim Aufruf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
936b0837bf |
docs(savepoint): Uhrzeit-Meldung erledigt - mit dem Beweis, der zaehlt
Ampel / ampel (push) Successful in 32s
Die "Substantial drift 7200 seconds"-Meldung kam nicht alle paar Sekunden, sondern einmal pro Absender-Hostname (celery memoized nach hostname) - und der Container-Hostname wechselt bei jedem Neubau. Die vier Meldungen im Log sind genau vier Deploys. Der Beweis, dass sie weg ist: Um 14:57:33 synchronisierte sich der Worker mit celery@d160eb2f3afd, dem frischen Container nach dem Deploy. Ein NEUER Hostname umgeht die Merk-Sperre, die Warnung haette also feuern muessen. Sie kam nicht. Dazu gemessen: TZ=UTC wirkt auf Windows wirklich (time.timezone -3600 -> 0), und die celery-Zeitstempel im lokalen Worker-Log stehen in UTC. Ausserdem festgehalten: Der Windows-Worker laeuft auf gemischten Dateistaenden (tasks.py 13:19, ripping.py 14:06, zombies.py 14:17) - die Sprachwahl ist vollstaendig drin, meta_merken fehlt noch. Es fehlt eine Versionsanzeige, die so etwas sichtbar macht, statt es nur beim Nachsehen auf dem PC zu finden. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a9cfcb272a |
docs(konzept): Rate-Limit 100/min -> 600/min als Abweichung vermerkt
Ampel / ampel (push) Successful in 32s
Regel B: keine stille Aenderung an einer KONZEPT-Vorgabe. Das Muss-Feature "Rate-Limiting pro Client-IP" bleibt, nur die Zahl aendert sich - mit der Rechnung, die zeigt warum: 100/min lag unter Rippys eigener Last (ein offener Tab braucht 111/min, ein Windows-Worker 12 dazu). Festgehalten durch test_grenze_deckt_die_eigene_last_ab, damit die Zahl nicht unbemerkt zurueck unter die Grundlast wandert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
163216a68e |
docs(savepoint): v3.20 - die Bremse war das Problem, nicht die Last
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> |
||
|
|
9156e8a4a9 |
fix(deploy): leere Argumente und eine fehlende .env brachen das Deploy - still
Ampel / ampel (push) Successful in 30s
Zwei Funde beim Deployen, beide fuer die Weitergabe relevant:
1. `./deploy.sh` ohne Argumente - der dokumentierte Normalfall - endete mit
"line 5: $4: unbound variable". Leere Argumente ueberleben den Weg durch ssh
nicht: Die Gegenseite bekommt die Befehlszeile als EINEN String und parst sie
neu, wobei "" ersatzlos verschwindet. ${4:-} statt "$4".
2. Fehlte die .env-Quelle, war das eine geschluckte Warnung. Auf der Ziel-VM lag
die echte .env unter ~/projects/rippy/.env, der Default zeigte auf
~/rippy/.env. Das `cp` schlug also jedes Mal fehl, die Warnung scrollte im
Build-Rauschen vorbei, und gebaut wurde mit der Kopie im Klon: zwei Tage alt,
darin ein MAKEMKV_URL_BASE-Notbehelf auf einen web.archive.org-Schnappschuss
(liefert inzwischen 525, waehrend makemkv.com wieder 200 gibt) und ein
JWT_SECRET_KEY aus der Zeit vor dem Auth-Rueckbau. Jeder worker-Build brach
daran ab, und die Ursache stand nirgends.
Jetzt: .env uebernommen -> es steht da. Quelle fehlt, Klon hat eine -> laute
Warnung samt Dateidatum und Kandidatenliste. Keine von beiden -> Abbruch, statt
mit leerem DB-Passwort zu bauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
bfb13f44a5 |
fix(ui+api): das "Neuladen" war HTTP 429 - und "Neu" komprimierte immer
Ampel / ampel (push) Successful in 30s
Zwei Commander-Befunde, beide mit derselben Wurzel: Rippy hat sich selbst
ausgebremst und dann geschwiegen.
## "Wenn der Worker installiert ist, wird dieser Bereich oft neu geladen"
Gemessen statt geraten. Die Antworten von /jobs und /capabilities waren ueber
zwanzig Sekunden byteweise identisch, alle Endpunkte antworteten unter 30 ms -
es wurde also gar nichts neu geladen. Im nginx-Log standen dagegen 97 Antworten
mit HTTP 429.
Drei Fehler griffen ineinander:
1. Das Limit war zu klein fuer Rippy selbst: 100 Anfragen/min, waehrend ein
offener Tab 111/min verursacht (Dashboard 75 + Log-Kasten 24 + Laufwerke 12)
und der Windows-Tray weitere 12/min dazulegt.
2. Der nginx gab die Client-Adresse nicht weiter. Fuer die API kam damit ALLES
von 172.19.0.6 - Browser, zweiter Tab und Tray teilten sich einen Eimer
(812 von 876 Anfragen). Das erklaert die Kopplung an den Worker: tray.py
fragt /api/jobs ueber Port 80, also durch denselben Proxy.
3. Ein abgewiesener Abruf leerte das UI. `catch(() => [])` heisst "es gibt
keine Jobs" - richtig waere "ich weiss gerade nichts Neues". Fuer einen Takt
stand "Keine Jobs", die Zaehler sprangen auf (0), vier Sekunden spaeter war
alles zurueck.
Behoben: X-Real-IP im nginx, Grenze auf 600/min mit vorgerechneter Herleitung,
jeder Fehlschlag laesst den alten Stand stehen (null statt []), axios bekommt
eine Zeitgrenze, und das Dashboard trennt schnelle Daten (Jobs/Laufwerke, 4 s)
von langsamen (Hardware/Worker/Ablagen, 12 s) - 75/min werden zu 30/min.
Ein greifendes Limit steht ab jetzt im Log, gedrosselt auf eine Meldung pro
Client und Minute.
## "Hier gibt es den Button 'neu' aber WAS wird dann gemacht?"
Immer die Komprimierung - auch bei einem Job, dessen RIP abgebrochen war. Am
26.07.2026 waeren aus 5,1 GB Bruchstueck (von rund 40 GB) brav ein Film
geworden, der bei 12 % aufhoert.
Die Phase war nach `status = "failed"` nicht mehr feststellbar, also wird sie
jetzt vermerkt (rip_fertig in den Job-Metadaten: false beim Rip-Start, true bei
der Uebergabe an die Kompression). Daraus folgt die Beschriftung: "Neu
komprimieren", "Neu rippen" - oder bei Bestandsjobs ohne Vermerk ein Dialog,
der beide Wege erklaert und die Groesse der Rohdaten als Entscheidungshilfe
nennt. Geraten wird nicht. Fuer den Rip-Fall gibt es POST
/jobs/{id}/retry-rip: neuer Job mit neuer ID (sonst laege das Bruchstueck im
Roh-Verzeichnis des neuen Rips), Titel/Ablage/Sprachwahl uebernommen, mit
ehrlicher Absage wenn keine Disc im Laufwerk liegt.
10 neue Tests (289 gruen), darunter eine Kopplungspruefung: der Name der
Phasen-Marke muss in worker/tasks.py und api/phasen.py zusammenpassen - genau
diese Sorte Auseinanderdriften hat die Zombie-Erkennung ein Release lang blind
gemacht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f7555a17d1 |
fix(zombies): die Fehlermeldung unterscheidet jetzt Rip von Kompression
Ampel / ampel (push) Successful in 29s
Nachtrag, direkt am echten Fall aufgefallen. Die Zombie-Erkennung hat den toten
Rip gefunden (Beleg: nach 140 s noch "processing 12 %", nach 160 s "failed") -
aber der Fehlertext sagte:
"Die Rohdateien wurden NICHT geloescht: mit 'Neu komprimieren' laeuft die
Kompression erneut, ohne die Disc noch einmal zu rippen."
Der Satz war fuer einen toten TRANSCODE geschrieben, wo die Roh-MKV vollstaendig
ist. Seit "running" mit zu den Arbeitsstati gehoert, traf er auch abgebrochene
RIPS - und da ist er schlicht falsch: Die Datei ist ein Bruchstueck (im Vorfall
5,1 GB von rund 40), und wer dem Rat folgt, komprimiert einen Film, der bei 12 %
aufhoert.
Jetzt unterscheidet der Text die Phase:
Rip tot -> "Der Rip war bei 12 % - die Roh-Datei ist UNVOLLSTAENDIG ...
Richtig ist: Disc wieder einlegen und neu rippen."
Transcode tot -> "Der Rip war fertig, nur die Kompression nicht ..."
Die deutschen Anfuehrungszeichen haben dabei zum vierten Mal an einem Tag
zugeschlagen (in DOPPELT gequoteten Python-Strings beendet das schliessende
Zeichen den String). Die betroffenen Zeilen sind jetzt einfach gequotet, wie es
der Bestand ohnehin macht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
19f3dc5330 |
fix(zombies): "running" fehlte - ein abgestuerzter RIP wurde NIE gefunden
Ampel / ampel (push) Successful in 30s
Der Commander: "Ausserdem ist gerade mitten im Rip das Laufwerk ausgegangen...
glaube ich zumindest." Gemessen war es etwas anderes, und der Fund ist groesser
als der Vorfall.
WAS MESSBAR WAR:
Job 2182d525 status=running progress=12 (Rip gestartet 14:00:24)
makemkvcon laeuft NICHT (per /proc geprueft, PID gegengeprueft)
Rohdatei 5.167.382.528 Bytes, waechst in 10 s nicht
Laufwerk Status 4 (Disc drin), /dev/sr0 + /dev/sg1 da
Worker-Start 14:07:02 <- mein `docker compose up -d --build`
Das Laufwerk ist also NICHT ausgegangen. Der Rip wurde von MEINEM Deploy
getoetet: `up -d --build` baut den worker-Container neu, und der laufende Rip
stirbt mit ihm. Genau davor warnt der SAVEPOINT seit v3.18 - die Warnung half
nichts, weil sie niemand liest und nichts sie prueft.
DER EIGENTLICHE FUND: Die Zombie-Erkennung lief um 14:09:04 und meldete
`{'geprueft': 0, 'aufgeraeumt': []}` - obwohl der tote Job direkt vor ihr lag.
Ursache:
ARBEITS_STATI = ("ripping", "transcoding", "canceling") # zombies.py
db.update_job(job_id, status="running", ...) # tasks.py - der Rip
Der Rip setzt "running", gesucht wurde "ripping". Dieser Wert steht
ausschliesslich in Celerys Task-META und NIE in einer Job-Zeile (nachgeprueft:
kein einziger Schreiber im ganzen Baum). Die Zombie-Erkennung aus v3.14 wurde
gebaut, um genau einen abgestuerzten Rip zu finden - und hat ihn nie gesehen.
Besonders tueckisch: `geprueft: 0` sah bei jedem Worker-Start wie "nachgesehen,
alles gesund" aus, waehrend sie nach einem Status suchte, den es nicht gibt.
Deshalb blieb der Job auf "processing 12 %" stehen - mit einer Restzeit-Schaetzung
von 1 h 30 min obendrauf, die es fuer einen toten Prozess nicht geben duerfte.
GEBAUT:
* "running" in ARBEITS_STATI.
* Ein Test, der das Auseinanderlaufen MECHANISCH verhindert: Er liest tasks.py,
sammelt jeden Status, den der Worker per db.update_job in eine Job-Zeile
schreibt, und verlangt, dass jeder davon entweder ein Arbeitsstatus oder ein
Endzustand ist. Ein Kommentar haette das nicht verhindert.
* GET /health/arbeit + eine Sperre in deploy.sh: Laeuft ein Job, bricht der
Deploy ab (uebersteuerbar mit RIPPY_TROTZDEM=1 - dann ist es eine
Entscheidung und kein Versehen). Ein Satz Code gegen eine verlorene Stunde.
* Server-Status zeigt jetzt die eingehaengten FREIGABEN mit freiem Platz, nicht
nur die Container-Platte (Commander-Wunsch). Genau dort liegen die Rohdaten,
und bei externem Encoden muessen sie dort liegen - wer wissen wollte, ob noch
Platz fuer eine Disc ist, sah die falsche Zahl.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
6438beec05 |
fix(worker): kein CMD-Blitzen mehr - und die 7200-Sekunden-Meldung ist erklaert
Ampel / ampel (push) Successful in 31s
Zwei Meldungen, beide auf dieselbe Wurzel zurueckgefuehrt: Der Worker startet
Konsolenprogramme, und das hat unter Windows Nebenwirkungen.
1. "Es geht immer alle paar Sekunden ne CMD auf." Kein Geist, sondern der
HERZSCHLAG. Der Worker laeuft als pythonw.exe, also ohne eigene Konsole. Wer
ohne Konsole ein Konsolenprogramm startet, bekommt von Windows eine NEUE - und
die ist sichtbar. `capture_output=True` hilft nicht: es leitet die Datenstroeme
um, unterdrueckt aber kein Fenster. Und caps.py fragt jede Minute
`HandBrakeCLI --help`, `--preset-list` und `--version` ab, dazu makemkvcon aus
der Schluessel-Automatik: vier bis fuenf Fenster pro Minute, in Schueben.
Neues winlauf.py haelt CREATE_NO_WINDOW an EINER Stelle; alle elf Aufrufe in
caps.py, ripping.py und schluessel.py benutzen es. Auf Linux ist der Wert 0 und
creationflags=0 eine Nulloperation - im Worker-Container gegengeprueft, damit
derselbe Code auf beiden Seiten laeuft.
2. Die 7200 Sekunden sind CELERYS EIGENE UMRECHNUNG, nicht die Uhren. Erst fiel
auf, dass die Sekundenbruchteile nur 40 ms auseinanderliegen (.177269 vs
.134799) - derselbe Augenblick, zweimal verschieden gerechnet. Die Quelle sagt
warum (celery/utils/time.py):
def utcoffset(): # Sekunden WEST von UTC, in Stunden
return time.altzone // 3600 # CEST -> -2 ; UTC -> 0
def adjust_timestamp(ts, offset, here=utcoffset):
return ts - (offset - here()) * 3600
Absender VM (offset 0), Empfaenger Windows-PC (here() = -2):
ts - (0 - (-2)) * 3600 = ts - 7200. Celery verschiebt den empfangenen Stempel
also selbst und vergleicht ihn dann mit der eigenen Uhr. Die Annahme dahinter -
Ereignisse truegen Ortszeit - stimmt nicht, `Event()` stempelt Epoch.
Abhilfe gemessen, nicht geraten: Mit TZ=UTC meldet Windows utcoffset 0
(timezone=0, isdst=0 - auf dem Commander-PC geprueft), damit wird die
Umrechnung zur Nulloperation. Beide .bat-Dateien setzen es jetzt. Nebeneffekt
ist erwuenscht: Der Worker rechnet dann in derselben Zone wie die VM und wie
Rippys UI.
Fuer Rippy war die Meldung ohnehin folgenlos - /capabilities fragt per
Celery-Ping (Frage/Antwort, ohne Zeitstempel), nicht ueber die
Gossip-Ereignisse. Aber eine Warnung, die bei jedem Blick ins Log steht und
nichts bedeutet, kostet Vertrauen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
5f01aa89fc |
fix(deinstaller): er brauchte Adminrechte und hatte keine
Ampel / ampel (push) Successful in 30s
Commander: "Ich kann den worker uebrigens immernoch nicht deinstallieren."
Der Screenshot zeigt ZWEI Dinge. Das erste ist erklaerbar: Es lief noch der ALTE
uninstall.ps1 von der letzten Installation - Zeile 2 mit dem kaputten
`if (-not $Force) { if ([System.Windows.Forms.MessageBox]...` . Meine Reparatur
schreibt die Datei erst beim naechsten Installer-Lauf neu.
Das zweite ist MEIN Fehler, und er haette auch die neue Fassung erwischt:
Remove-Item : Das Element C:\Program Files\Rippy Worker\doc\LICENSE
kann nicht entfernt werden: Der Zugriff auf den Pfad wurde verweigert.
Der Standard-Zielordner liegt unter "C:\Program Files", und der gehoert nicht dem
Benutzer. Die Installer-.exe ist mit -requireAdmin gebaut und fragt beim Start per
UAC - der Deinstaller hatte nichts Vergleichbares. Er war damit im STANDARDFALL
nutzlos, und mein `rd /s /q` waere genauso aufgelaufen.
Jetzt erhoeht er sich selbst: Schreibprobe im Ordner, Admin-Abfrage, und nur wenn
beides fehlt, ein Neustart per RunAs mit -Force (damit die Bestaetigung nicht
zweimal kommt). GEMESSEN statt angenommen - wer den Worker ins eigene Profil
installiert hat, bekommt gar keine UAC-Frage. An seinem echten Ordner
gegengeprueft: schreibbar=False, istAdmin=False -> Entscheidung "erhoehen".
Der erzeugte Deinstaller ist diesmal komplett nachgestellt worden: parst, und die
Reihenfolge stimmt (Add-Type Zeile 7, MessageBox Zeile 10, Schreibprobe,
Erhoehung, erst dann Loeschen).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
f146f33f5d |
feat(installer): Verknuepfung auf dem Desktop - mit Rippy-Icon
Ampel / ampel (push) Successful in 30s
Commander: "Und wenn man den worker installiert braucht man auch eine exe auf dem
Desktop abgelegt wird zum starten you know?"
Berechtigt: Die Startdateien lagen nur im Programmordner. Wer den Worker von Hand
starten wollte, musste erst "C:\Program Files\Rippy Worker" suchen - und genau
das war heute noetig, als der Worker stand.
Jetzt legen beide Installer eine Verknuepfung "Rippy Worker" auf den Desktop
(GUI mit Haekchen, Kommandozeile per -KeineDesktopVerknuepfung abschaltbar). Drei
Details, die den Unterschied machen:
* Das ICON wird mit ausgeliefert. rippy.ico lag im Repo, ging aber nie an die
Zielmaschine - die Verknuepfung haette das Batch-Standardsymbol getragen und
waere zwischen den anderen Icons unfindbar gewesen. Jetzt im Worker-Paket.
* WindowStyle 7 (minimiert): start-tray.bat startet pythonw, also ohne
Konsolenfenster - aber die .bat selbst blitzt sonst kurz auf. Gilt jetzt auch
fuer die Autostart-Verknuepfung, wo derselbe Blitz war.
* Der DEINSTALLER nimmt sie mit, an beiden moeglichen Orten (eigener Desktop
und Desktop aller Benutzer). "Rueckstandsfrei entfernbar" ist ein
MUSS-Kriterium; ein totes Symbol auf dem Desktop waere genau so ein Rueckstand.
Dieselbe Rechte-Ueberlegung wie beim Autostart: bei einer Installation unter
"Programme" auf den Desktop ALLER Benutzer, sonst auf den eigenen - bei einer
UAC-Erhoehung ueber ein fremdes Admin-Konto waere der eigene der falsche.
Layout headless gerendert und angesehen (Fenster 648 -> 672 px, nichts
ueberlappt), beide .ps1 mit echtem PowerShell 5.1 geprueft, .exe neu gebaut.
Und die CRLF-Falle aus dem Savepoint hat prompt wieder zugeschlagen: Mein
Python-Rewrite machte aus install-gui.ps1 LF - zurueckgedreht, `file` bestaetigt
BOM und CRLF fuer beide.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
a448ebc4de |
fix(ui): die Pfad-Warnung sagt jetzt, was zu tun ist - und bietet einen Knopf
Ampel / ampel (push) Successful in 29s
Commander: "Das ist ja quatsch. Mein PC hat das Ziel als Worker direkt auf dem
PC eingebunden."
Nachgemessen: Die Warnung hatte SACHLICH recht, war aber unbrauchbar. Sein PC hat
die Freigabe wirklich als Netzlaufwerk (X:) gemountet - nur lag das
ARBEITSVERZEICHNIS darauf und die ABLAGE nicht. Das Ziel war weiter
/app/media/movies, also die VM-Platte, und dorthin kommt sein PC nicht. Beleg aus
der Worker-Meldung:
pfad_map = /app/media/rippy=\192.168.178.62\rippy
Ziel = /app/media/movies <- nicht abgedeckt, richtig gewarnt
Die alte Meldung nannte den Container-Pfad und die Mapping-Zeichenkette - also
genau die zwei Dinge, die ein Nicht-Entwickler nicht deuten kann. Jetzt steht der
Handgriff drin, mit dem NAMEN der Freigabe, die dieser Worker wirklich erreicht
("Einstellungen -> Ripping -> Ablage auf 'rippy' stellen").
Dazu ein Knopf, der das Ziel NUR FUER DIESEN RIP auf die Freigabe legt
(/app/media/movies -> /app/media/rippy/movies), ohne die globale Ablage
anzufassen. Wer den Rip jetzt starten will, will jetzt eine Loesung und keine
Wegbeschreibung.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b4a0dd551a |
docs(savepoint): v3.19 - sieben Meldungen, drei echte Fehler, eine Kette
Ampel / ampel (push) Successful in 30s
Der Savepoint haelt fest, was gemessen wurde und was nicht. Die wichtigste Lehre steht auch in AGENTS.md: Bei "zu langsam" die DAUER je Schritt messbar machen statt die plausibelste Ursache zu beheben. Meine erste Erklaerung fuer die 150 s war falsch und machte es sogar langsamer (202 s); erst Zeitstempel im Log zeigten die Stelle - ein os.makedirs, das drei Minuten im Kernel hing. Danach 8 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
848deb1c24 |
fix(ui): Sprachnamen auf Deutsch - "Undetermined" versteht niemand
Ampel / ampel (push) Successful in 29s
An der echten Akira-Blu-ray gemessen, nachdem die Sprachauswahl live lief:
Ton: deu 4x, jpn 2x, und 4x
Untertitel: deu 4x, und 8x
MakeMKV liefert die Namen englisch ("German", "Japanese") und fuer Spuren ohne
Sprach-Kennzeichnung "Undetermined" - das sind typischerweise Audiokommentare.
Der Commander liest alles auf Deutsch (AGENTS Grundregel 4), und
"Undetermined" ist fuer einen Nicht-Entwickler schlicht keine Auskunft. Jetzt
steht dort "Ohne Sprachangabe", und die zwei Dutzend haeufigsten Codes haben
deutsche Namen. Unbekannte Codes behalten MakeMKVs Namen - geraten wird nichts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
9230d0dd7a |
fix(weitergabe): drei Luecken geschlossen, die einen Fremden gestolpert haetten
Ampel / ampel (push) Successful in 29s
Weitergabe nicht durch Lesen geprueft, sondern indem ich mich wie ein fremder Rechner verhalten habe: frischer `git clone` von Gitea in ein leeres Verzeichnis, dann `./install.sh --nur-pruefen`. Ergebnis: 2,6 MB, alles gruen, Laufwerk samt richtigem sg-Knoten ueber die SCSI-Adresse erkannt. Der Weg selbst traegt. Drei Luecken sind dabei aufgefallen: 1. IN BEISPIELBEFEHLEN STAND MEINE IP. `install.ps1` und `remote-transcode-worker.yml` nannten im Aufruf-Beispiel 192.168.178.162 - also genau die Zeile, die ein Fremder kopiert. Jetzt Platzhalter, beim Windows-Installer mit dem Hinweis, wo die richtige IP steht (und dass es NICHT die des eigenen PCs ist - der haeufigste Irrtum). 2. DER ASSISTENT FRAGTE DEN MAKEMKV-BETA-KEY NICHT. Er stand in der README, in der .env und in den Einstellungen - nur nicht dort, wo man beim Einrichten hinsieht. Ein Fremder installiert also, legt eine Blu-ray ein und bekommt spaeter einen Fehlschlag, ohne dass ihn jemand darauf hingewiesen haette. Das ist die wahrscheinlichste Stolperstelle einer frischen Installation. Jetzt fragt der Assistent ihn ab, mit dem Unterschied im Klartext: DVDs gehen ohne, Blu-ray braucht ihn - und er ist NICHT der Disc-Schluessel einer 4K-Disc. 3. Eine Docstring nannte meine NAS-IP als Beispiel - jetzt neutral (//NAS/rippy). Tests und Log-Beispiele behalten die echten Namen: Sie dokumentieren Messungen, und genau das ist ihr Wert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c171f8879c |
feat(schluessel): der Windows-PC holt die 4K-Schluessel jetzt selbst
Ampel / ampel (push) Successful in 31s
Auf Commander-Entscheid gebaut. Belegt am 25.07.2026 auf beiden Maschinen:
`makemkvcon` unter LINUX ruft Disc-Schluessel NIE ab, die WINDOWS-Version schon
(Meldung 3338). Deshalb scheiterte jede unbekannte UHD-Disc auf der VM mit "The
volume key is unknown", und der Weg, der funktioniert, war Handarbeit: Laufwerk
an den PC, Disc oeffnen, _private_data.tar suchen, im UI hochladen.
Das laeuft jetzt von selbst - und zwar ZWEISCHICHTIG, mit Absicht:
1. Der WAECHTER (verlaesslich): sieht _private_data.tar nach und laedt sie zu
Rippy hoch, sobald sie sich geaendert hat. Braucht keine
Laufwerkserkennung, kein Disc-Oeffnen, nichts geraten. Deckt auch den Fall
ab, dass man die Disc einfach in der MakeMKV-Oberflaeche oeffnet.
2. Das ANSTOSSEN (nach bestem Wissen): liegt eine Disc im Laufwerk, wird
`makemkvcon info` darauf losgelassen - dabei holt MakeMKV den Schluessel.
Warum getrennt: Das Format der BELEGTEN `DRV:`-Zeile liess sich auf dem
Commander-PC nicht messen, weil dort kein optisches Laufwerk steckt (alle 16
Plaetze melden `DRV:i,256,999,0,"","",""` - das ist gemessen). Geraten wird also
nur in Schicht 2, und wenn die Vermutung falsch ist, passiert dort einfach
nichts - Schicht 1 arbeitet weiter. Die teure Annahme steckt nie im
verlaesslichen Teil.
Gemessene Fundstellen: Datenverzeichnis ist `%USERPROFILE%\.MakeMKV` (NICHT
%APPDATA%\MakeMKV, wie man vermuten wuerde) - dort lag die echte Datei mit
6.420.480 Bytes. Programm: C:\Program Files (x86)\MakeMKV\makemkvcon64.exe,
v1.18.4. Hochgeladen wird mit ROHEM Koerper an POST /system/keystore, weil der
API python-multipart fehlt.
Die Automatik schaltet sich selbst ab, wenn MakeMKV nicht installiert ist: Auf
einem reinen Encoding-PC gibt es nichts zu holen, und eine Schleife, die jede
Minute ins Leere greift, waere nur Rauschen. Ihre Meldungen gehen ueber die
Log-Bruecke auch nach Rippy - das ist genau die Auskunft, auf die man nach dem
Einlegen einer neuen UHD-Disc wartet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
148ac494c2 |
feat(sprachen): Rippy fragt vor dem Rip, welche Sprachen du willst
Ampel / ampel (push) Successful in 29s
Commander-Anforderung: "Die Disc hat Material in X Sprachen und X Untertiteln -
Rippy muss VOR dem Rip fragen: Was genau willst du haben? In der Automatik muss
das ebenfalls einstellbar sein."
Die Auskunft lag laengst vor und wurde weggeworfen: Derselbe Titel-Scan, der die
Titel-Tabelle fuellt, liefert in derselben makemkvcon-Ausgabe die Streams mit.
Ein zweiter Info-Lauf haette eine Minute Wartezeit gekostet - jetzt kommt beides
aus einem Aufruf.
Format an der Akira-Blu-ray im Laufwerk gemessen (AGENTS Regel D):
SINFO:<titel>,<stream>,<attribut>,<code>,"<wert>"
1 = Typ ("Audio"/"Subtitles"), 3 = Sprachcode, 4 = Sprachname,
6 = Codec, 14 = Kanaele, 30 = Beschreibung
DIE FALLE dabei: Die Sprache steht in 3/4, NICHT in 28/29. Die tragen auf JEDEM
Stream "eng"/"English" - auch auf einer deutschen Tonspur und auf dem
Videostream; das ist MakeMKVs eigene Anzeigesprache. Wer 28 nimmt, haelt jede
Disc fuer englisch. Ein Test haelt das fest.
Angewendet wird die Wahl bei der KOMPRESSION, nicht beim Rippen - drei Gruende:
der Rip bleibt vollstaendig und verlustfrei (Muss-Feature laut KONZEPT); HandBrake
hat dafuer dokumentierte Schalter (--audio-lang-list / --subtitle-lang-list, die
genau die ISO-639-2-Codes nehmen, die MakeMKV liefert - beides gegengeprueft);
und wer spaeter andere Sprachen will, komprimiert neu statt die Disc wieder
einzulegen. Genau das sagt der Dialog auch, sonst glaubt man, es werde
unvollstaendig gerippt.
Nichts angeklickt heisst "alles behalten" - das Verhalten von vorher. Auch die
Einstellung ist bewusst LEER vorbelegt: Ein stilles "deu" wuerde bei einem
japanischen Original die Originaltonspur wegwerfen, ohne dass jemand gefragt hat.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
58f4991d92 |
fix(mounts): 3 min 15 s hingen in EINER Zeile - os.makedirs auf dem toten Mount
Ampel / ampel (push) Successful in 29s
Nachtrag, weil die letzte Runde die Wiederanbindung nicht schneller machte
(202 s statt 150 s). Die Zeitstempel im Log zeigten, wo die Zeit sitzt:
12:53:56 API gestartet
12:54:03 "antwortet nicht - wird neu verbunden" <- Erkennung: 7 s, gut
12:57:18 "eingehaengt" <- Reparatur: 3 min 15 s
Die Erkennung war also schon schnell; die REPARATUR fraess die Zeit. Und zwar
nicht in den Mount-Versuchen, sondern in der ersten Zeile von mounten():
os.makedirs(ziel, exist_ok=True)
`exist_ok` prueft mit os.path.isdir, und ein `stat` auf einen toten CIFS-Mount
blockiert im Kernel bis zum SMB-Timeout. Ausgerechnet der Aufruf, der nur "lege
den Ordner an, falls er fehlt" bedeutet, hing drei Minuten - BEVOR irgendeine der
sorgfaeltig begrenzten Pruefungen dran war. Dritter Fund derselben Sorte an einem
Tag: os-Aufruf auf einen Netzpfad ohne Zeitgrenze.
Jetzt klaert `pfad_lage()` die Lage mit einem abbrechbaren Kind-Prozess
(`timeout 4 ls -d`, drei Antworten: da / weg / unklar), und makedirs laeuft nur
bei "weg". Dieselbe Falle in `reparieren()` (os.path.ismount als Vorbedingung -
gebraucht wird es nicht, `umount -l` auf einen leeren Pfad kostet nichts) und in
`aushaengen()` (ismount + rmdir).
Dazu steht die DAUER jetzt im Log ("neu verbunden (4.2s)"). Sie war die
entscheidende Spur; wer sie ablesen kann, muss sie nicht rekonstruieren.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2a90538473 |
fix: Deinstaller, Verwaltungsfenster, Dashboard-Widerspruch, Mount-Tempo
Ampel / ampel (push) Successful in 30s
Vier Meldungen des Commanders, alle nachgemessen.
1. DEINSTALLER LIEF NICHT MEHR - reproduziert mit echtem PowerShell:
Der Typ [System.Windows.Forms.MessageBox] wurde nicht gefunden.
`Add-Type -AssemblyName System.Windows.Forms` stand EINE ZEILE ZU SPAET, die
MessageBox wurde davor benutzt. Der Deinstaller starb also in seiner ersten
Arbeitszeile, jedes Mal. Zwei weitere Maengel gleich mit:
Kodierung war ASCII trotz Umlauten, und `Remove-Item -Recurse -Force
$PSScriptRoot` loescht den Ordner, in dem das laufende Skript liegt - das
klappt auf Windows nicht zuverlaessig (venv-DLLs sind geladen). Jetzt raeumt
ein losgeloestes cmd nach, sobald PowerShell weg ist. Der erzeugte Deinstaller
ist gegengeprueft: parst, BOM da, Umlaute intakt.
2. VERWALTUNGSFENSTER (Doppelklick aufs Tray). Zeigt Status, Aufgaben und Log,
plus Knoepfe fuer Rippy, Log-in-Rippy und Deinstallieren - Deinstallieren geht
damit auch aus dem Tray-Menue. Eigener PROZESS statt Fenster im Tray, weil
pystray und tkinter beide den Haupt-Thread wollen; tkinter statt WinForms,
weil es bei jeder Windows-Python-Installation dabei ist. Headless gerendert
und angesehen. Alles Fachliche kommt von Rippy (/capabilities, /jobs), damit
dort nicht eine zweite, abweichende Wahrheit steht.
3. DASHBOARD-WIDERSPRUCH. Oben stand "Akira im Laufwerk erkannt", die
Server-Status-Karte gleichzeitig "Bereit - keine Disc in Arbeit / Disc
einlegen". Zwei Aussagen, ein Blick. Die Karte fragt jetzt /devices und sagt
"Disc erkannt - wartet auf Rippen starten" samt Titel.
4. MOUNT-TEMPO. Der Commander: "150 Sekunden? Das ist verrueckt langsam." Recht
hat er. Gemessen ging die Zeit fast komplett in einen FEHLVERSUCH: `umount -l`
ist lazy, der Abbau passiert spaeter, und wer direkt danach mountet, riskiert
dass der Abbau hinter dem neuen Mount landet. Der erste Reparaturversuch
scheiterte dadurch regelmaessig - und der zweite kostete 30 s mount-Timeout
plus Pruefungen. Jetzt werden 1,5 s auf den Abbau gewartet, bevor neu
gemountet wird; die Wache prueft erstmals nach 3 s statt 10 s und benutzt zum
ERKENNEN die einfache schnelle Probe (die Doppelprobe steckt dort, wo der
Wettlauf lauert: direkt nach dem Mount).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4c2331563c |
docs(savepoint): v3.18 - Auswurf, externer Worker, und die Mount-Ursache
Ampel / ampel (push) Successful in 31s
Stellt eine Aussage aus v3.17 richtig: Dort stand die Mount-Sache als "Ursache liegt beim NAS, nicht gefunden". Gemessen liegt sie bei uns - die CIFS-Verbindung lebt in der Netz-Namespace des api-Containers und stirbt mit ihm. Das NAS ist unschuldig. AGENTS.md bekommt die Lehre, die diesen Abend zweimal gekostet hat: Ein Rueckgabewert ist kein Beweis, wo die Wirkung pruefbar ist. CDROMEJECT quittiert Erfolg auf einem verriegelten Laufwerk, `mount` quittiert Erfolg auf einer Verbindung, die Sekunden spaeter stirbt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4cf7acbb96 |
fix(mounts): DIE URSACHE gefunden - die CIFS-Verbindung stirbt mit dem Container
Ampel / ampel (push) Successful in 31s
Der Savepoint v3.17 fuehrte das als "Ursache liegt beim NAS, nicht gefunden". Gemessen ist es etwas ganz anderes, und es liegt bei uns: /proc/fs/cifs/DebugData -> Net namespace: 4026532653 api-Container -> net:[4026532653] DIESELBE worker-Container -> net:[4026532540] andere Die CIFS-Verbindung lebt in der NETZ-NAMESPACE DES API-CONTAINERS - dort wird sie eingehaengt, weil nur dieser Container CAP_SYS_ADMIN hat. Wird der Container neu gebaut, stirbt sein Netz-Namespace und mit ihm der Socket. Der Mount steht danach weiter in /proc/mounts (per rshared auf den Host propagiert) und sieht vollkommen gesund aus - aber jeder Zugriff laeuft in den CIFS-Timeout. Damit erklaert sich alles, was vorher widerspruechlich aussah: warum es nach JEDEM Deploy passiert, warum `mount` Erfolg meldet, warum /proc/mounts genau eine korrekte Schicht zeigt, und warum nur ein echtes Neu-Verbinden hilft. Das NAS ist unschuldig (eine Sitzung, Status 1, 630 Credits, Ping 0,47 ms). Zweiter Fund, der den Rest erklaert: Direkt nach einem frischen Mount antwortete die Freigabe - und Sekunden spaeter nicht mehr. Das ist ein Wettlauf mit `umount -l`: lazy heisst, der Abbau passiert spaeter, und faellt er samt Propagation hinter den neuen Mount, zeigt der Pfad wieder auf die Leiche. Eine einzige Probe kann das nicht sehen - deshalb prueft `wirklich_erreichbar()` zweimal mit drei Sekunden Abstand, und zwar sowohl beim Mounten als auch in der Wache. Dazu: erste Pruefung der Wache schon nach 10 s statt 60 s. Genau dann ist die Lage nach einem Deploy kaputt. Ehrlich offen bleibt die strukturelle Folge: Der api-Container HAELT die NAS-Verbindung. Startet er mitten in einem Rip neu, verliert auch der Worker sein Ziel. Das saubere Gegenmittel waere ein Mount auf dem HOST statt im Container - ein eigener Umbau, und er widerspraeche "Speicherziele ueber das UI einhaengen". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
549727f648 |
fix(mounts): Wache, die die Freigabe nach einem Rebuild von selbst zurueckholt
Ampel / ampel (push) Successful in 28s
Dreimal in Folge reproduziert, jetzt bei JEDEM Deploy: Nach `docker compose up -d --build` ist die CIFS-Freigabe tot. `mount` meldet Rueckgabewert 0, /proc/mounts zeigt genau eine korrekt aussehende Schicht, die Erreichbarkeits-Probe antwortet direkt nach dem Mount sogar - und Sekunden spaeter laeuft jeder Zugriff in die Zeitgrenze. Derselbe Ablauf ein bis zwei Minuten spaeter stellt sie zuverlaessig her (POST /storage-mounts/rippy/repair, mehrfach belegt). Die Ursache liegt am NAS und ist nicht gefunden. Aber die Wirkung ist teuer: Nach jedem Update war jeder Rip auf die NAS kaputt, ohne dass irgendwo etwas davon zu sehen war - und der Commander haette es jedes Mal von Hand richten muessen. Wenn die Heilung bekannt und billig ist, gehoert sie automatisiert, auch ohne die Ursache zu kennen. Die Wache sieht jede Minute nach und verbindet stumme Freigaben neu. Zwei Dinge sind dabei wichtiger als die Heilung selbst: 1. NIE waehrend ein Job laeuft. Neu verbinden heisst `umount -l`; mitten in einem Rip oder Encode waere das ein Datenverlust. Die Wache steht still, solange irgendein Job nicht durch ist - auch bei einem wartenden, der jeden Moment anlaufen kann (db.hat_arbeit). 2. Gemeldet wird nur der UEBERGANG. Ist das NAS ausgeschaltet, waere ein Log je Minute ein Wasserfall. Der Zustand steht in /health/vorraete, damit man sehen kann, dass die Wache lebt - dieselbe Lehre wie heute Nachmittag beim stillen except. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
87484d6863 |
feat(worker): der externe Worker wird erwachsen - Log in Rippy, Anzeige, Slots
Ampel / ampel (push) Successful in 28s
Commander 26.07.2026: "der externe Encoder Worker ist ein bisschen duenn - der koennte noch viel mehr." Drei Punkte, alle am Tray. 1. LOG IN RIPPY STATT TXT-DATEI (ausdruecklich gewuenscht). Das Tray schrieb sein Log nach %LOCALAPPDATA% und oeffnete es im Editor - wer wissen wollte, warum der Worker nichts tut, musste sich an den PC setzen. Neue Bruecke (logbruecke.py) meldet die wichtigen Zeilen nach Rippy, Quelle "w:<name>", und die Logs-Seite hat jetzt Knoepfe je Quelle: "was macht mein PC" ist ein Klick. Der Filter konnte Quellen schon immer, es gab nur keinen Knopf. Durchgelassen wird WENIG und mit Grund: Die Job-Meldungen stehen laengst in Rippy (tasks.py schreibt sie selbst). Es fehlte, was DANEBEN passiert und den Worker unbrauchbar macht, ohne dass ein Job existiert - hochgefahren oder nicht, Verbindung zu Redis/Postgres, Abstuerze. Alles andere fliegt weg: Celery ist bei --loglevel=info gespraechig, die logs-Tabelle hat keine Aufraeumung, und ein zugemuelltes Log ist so unbrauchbar wie keins. Dazu eine Drossel (30 Zeilen/Minute), die MELDET, wieviel sie verschluckt hat. Zeilenformat woertlich aus dem laufenden Container abgenommen (Celery 5.4.0). Die lokale Datei bleibt - sie ist genau dann die einzige Auskunft, wenn Rippy nicht erreichbar ist. 2. DAS TRAY ZEIGT, WAS LAEUFT. Vorher stand dort "laeuft" oder "gestoppt" - auf einer Maschine, die stundenlang an einem Film rechnet, ist das keine Auskunft. Jetzt Titel, Prozent und Restzeit, geholt von Rippys /jobs. Bewusst dieselbe Quelle wie das Dashboard, damit im Tray nicht eine zweite, abweichende Schaetzung steht. Dazu: Windows schlaeft nicht mehr mitten im Encode ein (SetThreadExecutionState, ohne ES_DISPLAY_REQUIRED - der Bildschirm darf ausgehen). Die Sperre wird zurueckgenommen, sobald nichts laeuft, und auch bei einem harten Ende des Trays - sonst schlaeft der PC nie wieder ein und niemand weiss warum. 3. MEHRERE ENCODES GLEICHZEITIG. Der Worker lief fest mit --pool=solo und nahm genau EINEN Auftrag an. Der Installer fragt die Zahl jetzt (GUI: Feld neben dem Namen, mit der erkannten Kernzahl daneben), Vorbelegung ab 12 Kernen zwei, sonst einer: HandBrake nutzt schon alle Kerne, aber x265 skaliert nicht linear. Auf Windows gibt es keinen prefork-Pool (kein fork) - deshalb --pool=threads, was hier passt, weil die Arbeit ein Kind-Prozess ist und der Thread nur wartet. GUI-Layout headless gerendert und angesehen (nichts ueberlappt, 16 Kerne korrekt erkannt), beide .ps1 mit echtem PowerShell 5.1 geprueft, BOM und CRLF erhalten, .exe neu gebaut. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
be04a9772e |
fix(auswurf): CDROMEJECT meldete Erfolg und tat nichts - erst entriegeln
Ampel / ampel (push) Successful in 29s
Commander-Meldung: "Den Button gibt es in den Settings, aber es passiert nicht,
das Laufwerk geht nicht auf." Am laufenden System nachgestellt, mit der Disc, die
gerade drin lag:
wirf_disc_aus("/dev/sr0") -> True
CDROM_DRIVE_STATUS danach -> 4 (Disc drin)
Das ioctl wird also ANGENOMMEN und tut nichts. Ursache: MakeMKV verriegelt
waehrend des Rips die Laufwerkstuer (CDROM_LOCKDOOR 1) und entriegelt sie nicht
wieder. Ein verriegeltes Laufwerk quittiert den Auswurf trotzdem mit Erfolg.
Gegenprobe an derselben Disc:
CDROM_LOCKDOOR 0 + CDROMEJECT -> Status 2 (SCHUBLADE OFFEN)
Genau das macht das Werkzeug `eject` immer: erst entriegeln, dann auswerfen.
Zweite Haelfte des Fixes, und die wichtigere: Das Ergebnis wird GEPRUEFT statt
geglaubt. Bisher gab wirf_disc_aus True zurueck, sobald das ioctl nicht geworfen
hatte - und ins Log kam "Disc ausgeworfen", waehrend die Schublade zu blieb.
Deshalb ist der Fehler in v3.14 durchgerutscht: Dort wurde richtig festgestellt,
dass die Einstellung von niemandem gelesen wurde, und danach WURDE sie gelesen -
ausgeworfen wurde weiterhin nicht. Jetzt wird das Laufwerk gefragt (bis zu 5 s,
die Schublade braucht ein bis zwei), und "kein Datentraeger" zaehlt mit, weil ein
Slot-Laufwerk keine Schublade hat.
Dieselbe Luecke steckte im Auswurf-Knopf der API (devices.eject) - dort mit
Klartext-Fehler, wenn die Disc drin bleibt.
Der Auswurf sitzt uebrigens schon an der richtigen Stelle: nach dem Rip, VOR dem
Einreihen der Kompression. Und das Laufwerk ist waehrend `transcoding` frei -
has_active_job blockiert nur bei pending/running, der Worker laeuft mit 4 Slots.
Die zweite Disc parallel war also nur am nicht aufgehenden Laufwerk gescheitert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2587fe63af |
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> |
||
|
|
87537bf3e5 |
fix(mounts): Ergebnis pruefen statt glauben - "mount" meldet Erfolg und liefert nicht
Ampel / ampel (push) Successful in 28s
Nachtrag zum vorigen Commit, weil der die Freigabe noch nicht zurueckbrachte. Dreimal reproduziert: Beim API-Start meldete `mount` Rueckgabewert 0, das Log schrieb "rippy: eingehaengt", /proc/mounts zeigte GENAU EINE korrekt aussehende Schicht mit den richtigen Optionen - und `timeout 6 ls /app/media/rippy` lief trotzdem in die Zeitgrenze. Derselbe Ablauf ein zweites Mal, per POST /storage-mounts/rippy/repair, stellte sie sofort her (30 s, danach erreichbar). Der erste SMB-Sitzungsaufbau kurz nach dem Container-Start geht also gelegentlich schief, ohne es zu melden. Ein Rueckgabewert von `mount` beweist deshalb nichts. Jetzt: Nach dem Mount wird geprueft, ob die Freigabe ANTWORTET (ist_erreichbar, harte Grenze). Wenn nicht, einmal loesen und neu mounten. Hilft auch das nicht, fliegt ein Fehler mit Klartext - dann steht im Log "FEHLER" statt "eingehaengt", was schlicht die Wahrheit ist, und der Nutzer bekommt den Hinweis auf die Reparatur-Funktion statt eines Rips, der spaeter still scheitert. Damit ist die Kette geschlossen: os.path.isdir kann nicht mehr im Kernel haengen (voriger Commit), die Rohdaten-Schleife stirbt nicht mehr daran, und ein Mount gilt erst als hergestellt, wenn er antwortet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a15623cc70 |
fix(mounts): Freigabe kam nach einem Rebuild nicht zurueck - Erreichbarkeit zuerst
Ampel / ampel (push) Successful in 28s
Bestandsfehler, beim Gegenpruefen dieser Sitzung aufgefallen und dreimal
reproduziert: Nach `docker compose up -d --build` war die CIFS-Freigabe TOT
(4 von 4 Zugriffen liefen in 10 s Timeout, /storage-mounts meldete
`reachable: false`) - und blieb es. Erst POST /storage-mounts/rippy/repair
stellte sie in einer Sekunde her.
Beim API-Start haette dasselbe passieren muessen. Warum nicht:
if os.path.ismount(ziel):
return schreibtest(ziel)
Der Mountpunkt existiert im NEUEN Container weiter (Bind-Mount vom Host), also
sagte ismount "ist schon da" und alle_remounten() brach genau hier ab. Dazu
oeffnet schreibtest() eine Datei OHNE Zeitgrenze - auf einem toten CIFS
blockiert das im Kernel, und der Start-Thread haengt dauerhaft (das waren die
zwei Threads im Zustand D, die diese Sitzung schon einmal gekostet haben).
Jetzt entscheidet ist_erreichbar() zuerst - das hat eine harte Grenze
(`timeout 3 ls`, seit 24.07. im Einsatz). Antwortet die Freigabe, sind ismount
und schreibtest danach gefahrlos. Antwortet sie nicht, faellt es durch auf
loesen + frisch mounten: derselbe Weg, den reparieren() geht, nur automatisch.
Fuer den Commander heisst das: Der NAS-Mount steht nach einem Deploy wieder von
selbst, statt still zu fehlen. Ohne das war jeder Rip auf die NAS nach einem
Update kaputt, ohne dass irgendwo etwas davon zu sehen war.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8ccca90e3c |
fix(api): "konnte nicht nachsehen" ist nicht "ist weg"
Ampel / ampel (push) Successful in 28s
Gemessen nach dem letzten Deploy: Der ERSTE Zugriff auf die CIFS-Freigabe stallt
nach einem Container-Neustart mehrere Sekunden (die SMB-Sitzung wird neu
aufgebaut), danach antwortet sie in 0,01 s - zehn von zehn Versuchen, NAS per
Ping bei 0,47 ms. Die Freigabe ist also gesund, nur der erste Griff ist teuer.
In diesem Fenster lief die Pruefung in ihre Zeitgrenze, der Vorrat notierte
"keine Rohdaten", und der Knopf "Neu komprimieren" verschwand - obwohl 74 GB
dalagen. Der Nutzer haette daraus geschlossen, seine Daten seien weg. Genau
diese Sorte Fehlschluss ("aus einem Zustandswert auf einen Mechanismus") hat das
Projekt schon zweimal bezahlt.
Deshalb drei Antworten statt zwei: "da", "weg", "unklar". 124 ist der
Rueckgabewert von `timeout`, wenn es das Kind abgeschossen hat - das heisst
NICHT angesehen. Bleibt eine Pruefung unklar und der Vorrat hatte vorher einen
Treffer, wird die letzte bekannte Antwort gehalten statt Abwesenheit behauptet.
Wer eine Ja/Nein-Antwort braucht (verzeichnis_da), bekommt im Zweifel weiter
Nein - das ist die richtige Richtung fuer eine einzelne Abfrage.
Live davor geprueft: Die Schleife LEBT jetzt (alter_sekunden 30 -> 21 -> 12 -> 3
ueber 100 s) und heilt sich selbst - sobald die Freigabe antwortete, stand
mit_treffer=1 und can_retry=true.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
2554c2633b |
fix(api): os.path.isdir hing im Kernel und toetete die Schleife - harte Zeitgrenze
Ampel / ampel (push) Successful in 28s
Ursache gefunden, nicht geraten. Der neue Diagnose-Endpunkt sagte `rohdaten.alter_sekunden: null` - die Funktion war NIE EINMAL fertig geworden, und kein Fehler war gemeldet. Der Blick auf die Threads des API-Prozesses zeigte zwei im Zustand **D** (uninterruptible sleep, im Kernel blockiert): tid=1600034 name=uvicorn state=D tid=1600037 name=uvicorn state=D Der Mechanismus: Die Schleife startete ihren ersten Durchlauf, waehrend Rippy beim Container-Start die CIFS-Freigabe neu einhaengte. Ihr `os.path.isdir` blieb im Kernel stecken, `asyncio.to_thread` kam nie zurueck, die Schleife erreichte ihr `sleep` nie - und war damit fuer immer tot. Sichtbar war nur, dass can_retry dauerhaft false blieb. Ein Timeout um den Aufruf haette nichts geholfen: Ein im Kernel haengender Thread laesst sich aus Python nicht abbrechen, jeder Versuch haette einen weiteren Thread verbrannt, bis der Pool leer ist. Ein Kind-PROZESS laesst sich abbrechen. Geprueft wird jetzt mit `timeout 4 ls -d <pfad>` - dasselbe Werkzeug, das mounts.ist_erreichbar seit dem 24.07.2026 fuer genau dieses Problem benutzt (dort fuer den toten NAS-Mount). Laeuft es in die Zeitgrenze, gilt das Verzeichnis als "nicht da": Ein Ort, den man nicht in Sekunden ansehen kann, ist fuer einen Rip ohnehin unbrauchbar. os.listdir bleibt fuer /app/media selbst - das ist ein lokales Verzeichnis, die Freigaben sind Unterordner davon. Und os.path.isdir bleibt fuer den Rueckfall auf /app/temp/raw: ein Docker-Volume, dort kann nichts haengen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0d8426d353 |
diag(api): Vorrats-Schleifen pruefbar machen - der stille except hat gekostet
Ampel / ampel (push) Successful in 28s
Live-Befund: /jobs/<id>/rohdaten findet die 74,1 GB, aber can_retry blieb ueber 80 Sekunden false - der Vorrat der Hintergrund-Schleife war leer. Direkt im Container aufgerufen fuellt die Funktion ihn korrekt. Warum die Schleife im Server-Prozess nichts tat, war NICHT zu sehen: Der `except Exception: pass` verschluckte jeden Grund. Zwei Konsequenzen, unabhaengig von der Ursache: 1. Die Schleife MELDET ihren Fehler jetzt (print in den Container-Log) statt ihn zu verschlucken. Ein Hintergrund-Prozess, der still scheitert, ist schlimmer als einer, der laut scheitert. 2. Neuer Diagnose-Endpunkt GET /health/vorraete: nennt fuer Ping- und Rohdaten-Vorrat, wie ALT der letzte Durchlauf ist. Bleibt so ein Vorrat leer, ist am Endpunkt selbst naemlich nichts zu sehen - er antwortet nur dauerhaft "nichts gefunden". Die naheliegende Erklaerung (asyncio-Tasks ohne Referenz werden vom GC geholt) ist widerlegt: Die aeltere Ping-Schleife laeuft nach demselben Muster und lebt - ueber 72 Sekunden blieb jeder /capabilities-Aufruf unter 10 ms, waehrend ein toter Ping-Vorrat je 30 s einen synchronen Nachping von ~1 s gekostet haette. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9fbaed4af8 |
fix(api): Rohdaten-Suche in den Hintergrund - /jobs darf nie am NAS haengen
Ampel / ampel (push) Successful in 28s
Bei der Live-Gegenprobe gemessen: Waehrend der CIFS-Mount nach einem Container-Neustart hochkam, brauchten /jobs und /jobs/<id>/rohdaten jeweils 10,0 Sekunden - genau der CIFS-Timeout. Das Dashboard fragt /jobs alle vier Sekunden ab; ein schlafendes NAS haette es damit dauerhaft eingefroren. Dieselbe Loesung wie beim Celery-Ping in /capabilities (v3.15): eine Hintergrund-Schleife im 30-Sekunden-Takt sieht nach, wo Rohdaten liegen, und /jobs liest nur noch ab. Kennt der Vorrat einen Job noch nicht (frischer Fehlschlag), zaehlt der billige lokale Ort auf der Container-Platte - der antwortet immer sofort. Live geprueft, nachdem der Mount stand: /jobs 0,022 s, /rohdaten 0,014 s, can_retry = true, 79.604.951.639 Bytes (74,1 GB) gefunden. Der 80-GB-Rohschnitt des Commanders ist damit erstmals ueber das UI wieder erreichbar. Der leere Treffer davor war KEIN Codefehler: der Mount stand in dem Moment schlicht noch nicht. Beleg nachgeliefert - im Container gemessen findet die Suche den Pfad in 0,000 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
108368d583 |
fix(jobs): Rohdaten werden gesucht statt geraten - "Neu komprimieren" ging nicht
Ampel / ampel (push) Successful in 28s
Bei der Live-Gegenprobe des neuen /rohdaten-Endpunkts aufgefallen, und der Fund
ist groesser als der Endpunkt: Job 95afdc89 hatte `can_retry = false`, obwohl
79,6 GB intakter Rohschnitt unter /app/media/rippy/<id> lagen. Der Knopf
"Neu komprimieren" existierte gar nicht.
Der SAVEPOINT v3.16 schrieb dazu: "Rohschnitt 79,6 GB intakt -> 'Neu
komprimieren' genuegt, kein Neu-Rip." Das war falsch, und zwar doppelt:
_kann_neu_komprimieren suchte in /app/temp/raw/<id> und unter dem AKTUELLEN
workDir -> Knopf erschien nicht
retry_transcode berechnete raw_dir aus demselben aktuellen workDir
-> haette am falschen Ort gesucht
Ursache in beiden Faellen: Der Rip war mit einer Wahl NUR FUER DIESEN RIP auf
die NAS gelegt worden (gibt es seit v3.15), die Einstellung selbst stand auf
leer. Damit zeigte nichts mehr auf die Datei. Wieder derselbe Fehler, den dieses
Projekt schon mehrfach bezahlt hat: aus einem Zustandswert (der heutigen
Einstellung) auf einen Mechanismus (wohin damals gerippt wurde) geschlossen,
statt nachzusehen.
Neues Modul rohdaten.py sucht jetzt an allen Orten, die ueberhaupt in Frage
kommen: Container-Standard, eingestelltes Arbeitsverzeichnis und jedes
Ablageziel unter /app/media. Der Suchraum ist geschlossen, weil
_arbeitsverzeichnis() im Worker nur diese zulaesst; verwechseln kann man nichts,
weil Roh-Verzeichnisse exakt wie die Job-ID heissen (vollstaendige UUID) und
fertige Ablagen "Titel (Jahr) [kurz-id]".
Nicht in die Job-Zeile geschrieben, obwohl das sauberer waere: create_all legt
nur fehlende TABELLEN an, keine Spalten - und Bestandsjobs (genau dieser Fall)
haetten den Wert ohnehin nicht.
11 Tests, darunter der echte Fall, ein toter CIFS-Mount und der Klassiker
"/app/media-boese".
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
843c54cd1a |
fix(test): Erwartung korrigiert - ohne Quelle ist die Confidence 0,3, nicht 0,0
Ampel / ampel (push) Successful in 27s
Meine eigene Testannahme war falsch, nicht der Code: Antwortet keine Metadaten-Quelle, setzt _scan_video bewusst 0,3 mit `type: unknown` und behaelt den Disc-Titel. Der Rip laeuft dann trotzdem, die Datei heisst nur wie das Disc-Label. Genau dieser Zweig ist Bestand und richtig. Gefunden von der Ampel (Lauf 140) - lokal laeuft dieses Modul nicht, es braucht fcntl. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ee123f2a90 |
fix(install): Paketbefehl je Distribution statt stur apt
Ampel / ampel (push) Failing after 29s
Commander-Frage: "Was wenn der Unterbau nicht Debian ist?" Berechtigt - bei fehlendem Compose nannte install.sh stur `sudo apt install docker-compose-plugin`. Auf Arch, Fedora oder openSUSE ist das ein Befehl, den es nicht gibt. Erkannt wird ueber das VORHANDENE Paketwerkzeug (apt-get/dnf/yum/pacman/zypper/ apk), nicht ueber /etc/os-release: Ein Derivat kann sich anders nennen als sein Unterbau, sein Paketwerkzeug liegt aber im PATH. Findet sich keines, wird nichts behauptet. Auf der Rippy-VM gegengeprueft (Ubuntu 26.04 -> apt), unter Git Bash gegengeprueft (kein Treffer -> keine Behauptung). Dazu steht IMMER der Weg da, der auf jeder Distribution funktioniert: get.docker.com bringt das Compose-Plugin mit. Paketnamen wandern, dieses Skript nicht. README stellt jetzt oben klar, dass die Distribution gleichgueltig ist - Rippy bringt MakeMKV, HandBrake und abcde in seinen eigenen Containern mit, vom Host braucht es nur Docker und einen Kernel, der das Laufwerk sieht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0c269a97ee |
fix(metadaten): exakt schlaegt unscharf - Jikan beendet die Kette nur bei Treffer
Der SAVEPOINT v3.16 vermutete, das Aehnlichkeits-Gate 0,55 sei "grosszuegig" und Jikan stehe in der Kette zu frueh (vor OMDb). Nachgerechnet - titel_aehnlichkeit ist eine reine Funktion - ergibt sich ein anderes Bild als vermutet: "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 mit Confidence 0,85 - und OMDb, das ihn kennt, wurde nie gefragt. Bemerkenswert dabei, und das widerlegt die Vermutung "einfach zu niedrig": Fuer den Fall, fuer den Jikan ueberhaupt eingebaut wurde, ist das Gate sogar zu HOCH. "Evangelion 2.22" gegen den MAL-Titel "Evangelion: 2.0 You Can (Not) Advance" ergibt 0,51 und faellt durch. Eine einzelne Zahl kann beides nicht leisten. Deshalb zwei Schwellen statt Umsortieren (Reihenfolge bleibt, AGENTS Regel B): Nur ein praktisch exakter Titel (>= 0,9) beendet die Kette. Ein unscharfer Treffer wird GEMERKT, dann wird OMDb gefragt - und erst wenn OMDb nichts hat, kommt er als VORSCHLAG mit Confidence 0,6 zum Zug. Damit gewinnt "exakt" immer gegen "unscharf", egal aus welcher Quelle. Antwort auf die Commander-Frage "JIKAN ist drin - wird das genutzt?": ja, und ab jetzt an der richtigen Stelle. Ehrlicher Vorbehalt: Live gegengeprueft ist das nicht - MyAnimeList war waehrend dieser Sitzung durchweg weg (Jikan antwortete HTTP 504 auf jede Anfrage). Die Arithmetik des Gates ist davon unberuehrt und in test_jikan_helpers.py festgehalten, die Kettenlogik in test_prescan_helpers.py. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
6db64230eb |
fix(dashboard): zwei Placebos raus, Restzeit rein - und Loeschen sagt die Zahl
Commander: "Der Server Status muss dringend ueberarbeitet werden, das Dashboard soll ja quasi alles auf einen Blick zeigen." Berechtigt - die Karte hiess "Echte Live-Daten" und enthielt drei Angaben, von denen zwei erfunden waren: 1. "Auslastung: 0 % (Aktiv)" war NICHT die CPU-Last, sondern der Fortschritt des Jobs - bzw. eine feste 15 bzw. 5, wenn keiner lief. Rippy misst nirgends CPU-Last, also wird sie auch nicht behauptet. 2. Die Verlaufskurve daneben war Math.random(). Reine Dekoration, die wie eine Messung aussah. 3. "N Worker Online" zaehlte die registrierten Eintraege aus /system/info - auch Leichen alter Container-Rebuilds. Die echte Erreichbarkeit steht in /capabilities (Celery-Ping) und wird jetzt von dort geholt. Statt dessen: laufende Phase im Klartext mit RESTZEIT, jeder erreichbare Worker mit Kernen/Vektorbefehlen/extern, freier Platz mit Warnung, wenn er nicht mehr fuer eine Disc reicht. Und wenn kein Worker antwortet, steht das rot da statt "1 Worker Online". Restzeit auch im aktiven Rip-Banner und in der Job-Tabelle. Solange die Datenlage duenn ist, steht dort "Restzeit wird gemessen" - keine erfundene Zahl. Job entfernen fragt jetzt vorher nach den Rohdaten und nennt die GB, die daneben liegen bleiben und danach nicht mehr erreichbar sind - auf Wunsch loescht es sie mit. Der Fall aus v3.14, bei dem 75 GB unsichtbar verwaisten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
638edc6a9f |
feat(ui): Presets vom Worker, Warnung vor unerreichbaren Pfaden, Browser weg
Vier Commander-Punkte auf einmal, alle am selben Ort. Presets (Punkt 1 + 3b): Die drei Auswahllisten in Einstellungen kommen jetzt vom Worker statt aus dieser Datei, gefiltert auf die passende Aufloesung - die 4K-Liste bietet keine 1080p-Presets mehr als Normalfall an (genau dieser Griff rechnete in v3.12 eine 4K-UHD auf 1080p herunter; bewusstes Verkleinern steht jetzt in einer eigenen, benannten Gruppe). Knopf "Bestes waehlen" uebernimmt die Empfehlung fuer alle drei Disc-Typen, und unter jeder Liste steht, WARUM. Ein gespeicherter Wert, den der Worker nicht kennt, wird benannt statt verschluckt - so faellt eine falsche Bestandseinstellung ueberhaupt auf. Der Wizard entscheidet nicht mehr selbst, sondern nimmt dieselbe Empfehlung. Damit gibt es nur noch EINE Stelle mit dieser Logik, und die hat Tests. Warnung vor dem Start (Punkt 6 des Savepoints): Waehlt man einen externen Encoder, der Quelle oder Ziel nicht erreicht, steht das JETZT im Rip-Dialog - nicht erst nach einer Stunde Rip. Geprueft wird mit derselben Regel, die pfad_lokal() im Worker anwendet. Bei "Automatisch" wird genannt, welcher Worker die Aufgabe kaputtmachen koennte: die geteilte Queue nimmt den ersten freien. Datei-Browser (Punkt 7): "Warum wird hier der Datei Browser noch angezeigt - das ist doch quatsch." Er ist nicht wirklich redundant (nur ueber ihn geht ein freier Zielordner fuer EINEN Rip), aber er ueberschrieb die Schnellwahl stillschweigend. Jetzt eingeklappt - wer ihn aufklappt, entscheidet bewusst, und /browse wird erst dann geholt. Nebenbefund: browseFiles wurde geladen und NIE angezeigt, samt ungenutztem File-Icon - beides raus. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
aea26493db |
feat(installer): der Windows-Installer holt die Freigabe jetzt selbst von Rippy
DER BLOCKER aus dem SAVEPOINT v3.16 ist zu. Beide Installer (Kommandozeile und GUI) fragen GET /worker-setup/pfad-map, pruefen mit Test-Path, ob DIESER PC die Freigabe wirklich erreicht, und schreiben `set RIPPY_PATH_MAP=...` in start-tray.bat und start-worker.bat. Der Commander muss dafuer nichts ueber Container-Pfade wissen - das Feld bleibt leer, der Installer holt den Wert. Von Hand geht es trotzdem (Knopf "Von Rippy holen" bzw. -PfadMap), falls dieser PC die Freigabe anders erreicht. Ist nichts erreichbar, steht das als Klartext im Log samt dem haeufigsten Grund (fehlende Zugangsdaten - Freigabe einmal im Explorer oeffnen). Die Installation laeuft weiter: ein Worker, der sich meldet und ehrlich scheitert, ist besser als einer, der nicht existiert. Ein LEERES RIPPY_PATH_MAP wird bewusst nicht gesetzt - pfad_lokal() liest das als "kein Mapping", und die Fehlermeldung im Worker unterscheidet genau diese beiden Faelle. GUI: neues Feld samt Knopf, Fenster 560->648 px. Layout headless gerendert und angesehen (nichts ueberlappt, Umlaute korrekt), beide Dateien mit echtem PowerShell 5.1 auf Parser-Fehler geprueft, UTF-8-BOM erhalten. .exe neu gebaut. install.ps1 hatte durch ein Werkzeug LF statt CRLF bekommen - zurueckgedreht. remote-transcode-worker.yml richtiggestellt: Dort stand "mount -t nfs <rippy-host>:/srv/rippy" - das geht NICHT, auf der Rippy-Maschine laeuft kein NFS- und kein Samba-Server, sie ist selbst nur Client der NAS. Der Weg, der funktioniert: dieselbe Freigabe einhaengen, die auch Rippy nutzt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
8b7bdfe798 |
feat(api): drei Endpunkte, die das Raten beenden - plus Restzeit in /jobs
GET /worker-setup/pfad-map - was ein externer Worker fuer RIPPY_PATH_MAP
eintragen muss, abgeleitet aus Rippys eigenen Mounts. DAS war der stille
Blocker: /app/media/... sind Container-Pfade, ein externer Worker sieht sie nur
uebersetzt, und dieses Mapping setzte NIEMAND. Externes Encoden konnte deshalb
nie funktionieren, obwohl das Celery-Routing einwandfrei arbeitete (Job
95afdc89: angenommen, 182 ms spaeter abgelehnt). Hat Rippy keine Freigabe, sagt
der Endpunkt das im Klartext samt Abhilfe - statt ein leeres Mapping zu liefern.
GET /presets - Preset-Namen der Worker plus Empfehlung MIT Begruendung. Die
Namen standen bisher fest verdrahtet an vier Stellen im UI.
GET /jobs/{id}/rohdaten + DELETE /jobs/{id}?rohdaten=true - was liegen bleibt,
wenn ein Job aus der Liste fliegt. Grund (v3.14): Entfernen loescht bewusst
keine Dateien, aber Job und Rohdaten haengen nur an der Job-ID - der Rohschnitt
ist danach UNERREICHBAR. Damals verwaisten so 75 GB unsichtbar, gefunden erst
per SSH.
/jobs liefert jetzt eta_sekunden + eta_text. Die Schaetzung entsteht in der API
und nicht im Browser, aus drei Gruenden: ein Seitenwechsel setzte die Messreihe
zurueck, zwei offene Tabs zeigten verschiedene Zahlen, und fuer einen externen
Encoder-Worker gaebe es gar keine - genau dort wollte der Commander sie sehen.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
4195854bf8 |
feat(bausteine): Preset-Liste, Pfad-Vorschlag und Restzeit - alles gemessen
Vier neue reine Bausteine, jeder mit Tests. Sie beantworten Fragen, die Rippy bisher geraten oder gar nicht gestellt hat. 1. caps.parse_preset_liste - die Preset-NAMEN, die das HandBrake DIESES Workers wirklich kennt (`--preset-list`, Format im Worker-Image gemessen). Damit endet das Raten: die Namen unterscheiden sich je HandBrake-Version, und ein erfundener Name laesst die Kompression scheitern. Richtigstellung zum SAVEPOINT v3.16: Dort galt es als unmoeglich, die Hardware-Preset-Namen auf der Rippy-VM zu ermitteln, weil dort kein Hardware-Encoder laeuft. Gemessen ist das falsch - die Kategorie `Hardware/` steht vollstaendig in der Liste (VCN, NVENC, QSV, MF). HandBrake trennt zwei Fragen: --preset-list nennt alle Presets, --help nur die nutzbaren Encoder. Zwei Fragen, zwei Quellen. 2. mounts.pfad_map_vorschlag/pfad_map_zeile - der fehlende Anschluss fuer RIPPY_PATH_MAP. Geraten werden muss dafuer nichts: Rippy hat die Freigabe selbst eingehaengt und kennt ihre Quelle (//host/share). Mountpunkt plus Quelle IST das Mapping. NFS gibt bewusst "" - Windows-Schreibweise ist nicht ableitbar (AGENTS Regel D). Der Test fand dabei sofort die dokumentierte Windows-Falle: os.path.join baute `/app/media\rippy` in einen Container-Pfad, das Mapping waere still wirkungslos geblieben. _mountpoint nutzt jetzt posixpath. 3. presets.empfehlung - "immer das Beste" (Commander-Anforderung) ohne Punktesystem: fuer jede Lage eine feste Reihenfolge echter Namen, genommen wird der erste, den der Worker kennt. Hardware nur, wenn die Familie wirklich gemeldet ist (vce -> Preset heisst VCN, sonst liefe eine AMD-Karte unter dem Intel-Namen). vaapi und MF werden nie empfohlen: fuer vaapi gibt es kein Preset, bei MF ist die Nutzbarkeit nicht ablesbar. 4. eta - Restzeit aus dem gemessenen Fortschritt. Messreihe je Phase (Rip und Kompression haben nichts miteinander zu tun), Stillstand verlaengert die Schaetzung, und unter zwei Messwerten oder 60 Sekunden Spanne gibt es ehrlich keine Aussage. Test mit den echten 25.07.-Werten: 1,44 % in 29 min landet bei "noch ca. 1 Tag 23 h" - genau die Angabe, deren Fehlen den 50-Stunden-Lauf unsichtbar machte. Dazu: HandBrakes "Invalid preset <Name>" wird uebersetzt. Vorher stand im UI nur "HandBrake endete mit Code 3" - dass der Preset-NAME das Problem ist, war daraus nicht zu erraten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
a468e4b6a4 |
docs(savepoint): Commander-Feedback als Landkarte - neun Punkte, neun Staende
Ampel / ampel (push) Successful in 27s
Rueckfrage des Commanders: "Was ist mit diesen ganzen Sachen?" - er wollte wissen, ob seine Liste aus der ersten Selbst-Einrichtung vollstaendig erfasst ist. Nachgeprueft: alle neun Punkte kommen im v3.16-Block vor. Einer war aber nur IMPLIZIT zu finden: dass seine RX 9070 XT wirklich encodieren soll, musste man aus zwei anderen Punkten erschliessen (Pfad-Mapping und Preset-Namen). Genau so faellt etwas durch. Jetzt steht seine Liste woertlich als Tabelle im Savepoint, jede Zeile mit Stand und Verweis auf die Prioritaetenliste. Drei erledigt, eine Frage beantwortet, fuenf offen. Aufgeteilt wo noetig: Punkt 3 war eigentlich drei Forderungen - Vektorbefehle auslesen (erledigt), das Beste empfehlen (offen), und Warnung von orange auf gruen (erledigt, wirkt fuer seinen PC ab dem Deploy). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
4c78914529 |
docs(savepoint): v3.16 Uebergabe - externes Encoden konnte nie funktionieren
Ampel / ampel (push) Successful in 27s
Diese Sitzung endete wegen vollem Kontext. Der Savepoint ist so geschrieben, dass die naechste ohne Nachfragen weitermachen kann. ZUSTAND, den man kennen MUSS: Repo |
||
|
|
fd1feaaee3 |
fix(ui): die Ablage war im UI ueberhaupt nicht einstellbar
Ampel / ampel (push) Successful in 28s
Der Grund, warum der erste Rip mit externem Encoder scheitern MUSSTE - und Commander-Vorgabe: "Wenn ein externes Ziel eingehaengt ist, soll Rippy das ausgewaehlte als Arbeitsziel verwenden. Das stellt man ja sowieso in den Einstellungen ein. Das muss auch fuer den Automatik-Modus so sein." Genau das ging nicht. outputDir stand in den Einstellungen, wurde von der Vollautomatik (main.py) und der Schnellwahl (RipTargetModal) gelesen - aber es gab NIRGENDS ein Eingabefeld. Die Ablage klebte auf /app/media, der Container-Platte. Das Arbeitsverzeichnis hatte eine Auswahlliste der eingehaengten Ziele, das Ziel nicht. Folge im Praxistest: Rohdaten auf der NAS (die der PC erreicht), Ziel auf der VM-Platte (die er nicht erreicht, kein Samba dort). Der externe Encoder konnte lesen, aber nicht schreiben - und das haette man mit den vorhandenen Bedienelementen gar nicht anders einstellen koennen. JETZT: Ablage-Auswahl in Einstellungen -> Ripping, mit derselben Liste eingehaengter Ziele wie das Arbeitsverzeichnis. Damit wirkt sie automatisch auch in der Vollautomatik und in der Schnellwahl, weil beide outputDir lesen - also genau so, wie der Commander es beschrieben hat. DAZU die Pruefung, die den Vorfall verhindert haette: Sobald ein EXTERNER Worker gemeldet ist, warnt das UI, wenn Ablage und Arbeitsverzeichnis nicht beide auf einer erreichbaren Freigabe liegen. Drei Faelle, jeweils mit der Konsequenz im Klartext: - Arbeitsverzeichnis auf der Container-Platte -> externer Encoder kann nicht lesen - Arbeit auf Freigabe, Ablage lokal -> kann lesen, nicht schreiben (der Vorfall) - beide auf VERSCHIEDENEN Freigaben -> laeuft, kostet aber eine Vollkopie "Extern" ist dabei keine Heuristik ueber IPs oder Namen: caps.py meldet es selbst (`extern`), weil /app im Rippy-Image immer existiert und ausserhalb nie. Zusaetzlich meldet der Worker jetzt seine `pfad_map` - damit kann das UI im naechsten Schritt sagen, ob die Uebersetzung ueberhaupt gesetzt ist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
fe12401e34 |
fix(worker): Hardware-Encoder und CPU-Merkmale auf Windows - plus ehrliche Transcode-Fehler
Aus dem gescheiterten Test-Rip des Commanders (Job 95afdc89) und seiner
UI-Durchsicht. Der Reihe nach.
## 1. Der externe Encoder wurde SEHR WOHL gesehen (Diagnose-Korrektur)
Der Commander schloss aus der Meldung, das System sehe seinen PC nicht. Live
gemessen sagt das Gegenteil:
meta.transcode_node = tobisnicerpc@TobisNicerPC
VM-Worker-Log = nur rip_disc, KEIN transcode_files
22:19:41.271 Kompression eingereiht
22:19:41.453 "Keine Roh-MKVs gefunden" <- 182 ms spaeter
Das Routing hat funktioniert. Sein PC nahm die Aufgabe an und lehnte sie sofort
ab, weil er /app/media/rippy/<job> nicht finden kann - ein Pfad, der nur INNEN
im Container existiert. Der Rip war vollstaendig (79,6 GB auf der NAS).
## 2. Der Hardware-Encoder-Bug war meiner
leite_backends_ab() verlangte zusaetzlich ein Geraet: /dev/dri fuer
VAAPI/QSV/VCE bzw. nvidia-smi fuer NVENC. Unter WINDOWS gibt es beides nicht -
der PC des Commanders (RX 9070 XT) meldete deshalb nur CPU-Encoder, obwohl
HandBrakes Windows-Build vce_*/nvenc_*/qsv_* beherrscht. Der Bug traf genau den
Anwendungsfall, fuer den externe Worker gedacht sind.
Die Pruefung war ausserdem ueberfluessig: HandBrake probiert Hardware beim
Start selbst an und listet nur Nutzbares - auf der VM belegt durch
"qsv: not available on this system" bei gleichzeitig fehlendem qsv_* in --help.
Jetzt ist HandBrakes Liste die einzige Auskunft.
Dazu nach FAMILIE unterschieden (nvenc/qsv/vce/vaapi) statt alles in "vaapi" zu
werfen - eine AMD-Karte lief unter dem Intel-Namen. Hardware-AV1 bekommt eine
eigene Kennung, weil es die beste Kombination aus Tempo und Groesse ist und
sonst unsichtbar bliebe.
## 3. Vektorbefehle unter Windows - gemessen, nicht "unbekannt"
Es gibt dort kein /proc/cpuinfo, also meldete der Windows-Worker
"Vektorbefehle unbekannt" - gerade auf der Maschine, die encodieren soll.
Jetzt ueber IsProcessorFeaturePresent (kernel32, winnt.h dokumentiert) plus
CPU-Name aus der Registry. Auf dem Commander-PC gegengeprueft:
vorher: AMD64 Family 26 Model 68 Stepping 0 / Vektorbefehle unbekannt
jetzt: AMD Ryzen 7 9700X 8-Core Processor / avx512f / 16 Kerne
Damit verschwindet die AVX2-Warnung fuer diesen Worker von selbst - genau das
hatte der Commander gefordert.
## 4. Ehrliche Fehlermeldung statt "Keine Roh-MKVs gefunden"
Neues _erreichbarkeit_pruefen() unterscheidet jetzt drei Faelle statt einem:
Pfad nicht erreichbar UND kein Mapping gesetzt (nennt RIPPY_PATH_MAP samt
Beispiel), Mapping gesetzt aber greift nicht, oder Pfad einfach nicht da. Und
es prueft BEIDE Pfade - im Fall des Commanders lag die Quelle auf der
erreichbaren NAS, das ZIEL aber auf der VM-Platte ohne Samba; das waere erst
beim Schreiben nach Stunden Rechenzeit aufgefallen.
Jede Meldung sagt jetzt ausdruecklich, dass die Rohdaten NICHT verloren sind
und "Neu komprimieren" genuegt.
## 5. Persoenliche Werte raus
Vier Platzhalter trugen die Umgebung des Commanders (192.168.178.20,
TOBIS-PC, seine Jellyfin- und Rippy-IP). In einem Repo, das weitergegeben
wird, hat das nichts verloren.
Nebenbei dreimal in die eigene Falle getappt: deutsche Anfuehrungszeichen in
DOPPELT gequoteten Python-Strings beenden den String vorzeitig. Der Bestand
nutzt dafuer einfach gequotete f-Strings - jetzt auch meine.
NOCH OFFEN und wichtig: RIPPY_PATH_MAP wird von niemandem GESETZT (Installer,
UI, Compose). Ein Windows-Worker kann damit grundsaetzlich nicht transcodieren.
Das ist der naechste Schritt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
61c38a0d14 |
docs(readme): eigener Abschnitt fuer Dockge, Portainer, Arcane und Shell
Ampel / ampel (push) Successful in 28s
Commander-Plan fuer die Uebergabe: "Repo auf den Dockge-Server laden, Compose reinhauen, Profit?!" - und genau da sitzt die Falle, in die jeder zuerst tappt. DER KERN, der jetzt oben im Abschnitt steht: Rippy hat KEINE Registry-Images. api, worker und ui werden aus dem Repo gebaut (build: context: .). Die Compose-Datei allein ist deshalb wertlos - in ein leeres Verzeichnis kopiert gibt es sofort "failed to read dockerfile". Daraus folgt fuer JEDE Oberflaeche dieselbe Regel: das Repo muss dort liegen, wo die Oberflaeche den Stack baut. Je Werkzeug der konkrete Weg, weil sie sich genau darin unterscheiden: - Dockge kann das Repo NICHT selbst holen -> hinein in den Stacks-Ordner klonen (Standard /opt/stacks). Dann ist die Compose des Repos die des Stacks, und man darf den Stack gerade NICHT neu in Dockge anlegen - sonst liegt eine leere Compose in einem anderen Ordner. - Portainer KANN es selbst holen -> Stacks > Add stack > Repository mit Git-URL. Web-Editor und Upload funktionieren nicht (kein Build-Kontext). Geraeteknoten dort als Stack-Umgebungsvariablen statt .env. - Arcane: Repo auf dem Host, Deploy per Shell - der Git-Sync zieht nicht selbststaendig (auf dieser Installation seit Wochen die geuebte Praxis). - Shell: unveraendert git clone + sudo ./install.sh. Dazu die Vorab-Pruefung als gemeinsamer Einstieg: ./install.sh --nur-pruefen braucht kein root, aendert nichts und nennt vor allem die ECHTEN Geraeteknoten - der einzige Punkt, der zuverlaessig zuschlaegt. RICHTIGSTELLUNG an mir selbst: Ich hatte die Mount-Propagation als Huerde dargestellt. Auf den meisten systemd-Hosts ist / schon rshared und /srv/rippy/ media erbt das - da ist nichts zu tun. Nur wenn Docker ueber "not a shared mount" klagt, braucht es die Shell. Steht jetzt so drin. Stoerungstabelle von vier auf sechs Faelle: "failed to read dockerfile" und "Worker startet nicht, /dev/sgN fehlt" ergaenzt - die beiden, die ein Oberflaechen-Nutzer als erste sieht. Interne Verweise gegengeprueft, alle vier loesen auf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
29444805a8 |
fix(build): der MakeMKV-Fallback der VM war seit Monaten tot - Kette jetzt dreistufig
Ampel / ampel (push) Successful in 28s
Beim Aufraeumen der VM-.env aufgefallen und nachgemessen (25.07.2026):
https://www.makemkv.com/download HTTP 200 <- Repo-Standard
https://www.makemkv.com/download/old HTTP 525 (Cloudflare)
web.archive.org-Schnappschuss HTTP 404 <- stand in der VM-.env
Die VM zeigte also auf eine KAPUTTE Adresse. Aufgefallen ist es nie, weil dort
die vendor/-Tarballs liegen und der Download-Zweig gar nicht erreicht wird.
Nimm die Tarballs weg, und jeder Bau scheitert mit 404 - waehrend die
Konfiguration gesund aussieht. Genau diese Sorte Fehler ist bei einer Uebergabe
teuer, weil sie erst beim Fremden zuschlaegt.
## Bereinigt (auf Commander-Wunsch, Sicherung liegt auf der VM)
.env.sicherung-vor-aufraeumen-20260725 angelegt, dann:
- MAKEMKV_URL_BASE raus -> es gilt der Compose-Standard, der liefert.
Von Docker gegengeprueft: MAKEMKV_URL_BASE: https://www.makemkv.com/download
- JWT_SECRET_KEY raus -> Ueberrest der in v3.4 ausgebauten Anmeldung, wirkungslos.
## Fallbacks BEHALTEN, aber echt gemacht (Commander-Vorgabe)
Vorher war der "Fallback" eine Zeile, die man von Hand eintragen musste - und
die hier ins Leere zeigte. Jetzt eine automatische Kette:
1. vendor/-Tarballs (braucht kein Netz, zuverlaessigster Weg)
2. MAKEMKV_URL_BASE
3. MAKEMKV_URL_FALLBACK (NEU, wird automatisch versucht wenn 2 versagt)
Stufe 3 ist ABSICHTLICH leer vorbelegt. Es gibt derzeit keine belegbare zweite
Quelle (siehe Messung oben), und eine einzutragen, die nicht liefert, waere
schlimmer als keine - genau das lag auf der VM vor. Wer eine eigene Quelle hat
(Spiegel im LAN), traegt sie ein und sie wird automatisch genutzt.
Scheitert alles, nennt die Fehlermeldung jetzt die beiden Wege, die
FUNKTIONIEREN, statt nur einen curl-Rueckgabewert zu hinterlassen.
## Geprueft, beide Zweige
- vendor-Pfad: Bau durch, 828 MB
- Download-Pfad: im FREMDEN Klon ohne Tarballs gebaut - Bau durch, 777 MB,
makemkvcon liegt unter /usr/local/bin/. Die leere Fallback-Stufe wird
korrekt uebersprungen, "Erste Quelle lieferte nicht" erschien nicht.
docker compose config validiert. README und .env.example nennen die gemessenen
HTTP-Antworten, damit niemand wieder eine 404-Adresse als Fallback eintraegt.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b0205c42c9 |
refactor(install): erst ALLES pruefen, dann erst aendern
Ampel / ampel (push) Successful in 28s
Commander-Einwand: "Warum macht das install script den pre-check nicht selbst
vor der Installation?" Zu Recht - es tat es nur halb.
VORHER war Pruefen und Aendern verschraenkt:
1 Voraussetzungen pruefen (bricht ab)
2 Laufwerk pruefen (warnt)
3 Verzeichnisse AENDERT
4 Mount AENDERT
5 .env AENDERT
6 Bauen AENDERT
Nur die harten Voraussetzungen liefen vorab. Verzeichnisse wurden angelegt,
bevor klar war, ob der Rest durchlaeuft - scheiterte etwas in der Mitte, blieb
ein halb eingerichteter Rechner zurueck. Und --nur-pruefen war als if-Zweig
durch FUENF Schritte gefaedelt: so etwas laeuft zwangslaeufig aus dem Ruder,
weil jede neue Aktion daran denken muesste.
JETZT zwei klar getrennte Phasen:
Phase 1 PRUEFEN - fuenf Pruefungen, aendert NICHTS
Weiche - Probleme -> Abbruch mit Liste, nichts angefasst
Phase 2 EINRICHTEN - Verzeichnisse, Mount, .env, bauen, starten
Es gibt damit GENAU EINEN Pruef-Pfad, den beide Modi benutzen: --nur-pruefen
heisst schlicht "nach Phase 1 aufhoeren". Die Pruefung kann nicht mehr etwas
anderes behaupten als die Installation tut.
Die Weiche unterscheidet PROBLEME (verhindern die Installation) von HINWEISEN
(halten nicht auf) und fasst am Ende beides als Liste zusammen - vorher musste
man die Ausgabe rueckwaerts lesen, um zu wissen, ob es gereicht hat.
Neu mitgeprueft, weil es jetzt an einer Stelle passt:
- Ist der Elternpfad der Verzeichnisse ueberhaupt beschreibbar? (Vorher waere
das erst beim mkdir aufgefallen - mitten in der Aenderungsphase.)
- Freier Plattenplatz mit Schwelle 60 GB, samt Hinweis auf die Groessen
(Blu-ray roh ~40 GB, 4K-UHD bis 100 GB).
- Ist ueberhaupt .env.example da, also stehen wir im Rippy-Repo?
- Was in die .env geschrieben WUERDE, steht jetzt in Phase 1 als Ansage.
Auf der VM gegengeprueft: aus /tmp gestartet meldet es korrekt "kein
Rippy-Repo" und bricht ab, ohne etwas anzufassen; aus dem Repo heraus laeuft
die Pruefung sauber durch ("Alles in Ordnung, keine Einschraenkungen").
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
35cfcbcb07 |
style: echte Umlaute im ganzen Projekt + Installer im Rippy-Look
Ampel / ampel (push) Successful in 27s
Commander-Rueckmeldung: Umlaute fehlen. Ausloeser war sichtbar der Installer -
"Diese Maschine uebernimmt die Video-Kompression fuer Rippy" stand woertlich im
Screenshot.
## Die .ps1-Falle war loesbar, nicht unumgehbar
Bisher galt: ausgelieferte .ps1 MUESSEN ASCII sein, weil PowerShell 5.1 sie
ohne BOM als ANSI liest. Das ist nur die halbe Wahrheit - gemessen mit echtem
powershell.exe 5.1:
ohne BOM: $s = "Größe: äöü" + Unerwartetes Token -> Skript kaputt
mit BOM: Groesse: aeoeue + laeuft, Length 16 korrekt
Alle drei Skripte sind jetzt UTF-8 MIT BOM und tragen echte Umlaute; unter 5.1
gegengeprueft (BOM vorhanden, Parser fehlerfrei, Text korrekt gelesen). Der
Kopfkommentar sagt das jetzt richtig statt "ASCII-only".
## Umstellung: Text ja, Bezeichner nein
Umlaute gehoeren in Kommentare und Anzeigetexte, nicht in Funktionsnamen oder
Datenschluessel. Deshalb je Sprache das passende Werkzeug:
- Python: ueber den TOKENIZER - angefasst wurden ausschliesslich COMMENT- und
STRING-Tokens. 156 Stellen. Code ist damit garantiert unberuehrt.
- TypeScript: nur // und /* */ Kommentare sowie JSX-Text (kann per Definition
kein Bezeichner sein). 24 Stellen. `const waehlen`, `let laeuft`, `plaetze`,
`GeraetInfo` sind nachweislich unversehrt.
- install.sh: Anzeigetext, aber die Shell-Funktionen (gruen/rot/gelb/titel) und
der Schalter --nur-pruefen bleiben ASCII - das sind Schnittstellen.
- Markdown: 0 Aenderungen, die Doku hatte schon Umlaute.
ZWEI FEHLER MEINES KONVERTERS, beide von Werkzeugen gefangen:
1. In f-Strings steht in {...} CODE, kein Text. Aus f"{groesse}" wurde
f"{größe}", waehrend die Variable groesse hiess - Ruff meldete F821
"Undefined name". Der Konverter lagert Einsetzungen jetzt aus.
2. Ein Dict-Schluessel wurde umbenannt: die Wortliste enthaelt das PRAEFIX
"uebersprung", der Tabu-Schutz prueft aber ganze Woerter. bericht[...] ist
wieder ASCII - Umlaute in Datenschluesseln brechen JSON-Runden und DB-Felder.
## Installer im Rippy-Look
Statt hellgrau jetzt dieselben Toene wie das Web-UI (aus lib/design.ts
uebernommen): slate-900 Flaeche, dunkle Eingabefelder, Amber-Hauptknopf wie
"Los geht's" im Wizard. Oben ein Kopfbereich mit dem Farbverlauf
amber -> indigo -> purple und dem Disc-Symbol - in WinForms per Paint-Ereignis
gezeichnet, weil es dort keine Verlaeufe von der Stange gibt.
Geprueft, nicht gehofft: Das Fenster wurde headless in ein PNG gerendert
(DrawToBitmap) und angesehen - Verlauf, Symbol, Umlaute und Farben sitzen.
RippyWorkerSetup.exe neu gebaut (57344 -> 60416 Bytes); Umlaute und das
requireAdministrator-Manifest sind in der .exe verifiziert.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
65edf8369e |
feat(worker-windows): Installation nach "Programme", Zielordner frei waehlbar
Ampel / ampel (push) Successful in 28s
Commander-Wunsch: nicht ins Benutzerprofil, sondern wie jedes andere Programm
nach C:\Program Files - und selbst waehlbar, mit diesem Standard.
## Was neu ist
- Feld "Installieren nach" mit Knopf "Aendern ..." (Ordner-Dialog). Standard
ist [Environment]::GetFolderPath("ProgramFiles") + "Rippy Worker" - der
ECHTE Pfad; deutsche Windows-Versionen zeigen im Explorer "Programme", der
Pfad heisst trotzdem "Program Files".
- Der Ordner-Dialog liefert den ELTERN-Ordner, "Rippy Worker" haengt der
Installer selbst an. Sonst koennte die Deinstallation einen fremden Ordner
mitloeschen - sie macht Remove-Item -Recurse auf das Zielverzeichnis.
- install.ps1 bekommt -InstallDir mit demselben Standard.
## Drei Folgen, die mitbehandelt werden mussten
1. RECHTE. In C:\Program Files darf nur ein Administrator schreiben. Die .exe
traegt jetzt ein requireAdministrator-Manifest (-requireAdmin in
build-exe.ps1), fragt also beim Doppelklick EINMAL per UAC - danach stimmen
die Rechte fuer alles Weitere. Verifiziert: "requireAdministrator" steckt im
Manifest, "asInvoker" nicht mehr. Zusaetzlich prueft der Installer VOR dem
Entpacken, ob er dort schreiben darf, und nennt bei Nein die zwei Wege
(als Administrator starten ODER Ziel ins eigene Profil legen) - statt
irgendwo tief "Zugriff verweigert" zu produzieren.
2. DAS LOG. tray.py schrieb worker.log NEBEN das Programm. Unter Program Files
darf ein normaler Benutzer das nicht - der Worker waere beim Starten
gescheitert, und zwar still. Das Log liegt jetzt in
%LOCALAPPDATA%\Rippy Worker\worker.log, also da, wo veraenderliche Daten
hingehoeren. Rueckfall auf das Programmverzeichnis bleibt fuer
Installationen ins eigene Profil.
3. DER AUTOSTART. Bei einer Installation unter Programme ist es eine
Installation FUER DIE MASCHINE - der Autostart geht deshalb in den
Autostart-Ordner aller Benutzer (CommonStartup). Das loest zugleich ein
Rechte-Problem: erhoeht man ueber ein FREMDES Administratorkonto, waere
GetFolderPath("Startup") der Ordner dieses Admins, also der falsche. Bei
einem Ziel im eigenen Profil bleibt es persoenlich. uninstall.ps1 raeumt
beide Orte ab.
## Geprueft
- Leerzeichen im Pfad: die erzeugte .bat mit cd /d "%~dp0", PATH-Erweiterung
und relativem Aufruf laeuft aus "C:\...\Rippy Test Ordner" korrekt durch.
- Alle drei .ps1 ASCII-rein und fehlerfrei geparst.
- RippyWorkerSetup.exe neu gebaut (52736 -> 57344 Bytes, Version 1.0.1.0).
UI: Der Copy-Paste-Befehl fuer den CLI-Weg sagt jetzt, dass PowerShell als
Administrator laufen muss, und nennt -InstallDir als Ausweg ohne Adminrechte.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
8f208ed0da |
fix(worker-windows): Autostart brach die ganze Installation ab
Ampel / ampel (push) Successful in 28s
Beim Commander live aufgetreten: Der Installer meldete
"FEHLER: FEHLER: Zugriff verweigert" samt dem Tipp, die IP koenne falsch sein -
und das, NACHDEM Worker-Code, Abhaengigkeiten und HandBrake schon fertig waren.
Die IP war also nie das Problem.
URSACHE: schtasks /create mit /tn "RippyWorker" legt die Aufgabe im
WURZELORDNER der Aufgabenplanung an, und das verlangt Administratorrechte. Der
Installer laeuft normal ohne. schtasks schrieb "FEHLER: Zugriff verweigert."
nach stderr, und weil im Skript $ErrorActionPreference = "Stop" steht, machte
PowerShell daraus einen TERMINIERENDEN Fehler - der catch-Block riss damit die
komplette Installation ab, obwohl nur der Autostart fehlte. Das doppelte
"FEHLER: FEHLER:" war der Hinweis: der aeussere Handler setzt sein Praefix vor
eine Meldung, die selbst schon mit "FEHLER:" begann, also aus schtasks kam.
FIX, zwei Teile:
1. Autostart ueber den Autostart-ORDNER statt schtasks. Eine Verknuepfung in
[Environment]::GetFolderPath("Startup") braucht NIE Adminrechte - und der
Nutzer kann sie dort selbst sehen und loeschen. Auf diesem Windows-PC
gegengeprueft: Verknuepfung angelegt (1047 Bytes) mit Admin=False.
2. Ein Fehlschlag beim Autostart ist jetzt eine WARNUNG in eigenem try/catch,
kein Abbruch. Der Worker ist zu dem Zeitpunkt fertig und startbar, und der
Text sagt das auch - plus den Handweg (shell:startup).
In BEIDEN Installern (install-gui.ps1 und install.ps1, dort mit -Autostart) -
der CLI-Weg hatte denselben Fehler. uninstall.ps1 raeumt jetzt die
Verknuepfung weg UND versucht weiter schtasks /delete, damit aeltere
Installationen sauber verschwinden.
RippyWorkerSetup.exe neu gebaut (50688 -> 52736 Bytes): die GUI ist in der .exe
eingebettet, ohne Rebuild wirkt der Fix beim Nutzer nicht.
Nebenbei: build-exe.ps1 hatte zwei Gedankenstriche. Ausgelieferte .ps1 muessen
reines ASCII sein (PowerShell 5.1 liest sie als ANSI) - jetzt sind alle drei
Skripte ASCII-rein und parsen fehlerfrei.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
b76ca5d9be |
docs(savepoint): install.sh und README-Umbau nachgetragen
Ampel / ampel (push) Successful in 28s
Der Block dokumentiert jetzt auch die letzte Runde: ein Befehl statt Checkliste, README neu aufgebaut, VM-CPU-Anleitung drin. Ehrlich als UNGETESTET markiert (neuer Punkt 6): install.sh ist nie als root durchgelaufen. Auf dieser VM sind alle root-Schritte No-Ops - Verzeichnisse liegen da, die Propagation ist schon shared - und sudo verlangt hier ein Passwort. Bewiesen sind Laufwerkserkennung (SCSI-Abgleich gegen die echte Hardware), die .env-Logik in drei Faellen und die Syntax. Das Anlegen der Verzeichnisse, mount --make-rshared und die systemd-Unit laufen erst bei einer echten Neuinstallation - das gehoert in die Uebergabe statt in eine Erfolgsmeldung. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ca91e5b693 |
feat(install): ein Befehl statt Checkliste - install.sh nimmt die Handarbeit ab
Ampel / ampel (push) Successful in 28s
Commander-Rueckmeldung: "Das Docker Deployment ist mir zu kompliziert." Zu
Recht - die Installation war eine Checkliste aus sechs Schritten, von denen
zwei nur mit Fachwissen zu schaffen waren.
## Neu: sudo ./install.sh
Nimmt genau die Schritte ab, an denen man scheitern konnte:
1. Prueft Voraussetzungen (Linux, Docker, Compose) und nennt bei jedem Mangel
den Installationsbefehl.
2. FINDET DAS LAUFWERK SELBST. Das war der schlimmste Punkt: MakeMKV braucht
ZWEI Geraeteknoten, und die sg-Nummer ist je Host anders. Das Skript gleicht
sie ueber die SCSI-Adresse in /sys ab statt zu raten. Auf der VM
gegengeprueft: sr0 -> 3:0:0:0, sg1 -> 3:0:0:0, also dasselbe Geraet -
korrekt als /dev/sr0 + /dev/sg1 erkannt.
3. Legt Ablage- und MakeMKV-Verzeichnis an.
4. Richtet die Mount-Propagation ein UND macht sie per systemd-.mount-Unit
neustart-fest. Vorher stand in der README nur "reboot-fest persistieren" -
ohne zu sagen wie; nach einem Reboot scheiterte das Einhaengen von
NAS-Freigaben aus dem UI stillschweigend.
5. Schreibt die .env, ueberschreibt aber NIE einen bestehenden Wert.
6. Baut und startet, und nennt bei Fehlschlag die drei haeufigsten Ursachen
mit Diagnosebefehl.
Wiederholbar (mehrfach ausfuehren aendert nichts kaputt) und damit auch der
Update-Weg: git pull && sudo ./install.sh
## Zwei Fallgruben, die beim Testen auffielen - beide meine eigenen
- --nur-pruefen verlangte root und brach ab. Ein Pruef-Modus, der nichts
aendert, darf daran nicht scheitern - sonst kann man vor der Installation
nicht nachsehen, ob alles passt. Behoben.
- .env.example hatte OPTICAL_SG=/dev/sg1 UNKOMMENTIERT vorbelegt. Damit haette
der Installer den erkannten Wert nicht eingetragen ("steht schon drin") und
auf jedem fremden Host still eine kaputte Konfiguration hinterlassen - genau
das, was er verhindern soll. Beide Geraetezeilen sind jetzt auskommentiert;
Compose hat ohnehin Vorgaben. Dazu eine Gegenprobe im Skript: zeigt ein
wirksamer Wert auf ein Geraet, das es hier nicht gibt (".env von einem
anderen Rechner"), wird das benannt statt kryptisch von Docker gemeldet.
## README neu aufgebaut
Vorher 293 Zeilen, in denen der Schnellstart zwischen lsscsi, sg-Knoten,
USB-Passthrough und mount --make-rshared begraben war. Jetzt: Installation in
zwei Zeilen oben, dann eine Tabelle "Wenn etwas nicht geht" mit den vier
Faellen, die praktisch alles abdecken. Alles Technische steht darunter in
aufklappbaren Abschnitten - inklusive der Handarbeits-Variante fuer die, die
sie wollen.
NEU und ausdruecklich gewuenscht: Abschnitt "Rippy schneller machen" mit dem
VM-CPU-Typ. Erklaert, warum Virtualisierer eine generische CPU ohne AVX2
geben, was das kostet (gemessene 28-55 h je 4K-Film), die genauen Schritte in
Proxmox (herunterfahren - Hardware/Processors/Type auf 'host' - starten, bzw.
qm set <vmid> --cpu host), warum ein Neustart von innen NICHT genuegt, und wie
man nachprueft: Rippy zeigt die Vektorbefehle seit v3.14 selbst an. Dazu der
Nachteil (keine Live-Migration auf andere CPUs) und die Alternative x86-64-v3
fuer Cluster.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|