Commit Graph

231 Commits

Author SHA1 Message Date
Hitonabi d15f4b2ab6 feat: Thumbnails, bunte Logs und Button-Status im Worker UI 2026-07-27 16:17:30 +02:00
Hitonabi a221a161f4 feat: Tray-Icon fuer Worker GUI (pystray) — Fenster schliessen minimiert ins Tray 2026-07-26 22:33:54 +02:00
Hitonabi 264b2c77f0 chore: finale exe mit venv --clear fix neu gebaut 2026-07-26 22:32:12 +02:00
Hitonabi 16ea54e35d fix: venv --clear bei Reinstall, verhindert veraltete Paketversionen 2026-07-26 22:30:12 +02:00
Hitonabi 81ba6bd1c6 fix: don't override pinned flet==0.23.2 with unpinned pip install 2026-07-26 22:24:20 +02:00
Hitonabi 06c1718a52 fix: installer buttons (Durchsuchen/Freigabe holen), Worker-Start via os.startfile, Shortcut via PowerShell 2026-07-26 22:15:50 +02:00
Hitonabi de500f9890 feat: complete rewrite of worker installer in Flet (standalone exe) 2026-07-26 22:07:24 +02:00
Hitonabi 9a151f3d8b feat: Worker V2 Flet GUI mit Feature-Parität und neuem Installer 2026-07-26 22:01:27 +02:00
Hitonabi 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
2026-07-26 21:13:27 +02:00
Hitonabi d15900d1eb fix(audio): nur die beste Tonspur pro Sprache verlustfrei kopieren
Ampel / ampel (push) Failing after 29s
2026-07-26 20:55:57 +02:00
Hitonabi 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>
2026-07-26 18:52:12 +02:00
Hitonabi 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>
2026-07-26 17:47:27 +02:00
Hitonabi 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>
2026-07-26 17:43:51 +02:00
Hitonabi 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>
2026-07-26 17:23:14 +02:00
Hitonabi 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>
2026-07-26 17:09:23 +02:00
Hitonabi 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>
2026-07-26 17:06:56 +02:00
Hitonabi 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>
2026-07-26 16:57:12 +02:00
Hitonabi 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>
2026-07-26 16:49:10 +02:00
Hitonabi 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>
2026-07-26 16:23:15 +02:00
Hitonabi 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>
2026-07-26 16:16:05 +02:00
Hitonabi 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>
2026-07-26 16:05:54 +02:00
Hitonabi 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>
2026-07-26 15:55:05 +02:00
Hitonabi 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>
2026-07-26 15:46:15 +02:00
Hitonabi 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>
2026-07-26 15:38:31 +02:00
Hitonabi 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>
2026-07-26 15:25:46 +02:00
Hitonabi 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>
2026-07-26 15:22:13 +02:00
Hitonabi 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>
2026-07-26 15:18:30 +02:00
Hitonabi 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>
2026-07-26 15:14:52 +02:00
Hitonabi 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>
2026-07-26 15:10:21 +02:00
Hitonabi 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>
2026-07-26 15:01:09 +02:00
Hitonabi 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>
2026-07-26 14:52:53 +02:00
Hitonabi 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>
2026-07-26 14:40:31 +02:00
Hitonabi 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>
2026-07-26 14:32:51 +02:00
Hitonabi 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>
2026-07-26 14:24:09 +02:00
Hitonabi 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>
2026-07-26 14:17:14 +02:00
Hitonabi 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>
2026-07-26 14:03:10 +02:00
Hitonabi 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>
2026-07-26 13:55:41 +02:00
Hitonabi 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>
2026-07-26 13:46:37 +02:00
Hitonabi 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>
2026-07-26 13:40:58 +02:00
Hitonabi 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>
2026-07-26 13:34:39 +02:00
Hitonabi 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>
2026-07-26 13:28:02 +02:00
Hitonabi 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>
2026-07-26 13:23:32 +02:00
Hitonabi 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>
2026-07-26 13:15:42 +02:00
Hitonabi 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>
2026-07-26 13:09:44 +02:00
Hitonabi 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>
2026-07-26 13:01:34 +02:00
Hitonabi 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>
2026-07-26 12:59:42 +02:00
Hitonabi 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>
2026-07-26 12:59:26 +02:00
Hitonabi 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>
2026-07-26 12:59:08 +02:00
Hitonabi 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>
2026-07-26 12:58:53 +02:00
Hitonabi 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>
2026-07-26 12:58:23 +02:00