Commit Graph

213 Commits

Author SHA1 Message Date
Hitonabi a1aabd591d fix(test): Ampel rot - der Backslash-Test prueft nichts
Ampel / ampel (push) Successful in 28s
Meine eigene Zeile aus dem vorigen Commit. Im Quelltext stand "a\b.mkv" mit
EINEM Backslash - Python liest \b als Backspace-Zeichen, der String enthaelt
also gar keinen Backslash, und _sicherer_dateiname gab korrekt True zurueck.
Der Test behauptete, den Backslash-Pfad zu pruefen, und tat es nicht.

Jetzt ein Raw-String r"a\b.mkv".

Warum das lokal nicht auffiel: test_api_smoke.py ueberspringt sich unter
Windows selbst (main.py -> detection.py -> fcntl). Genau die Luecke, die im
Savepoint als offener Punkt steht - hier hat sie sofort zugeschlagen. Lehre:
Tests, die nur in der Ampel laufen, sind erst nach dem Push bewiesen, und
Backslashes gehoeren in Raw-Strings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:12:39 +02:00
Hitonabi 8394de6926 fix(transcode): "Abbrechen" wirkt sofort statt erst beim naechsten Prozent
Ampel / ampel (push) Failing after 28s
Fund des Commanders am laufenden Akira-Job (Nachtrag im vorigen Savepoint):
Job stand auf 'canceling', HandBrake lief weiter. Gemessen 3,4 Minuten
zwischen Anforderung (18:30:17) und Bestaetigung (18:33:38) - bei langsamerem
Fortschritt entsprechend mehr.

Ursache genau wie dort beschrieben: Der Abbruch wurde nur in
datei_fortschritt geprueft, und diese Closure stieg oben sofort wieder aus,
wenn sich die Prozentzahl nicht geaendert hatte ("if gesamt == letzter[0]:
return"). Bei einem Prozent je halber Stunde hing der Abbruch also an einem
Ereignis, das eine halbe Stunde lang nicht eintrat.

run_handbrake bekommt jetzt einen eigenen abbruch_cb neben progress_cb -
dasselbe Muster, das run_makemkv schon fuer log_cb benutzt, und aus
demselben Grund: der Fortschritts-Kanal verwirft Aufrufe. Er wird bei JEDER
Ausgabezeile aufgerufen und im Worker auf 5 s gedrosselt.

Nebeneffekt, der genauso wichtig ist: Er greift auch waehrend des
Scan-Durchlaufs, der ueberhaupt keine Encode-Prozente liefert. Dort war ein
Abbruch vorher grundsaetzlich unmoeglich - und seit die Prozent-Regex den
Scan korrekt ignoriert, waere das sonst sogar schlimmer geworden.

Die Leseschleife ist als _handbrake_schleife() herausgezogen, damit die
Reihenfolge (Abbruch VOR Fortschritt) ohne echtes HandBrake pruefbar ist.
Der Test belegt: zwei Scan-Zeilen genuegen fuer den Abbruch, es muss NICHT
auf eine Encode-Zeile gewartet werden.

Savepoint nachgezogen: Der Encode laeuft nicht mehr, ein Deploy ist
gefahrlos moeglich, und die alte "SOFORT ENTSCHEIDEN"-Passage (Platte
laeuft voll, wenn der Job fertig wird) ist damit gegenstandslos. Neu darin:
die drei Wege fuer UHD, und die Warnung, dass eine /proc-Suche nach
"HandBrake" die eigene Shell mittrifft - zwei Fehlalarme in dieser Sitzung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:10:03 +02:00
Hitonabi 574354131c docs(savepoint): v3.14 - Durchsicht, mit den zwei eigenen Fehlschluessen
SAVEPOINT bekommt den Stand v3.14: der unwirksame Platten-Schutz zuerst
(wichtigster Fund), dann die gemessenen Werte, das Gebaute, die vier toten
Endpunkte als Entscheidungsvorlage - und ein Abschnitt "SOFORT ENTSCHEIDEN".

Der laufende Akira-Encode wird beim Fertigwerden mit dem DEPLOYTEN, alten
Code versuchen, 75 GB in 37 GB zu kopieren. keepOriginal steht auf true, und
den Wert hat die Aufgabe beim Start gelesen - jetzt umstellen aendert daran
nichts mehr. Der Savepoint nennt die drei Optionen mit Empfehlung.

Zwei Richtigstellungen an v3.13, beide durch Messung:
- "progress=99 ist ein Altwert aus dem Absturz" war falsch. Beide Startpfade
  setzen auf 0; der Wert kam frisch vom Scan-Durchlauf. Ein Bug, kein Ueberrest.
- Die Erwartung, _original_aufheben werde nur warnen, war falsch - siehe
  EXDEV-Fund im vorigen Commit.

Ehrlich als OFFEN markiert: Was die Platte am 25.07. mittags fuellte, ist
NICHT belegt. Es gibt keine Kopier-Reste, kein original-Verzeichnis und
keinen einzigen Log-Eintrag von _original_aufheben, das in beiden Zweigen
loggt. Der Mechanismus ist jetzt bewiesen, sein Zuschlagen an jenem Tag
nicht. Lieber offen lassen als eine dritte Vermutung aufstellen.

AGENTS bekommt Etappe 19 und einen neuen Abschnitt, der das wiederkehrende
Muster benennt: nicht aus einem Zustandswert auf einen Mechanismus
schliessen - vier belegte Faelle. Plus die Umkehrung, die diese Sitzung
gekostet hat: gleiches st_dev heisst NICHT gleicher Mount. Was ausprobierbar
ist, wird ausprobiert statt vorhergesagt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:05:38 +02:00
Hitonabi ef0a574a70 fix(worker,api): Platten-Schutz griff nicht, Fortschritt log, Auswurf tat nichts
Vier Funde aus der Durchsicht, alle auf der VM gemessen.

1. DER PLATTEN-SCHUTZ AUS c065967 WAR WIRKUNGSLOS

_original_aufheben() entschied per os.stat().st_dev, ob umgehaengt oder
kopiert werden muss. Im Worker-Container gemessen - beides gleichzeitig wahr:

    st_dev /app/temp  = 2050
    st_dev /app/media = 2050        → identisch
    os.rename(...)    → EXDEV, "Invalid cross-device link"

Der Kernel vergleicht bei rename() den MOUNT, nicht das Geraet. /app/temp
(Docker-Volume) und /app/media (Bind-Mount) sind zwei Mounts DERSELBEN
ext4-Partition. Die Pruefung sah "gleiches Dateisystem", uebersprang die
Platzpruefung, und shutil.move kopierte doch - 75 GB bei 37 GB frei. Der
Schutz haette genau den Schaden zugelassen, gegen den er gebaut wurde.

Jetzt wird os.rename VERSUCHT statt vorhergesagt: klappt es, ist es
umgehaengt und fertig; kommt EXDEV, steht die Kopie fest und ERST DANN wird
der Platz geprueft. Das ist keine Vermutung mehr, sondern die Antwort des
Kernels. Vier Tests in test_original_aufheben.py, darunter genau der Fall,
der die Platte fuellte. Die zwei alten Tests in test_medien.py sind dorthin
gewandert - sie taeuschten per gefaelschtem os.stat "verschiedene
Dateisysteme" vor, also genau die Annahme, an der der Schutz scheiterte.

2. DIE FORTSCHRITTSANZEIGE ZEIGTE DEN SCAN, NICHT DEN ENCODE

get_progress_from_line matchte jede Zahl vor einem Prozentzeichen. HandBrake
gibt Prozente aber in drei Phasen aus (Formatstrings aus dem Binary gelesen):

    Scanning title %d of %d, preview %d, %.2f %%          → laeuft VOR dem
                                                            Encode bis 100 %
    Encoding: task %d of %d, %.2f %%       (%.2f fps, avg  → der echte Wert
    Encoding: task %d of %d, Searching for start time, ... → Vorlauf

Dazu warf `if progress > 0` im Aufrufer jeden Wert unter 1,00 % weg. Live
beobachtet: Anzeige stand auf 99 %, der Encode bei 1,06 %; sie fiel erst auf
1, als der Encode die 1-%-Marke ueberschritt. Jetzt wird nur die
Encoding-Zeile gelesen, `task N of M` mitgerechnet (sonst springt die
Anzeige bei Zwei-Pass-Presets mitten in der Datei zurueck), und -1 heisst
"keine Angabe" - dasselbe Muster wie bei get_progress_from_prgv.

3. "AUTOMATISCHER AUSWURF" WURDE VON NIEMANDEM GELESEN

Die Einstellung (Standard: ein, "Disc nach erfolgreichem Ripping automatisch
auswerfen") kam in keiner Zeile Backend-Code vor. DVD/Blu-ray warfen deshalb
NIE aus, Audio-CDs IMMER, weil abcde `-x` fest verdrahtet bekam. Jetzt
entscheidet die Einstellung beides: wirf_disc_aus() per CDROMEJECT-ioctl
(fcntl-guarded, der native Windows-Worker laedt das Modul auch) und `-x` nur
noch, wenn gewuenscht.

4. PFAD-PRUEFUNG FIEL AUF PRAEFIX-NAMEN HEREIN

Elf Stellen prueften mit nacktem startswith(MEDIA_ROOT). "/app/media-boese/x"
beginnt mit "/app/media", liegt aber ausserhalb - betroffen waren auch
/browse und /browse/mkdir, wo der Pfad vom Nutzer kommt. Neuer
Zwillings-Helfer unter_wurzel() in api/main.py und worker/tasks.py, alle elf
Stellen umgestellt, Tests in beiden.

Nebenbefund: _zielbasis() benutzte os.path.normpath - unter Windows werden
daraus Backslashes, die MEDIA_ROOT-Pruefung greift nicht mehr, und das
gewaehlte Ziel faellt still auf den Standard zurueck. Genau die Falle, die
_arbeitsverzeichnis() drei Zeilen weiter dokumentiert und mit posixpath
vermeidet. Live war es nie (nur aus rip_disc, das auf Windows verriegelt
ist), jetzt konsistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:05:11 +02:00
Hitonabi 883c1c290b fix(caps,ui): Encoder werden gemessen statt behauptet
Der Modulkopf von caps.py verspricht "ehrlich erkannt, nicht behauptet" -
erkenne_encoder() tat aber drei Mal das Gegenteil:

1. cpu-x264 und cpu-x265 standen fest verdrahtet in der Liste ("immer
   dabei"). Ein Rip-Worker OHNE HandBrake behauptete damit, komprimieren zu
   koennen; jeder Transcode dort endete sofort mit "nicht installiert".
2. 'vaapi' wurde allein wegen /dev/dri gemeldet, ohne zu pruefen, ob
   HandBrake das ueberhaupt kann. Auf der VM gemessen: das Worker-Image
   kennt svt_av1/x264/x265/mpeg4/mpeg2/VP8/VP9/theora und KEINEN einzigen
   Hardware-Encoder. Rippy haette VAAPI versprochen und dann versagt.
3. Ueber die Rechenleistung sagte es gar nichts. Genau deshalb war nicht zu
   sehen, dass ein 4K-Encode auf dieser Maschine Tage braucht: CPU-Modell
   ist das generische "QEMU Virtual CPU version 2.5+", grep -c avx2
   /proc/cpuinfo = 0, nur bis sse4_2. x265 lebt von AVX2. Gemessen an der
   Leseposition des Quellstroms: 28-55 h fuer einen Film, 3,84 von 4 Kernen
   gesaettigt. Abhilfe waere der CPU-Typ 'host' in Proxmox.

Jetzt wird die Encoder-Liste aus `HandBrakeCLI --help` geparst (Abschnitt
-e/--encoder, Formatstrings im Worker-Image gemessen - AGENTS Regel D), und
Hardware zaehlt nur, wenn Geraet UND HandBrake-Unterstuetzung da sind. Neu
gemeldet: cpu-av1 (svt_av1 ist wirklich verfuegbar) sowie CPU-Modell,
Kernzahl und Vektorbefehlsstufe je Worker.

UI: Kerne/Vektorbefehle stehen bei jedem Worker (Worker-Tab und
Einstellungen -> System), dazu die ungefilterte HandBrake-Auskunft und eine
sichtbare Warnung, wenn AVX2 fehlt - inklusive des Hinweises auf den
VM-CPU-Typ. Der Warntext liegt in lib/encoder.ts, damit er nicht doppelt
im Code steht.

Mit im gleichen Zug (dieselbe Datei): der Schalter "Alle Tracks rippen" ist
entfallen. Er war ein Placebo - abcde bekommt keine Track-Auswahl und es gab
auch keine Oberflaeche, um eine Teilmenge zu waehlen. Eine Audio-CD wurde
also immer vollstaendig gerippt, egal wie der Schalter stand. An seiner
Stelle steht jetzt die Wahrheit.

7 Tests, Testdaten sind echte HandBrake-Ausgaben von der VM. Der Parser ist
zusaetzlich gegen die vollen 697 Zeilen der echten --help gegengeprueft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:04:42 +02:00
Hitonabi e0cb7b3ddc feat(worker): Zombie-Erkennung - Jobs, an denen niemand arbeitet
Nach einem Absturz oder Rebuild blieb ein Job auf 'transcoding' stehen,
obwohl weder ein Prozess lief noch etwas in den Queues stand. Folge: keine
Anzeige, kein Download - und der Knopf "Neu komprimieren" fehlte, weil
_kann_neu_komprimieren (api/main.py) status=='failed' verlangt. Der Job war
unerreichbar, obwohl die Rohdateien vollstaendig dalagen.

zombies.py haelt beim Worker-Start die Jobs in 'ripping'/'transcoding'/
'canceling' gegen Celerys active/reserved/scheduled und setzt sie ehrlich
auf 'failed', wenn niemand daran arbeitet.

Drei Sicherungen, weil ein falsch getoeteter Job teurer ist als eine
stehengebliebene Leiche:
- nur beim Start (da ist "es lief nichts" eindeutig; ein periodischer Lauf
  koennte einen Job erwischen, der legitim in der Warteschlange wartet)
- 120 s Gnadenfrist (Celery stellt unbestaetigte Aufgaben erneut zu)
- Vollzaehligkeit: antworten weniger Knoten als laut Herzschlag online sind,
  wird NICHTS gewertet - sonst waere der laufende Job eines beschaeftigten
  Remote-Workers eine falsche Leiche

Die Job-ID wird per Textsuche ueber die Inspektions-Antwort gefunden, nicht
per Position: sie steht bei rip_disc an zweiter, bei transcode_files an
erster Stelle, und Celery liefert args je nach Version als Liste oder Text.

13 Tests ohne Postgres/Redis, darunter "laufender Job wird nicht angetastet"
und "schweigender Worker verhindert jedes Urteil".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 21:04:19 +02:00
Hitonabi 5cd1876c90 docs(savepoint): Nachtrag - 4K-Transcode abgebrochen, zwei neue Befunde
Ampel / ampel (push) Successful in 48s
Der im Uebergabestand als "laeuft" beschriebene 4K-Lauf wurde um 18:31 vom
Commander abgebrochen. Damit war die Aussage im Block darueber ueberholt -
nachgezogen, statt sie stehen zu lassen.

Zwei Befunde, beide gemessen:

1. 4K-HEVC ist auf dieser CPU nicht machbar: 18:02 bis 18:31 ergab 1 Prozent,
   hochgerechnet rund 50 Stunden fuer den Film. Zu entscheiden: UHD gar nicht
   komprimieren (Roh-MKV behalten), Hardware-Encoder (Remote-Worker mit GPU),
   oder bewusst bei 1080p bleiben. Preset-je-Disc-Typ ist richtig, reicht
   allein aber nicht.

2. Abbruch wirkt verzoegert (FEHLER, nicht behoben): Job steht seit 18:31 auf
   "canceling", HandBrake lief um 18:35 immer noch. Der Abbruch wird nur in
   datei_fortschritt geprueft, und die Closure laeuft nur bei geaenderter
   Prozentzahl ("if gesamt == letzter[0]: return"). Bei 1 Prozent alle 30
   Minuten sieht "Abbrechen" bis zu eine halbe Stunde lang wirkungslos aus.
   Der Abbruch gehoert unabhaengig vom Fortschritt geprueft.

Ausserdem festgehalten: die 1080p-Fassung wurde von HandBrake beim Start auf
0 Bytes gekuerzt, im Akira-Ordner liegt derzeit ein unbrauchbares Fragment.
Der 75-GB-Rohschnitt ist unversehrt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 20:32:34 +02:00
Hitonabi b526a0a033 docs(savepoint): Uebergabestand v3.13 - gemessen vs. vermutet getrennt
Ampel / ampel (push) Successful in 46s
Uebergabe an eine neue Sitzung. Der Block trennt bewusst, was per Befehl auf
der VM geprueft wurde, von dem was offen bzw. nur erwartet ist - inklusive
zweier Fehlschluesse dieser Sitzung, damit die naechste Sitzung weiss, welchen
Aussagen sie nicht blind glauben soll.

Kern:
- Ein 4K-HandBrake-Lauf laeuft GERADE (Akira, Preset "H.265 MKV 2160p60 4K").
  Die vorherige 1080p-Datei wurde dabei auf 0 Bytes gekuerzt; der 75-GB-
  Rohschnitt liegt unversehrt in /app/temp/raw.
- progress=99 am Job ist ein Altwert aus dem Absturz, NICHT der echte Stand.
- Noch nicht bewiesen: dass _original_aufheben() im Ernstfall nur warnt statt
  die Platte vollzuschreiben. Genau dieser Pfad ist neu.
- Gefunden, nicht gebaut: Zombie-Erkennung. Nach dem Absturz stand der Job auf
  "transcoding", obwohl kein Prozess lief und beide Celery-Queues leer waren.
  _kann_neu_komprimieren verlangt status == "failed", deshalb fehlt dem Nutzer
  auch der "Neu komprimieren"-Knopf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 20:08:51 +02:00
Hitonabi 8bb075c656 feat(rip): Arbeitsverzeichnis je Rip waehlbar statt global vorgegeben
Ampel / ampel (push) Successful in 28s
Commander-Vorgabe 25.07.2026: "Du sollst es nicht automatisch setzen, es soll
auswaehlbar sein - z. B. beim Rippen starten, oder VORHER wenn Vollautomatik
eingeschaltet ist."

Bisher gab es nur die globale Einstellung workDir, und die war ein freies
Textfeld - man musste den Container-Pfad (/app/media/...) kennen. Beim
Akira-Rip landeten deshalb 74 GB Rohdaten auf der 148-GB-VM-Platte, obwohl
eine NAS-Freigabe mit 2,3 TB eingehaengt war.

- RipTargetModal: neue Auswahl "Arbeitsverzeichnis fuer die Rohdaten",
  gespeist aus /storage-targets (mit Kennzeichnung als Netzwerk-Freigabe und
  freiem Platz je Ziel). Leer = "Standard aus den Einstellungen", der Wert
  wird zur Orientierung mit angezeigt. Bei Musik ausgeblendet - CD-Rips gehen
  direkt als FLAC ins Ziel, ohne Roh-Zwischenstufe.
- POST /jobs nimmt work_dir entgegen, mit derselben Pfad-Haerte wie das Ziel
  (_validiere_ziel: muss unter /app/media liegen), und legt es in die
  Job-Metadaten.
- _arbeitsverzeichnis(einstellungen, job_wahl) im Worker: Wahl dieses Rips ->
  Setting -> Container-Default. Damit bleibt die Einstellung genau das, was
  bei Vollautomatik-Rips greift, weil dort niemand gefragt wird. Der Text im
  Einstellungen-Tab sagt das jetzt auch so.
- posixpath statt os.path in _arbeitsverzeichnis: das sind immer
  Container-Pfade, auch wenn ein nativer Windows-Worker das Modul laedt (der
  uebersetzt erst spaeter per pfad_lokal). os.path.normpath machte unter
  Windows Backslashes daraus, wodurch die MEDIA_ROOT-Pruefung nicht mehr
  griff - lokal als Testfehler aufgefallen.
- Test deckt die Reihenfolge und die Ausbruchsversuche ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:59:37 +02:00
Hitonabi c065967f4d fix(ui,transcode): Dialog-Falle, Arbeitsverzeichnis waehlbar, Original-Aufheben entschaerft
Ampel / ampel (push) Successful in 28s
Drei Befunde aus dem ersten echten UHD-Durchlauf (Akira). Der Rip und die
Kompression liefen sauber durch - danach hat "Original behalten" die Platte
vollgeschrieben und den fertigen Job als fehlgeschlagen markiert.

1. ORIGINAL-AUFHEBEN DARF DEN JOB NICHT MEHR TOETEN (tasks.py)
   Vorher stand dort ein nacktes shutil.move(raw_dir, ziel). Arbeits-
   verzeichnis (/app/temp, Docker-Volume) und Ziel (/app/media, Bind-Mount)
   sind VERSCHIEDENE Dateisysteme - os.rename scheitert mit EXDEV, shutil.move
   faellt auf Kopieren zurueck. Ergebnis am 25.07.: 74-GB-Vollkopie auf
   dieselbe Platte, Abbruch bei 41 GB mit ENOSPC, Platte 100 % voll, Worker-
   Container startete nicht mehr ("failed to mount: no space left on device"),
   und der Job galt als FEHLGESCHLAGEN - obwohl die komprimierte Datei
   (4,8 GB) fertig und in Ordnung war. Der Nutzer sah nur eine leere Queue.
   Jetzt: _original_aufheben() prueft erst, ob ueberhaupt kopiert werden muss
   (gleiches Dateisystem -> reines Umhaengen), prueft sonst den freien Platz
   VORHER, faengt jeden OSError ab, raeumt eine halbe Kopie weg und meldet das
   als WARNUNG. Der Job bleibt erfolgreich, die Roh-Datei bleibt liegen.
   Zwei Tests decken beide Wege ab.

2. ARBEITSVERZEICHNIS IST JETZT WAEHLBAR (Settings.tsx)
   Es war ein freies Textfeld - man musste den Container-Pfad (/app/media/...)
   KENNEN, um eine Netzwerk-Freigabe zu treffen. Genau daran ist es
   gescheitert, weshalb der 74-GB-Rohschnitt ueberhaupt erst auf der VM-Platte
   landete. Jetzt eine Auswahl aus /storage-targets (dieselbe Liste wie bei
   den Speicherzielen), inklusive Kennzeichnung als Netzwerk-Freigabe und
   freiem Platz je Ziel.

3. DIALOGE KLEBTEN IM PANEL (ui/Modal.tsx)
   "Rippen starten" im Laufwerke-Tab oeffnete den Dialog INNERHALB des
   Bereichs, teils abgeschnitten. Ursache: .glass-panel in index.css setzt
   backdrop-filter: blur(16px), und ein Element mit backdrop-filter wird zum
   Bezugsrahmen fuer position: fixed seiner Nachfahren - das Modal war damit
   an der Card ausgerichtet statt am Fenster. Modal rendert jetzt per
   createPortal an document.body. Behebt es fuer ALLE Dialoge auf einmal.

Aufgeraeumt: die abgebrochene 41-GB-Teilkopie unter
"/app/media/movies/Akira (1988)/original/" geloescht (nachweislich
unvollstaendig - 41 GB gegen 79,6 GB Quelle, Quelle intakt). Platte wieder
bei 78 %, Container laufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 19:48:10 +02:00
Hitonabi e84afc1718 feat(transcode): ein HandBrake-Preset je Disc-Typ statt eines fuer alles
Ampel / ampel (push) Successful in 29s
Rueckfrage des Commanders beim ersten echten UHD-Rip: "merkt Rippy, dass es
eine UHD ist, und nimmt direkt das 4K-Preset?" Antwort war nein. Die
Kompression fragte den Disc-Typ ueberhaupt nicht:

    preset = einstellungen.get("transcodePreset") or DEFAULT_HB_PRESET

Live eingestellt war "HQ 1080p30 Surround". Der gerade laufende Akira-Rip
waere also verlustfrei in 4K gerippt und danach auf 1080p heruntergerechnet
worden - und mit keepOriginal=False waere der 4K-Rohschnitt danach geloescht
worden. Umgekehrt wurde eine DVD auf 1080p hochskaliert, was nichts bringt.

- preset_fuer(disc_type, einstellungen) in ripping.py, pure und getestet.
  Reihenfolge: Preset des Disc-Typs -> allgemeines transcodePreset ->
  DEFAULT_HB_PRESET. Bestandsinstallationen aendern ihr Verhalten NICHT,
  solange die neuen Felder nicht gespeichert sind.
- Drei Einstellungen: transcodePresetDvd / transcodePresetBluray /
  transcodePresetUhd. transcodePreset bleibt als Rueckfall bestehen.
- transcode_files holt den Disc-Typ aus dem Job-Datensatz und schreibt ihn
  mit ins Log ("Disc-Typ 'uhd', Preset '...'").
- UI: drei Auswahlfelder statt einem, mit Klartext dazu, warum eine 4K-UHD
  auf ein 2160p-Preset gehoert.

Preset-Namen stammen aus "HandBrakeCLI --preset-list" im Worker-Image
(HandBrake 1.6.1) - nicht aus dem Kopf (AGENTS Regel D):
H.265 MKV 2160p60 4K, HQ 2160p60 4K HEVC Surround,
Super HQ 2160p60 4K HEVC Surround, H.265 MKV 1080p30, HQ 1080p30 Surround,
Super HQ 1080p30 Surround, H.265 MKV 576p25, H.265 MKV 480p30,
HQ 576p25 Surround.

Sofortmassnahme am laufenden Job (auf Ansage des Commanders): keepOriginal
auf True gesetzt - nur dieses eine Feld, gegengeprueft dass kein anderer
Schluessel veraendert wurde. Damit ueberlebt der 4K-Rohschnitt die
Kompression.

ACHTUNG - Deploy bewusst NICHT ausgefuehrt: docker compose up -d --build
wuerde den Worker-Container neu erstellen und den laufenden Akira-Rip
abbrechen. Erst nach Abschluss des Jobs deployen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:38:52 +02:00
Hitonabi f449c4ee34 fix(uhd): 4K-UHD geloest - makemkvcon holt Schluessel unter Linux nie
Ampel / ampel (push) Successful in 28s
Richtigstellung des Vortags-Befunds. Dort stand, MakeMKVs Schluessel-Kanal
sei abgeschaltet. Das war FALSCH: die Herleitung stuetzte sich auf zwei
Hostnamen aus alten Forumsbeitraegen (hkdata.fairuse.org,
hkdata.crabdance.com), die zwar wirklich nicht mehr aufloesen, von MakeMKV
aber laengst nicht mehr benutzt werden. Aufgedeckt durch den Einwand des
Commanders, unter Windows ginge es sofort.

Gegenprobe mit demselben Laufwerk und derselben Disc (Akira UHD, MKB v76):

                        Linux (Worker)      Windows
  Verbindungen          KEINE EINZIGE       185.84.108.20:443
  Meldung 3338          nie                 "Downloading latest HK"
  _private_data.tar     2048 B, 0 Keys      6,4 MB, 604 Keys
  Disc                  volume key unknown  TCOUNT:5, geht auf

Gegengeprueft mit leerem UND gefuelltem Speicher, mit und ohne --noscan,
mit dev:/dev/sr0 und disc:0, mit geloeschter update.conf. Linux fragt nie.
Die Meldungsvorlage "Downloading latest %1 to %2 ..." steckt sehr wohl im
Linux-Binary - sie loest nur nicht aus. Gleiches Symptom im MakeMKV-Forum,
seit Jahren offen (t=25782, t=34022). Der Dienst lebt; der Worker erreicht
185.84.108.20:443 sogar problemlos.

BEWIESEN: Nach Uebernahme des Windows-Schluesselspeichers oeffnet
makemkvcon auf der VM die Akira-UHD - "Operation successfully completed",
TCOUNT:5, fuenf Titel, identisch zum Windows-Ergebnis. Erster belegter
UHD-Disc-Zugriff auf der Rippy-Maschine.

- makemkv_daten.py (beide Zwillinge): zaehle_schluessel,
  private_data_pruefen, schluesselspeicher_status, private_data_schreiben.
  Die Pruefung lehnt einen Speicher OHNE hkd_*.bin ab - sonst laedt jemand
  den leeren Vorrat einer frischen Installation hoch, nichts aendert sich,
  und niemand versteht warum. Modulkopf komplett neu, inkl. der
  Fehldiagnose als Warnung fuer spaeter.
- API: GET/POST /system/keystore. Der Rohkoerper der Anfrage IST die Datei
  (binaer - JSON/Base64 waere Ballast, Multipart kann die API nicht).
  Groessengrenze 64 MB = client_max_body_size in nginx.conf.
- UI: neuer Block "Disc-Schluessel fuer 4K-UHD" UEBER dem KEYDB-Block, mit
  Schluessel-Anzahl, Upload und Anleitung fuer den Windows-Weg. KEYDB.cfg
  ist jetzt als Notnagel beschriftet. Worker-Plakette zeigt die Anzahl;
  0 heisst sichtbar "4K-UHD scheitert".
- tasks.py: UHD-Fehlertext sagt den Windows-Weg an und nennt die Anzahl
  bekannter Schluessel dieses Workers.
- caps.py meldet schluessel je Worker.
- Alle Falschaussagen korrigiert: UI (3), Anleitung (2), README (3),
  KONZEPT §8 + §10, Worker-Dockerfile, makemkv_key.py (dort stand "Den
  AACS-Schluessel zieht MakeMKV via LibreDrive ohnehin selbst aus dem
  Laufwerk" - gilt fuer Blu-ray, NICHT fuer UHD).
- SAVEPOINT v3.11, ROADMAP Etappe 18 (Etappe 17 mit Nachtrag), AGENTS.

Offen: voller UHD-Rip inkl. Transcode-E2E; und ob sich der Abruf unter
Linux doch anstossen laesst.

Quellen (AGENTS Regel D):
- Linux laedt keine Hashed Keys, gleiches Symptom:
  https://forum.makemkv.com/forum/viewtopic.php?t=25782
  https://forum.makemkv.com/forum/viewtopic.php?t=34022
- Schluessel als hkd_*.bin in _private_data.tar:
  https://forum.makemkv.com/forum/viewtopic.php?t=32675
- Meldungsformat: https://www.makemkv.com/developers/usage.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 11:18:09 +02:00
Hitonabi 0935766f61 feat(uhd): KEYDB.cfg-Unterstuetzung - MakeMKVs Schluessel-Kanal liefert nichts mehr
Ampel / ampel (push) Successful in 28s
4K-UHD scheiterte an "The volume key is unknown for this disc". Am 25.07.2026
im Worker-Container nachgemessen: Laufwerk und MakeMKV sind in Ordnung
(LibreDrive v06.3 / Meldung 1011, Disc wird gelesen, AACS-Dump geschrieben) -
MakeMKV versucht gar nicht erst, online einen Schluessel zu holen. Belege:
_private_data.tar enthaelt nur die Index-Datei und KEINE hkd_*.bin; auch mit
geloeschter update.conf (Meldung 5074 belegt den Web-Kontakt) und mit
app_UpdateEnable="1" kam keiner; hkdata.fairuse.org und hkdata.crabdance.com
loesen weltweit nicht mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8, 1.1.1.1).
Betroffen war Akira UHD (MKB v76, Pressung Dez. 2020) - also gerade KEINE
Neuerscheinung. Der bisherige Fehlertext ("Disc neuer als die
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war falsch.

Einziger heute funktionierender Weg ist eine vom Nutzer selbst mitgebrachte
KEYDB.cfg. Rippy liefert KEINE Schluessel mit, laedt keine herunter und
verteilt keine - es stellt nur den Platz bereit und zeigt an, was dort liegt.

- Datenverzeichnis persistent gemountet (MAKEMKV_DATA_HOST, Default
  /srv/rippy/makemkv): KEYDB.cfg und AACS-Dumps ueberleben jeden Rebuild.
  Vorher loeschte jeder "up -d --build" beides - inklusive des Dumps, auf den
  die Fehlermeldung selbst verwies.
- entrypoint.sh und tasks.py schreiben settings.conf ergaenzend statt
  zerstoerend. Der entrypoint bricht bei nicht beschreibbarem Verzeichnis
  nicht mehr ab - mit "restart: unless-stopped" waere das ein Crashloop
  gewesen, in dem auch reines DVD-Rippen tot ist.
- Neues Zwillings-Modul makemkv_daten.py (docker/api + docker/worker,
  byteweise identisch; test_zwillinge_sind_byteweise_identisch wacht darueber
  und wurde durch absichtliches Verstellen als wirksam nachgewiesen).
- API: GET/POST/DELETE /system/keydb, GET /system/aacs-dumps(/{dateiname}).
  JSON-Body statt Multipart - python-multipart ist bewusst nicht installiert
  und wuerde die API beim Import toeten. nginx client_max_body_size 64m,
  sonst scheitert der Upload mit 413, bevor die API ihn sieht.
- UI (Einstellungen -> System): Status, Hochladen per Datei-Dialog, Entfernen,
  Dump-Download, KEYDB-Plakette je Worker (nur wo das Verzeichnis wirklich
  gemountet ist - ein Remote-Transcode-Worker truege sonst eine Warnung,
  die ihn nichts angeht).
- parse_msg() + log_cb: MakeMKV-Meldungen landen im Rippy-Log (gedrosselt:
  Code 1003 raus, keine Wiederholungen, max. 40 je Rip). Nebenbei behoben:
  der alte Parser (split(",", 4)[3]) schnitt jede Meldung am ersten Komma ab.
- Fuenf Stellen richtiggestellt, die behaupteten, MakeMKV-Updates braechten
  die neueste Disc-Schluessel-Datenbank mit (UI, Anleitung, README,
  Worker-Dockerfile, makemkv_key.py).

NICHT bewiesen: ein erfolgreicher UHD-Rip - es lag keine KEYDB.cfg mit dem
Akira-Schluessel vor. Belegt sind der Befund und die neue Mechanik. So steht
es auch im SAVEPOINT und in der ROADMAP.

Quellen (AGENTS Regel D):
- Datenverzeichnis + Dateiname GROSS/case-sensitiv:
  https://forum.makemkv.com/forum/viewtopic.php?t=30636
- hkd_*.bin in _private_data.tar:
  https://forum.makemkv.com/forum/viewtopic.php?t=32675
- headless settings.conf / app_UpdateEnable:
  https://forum.makemkv.com/forum/viewtopic.php?t=20364
- KEYDB.cfg-Zeilenformat (libaacs):
  https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
- MSG-/PRGV-Format: https://www.makemkv.com/developers/usage.txt

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 01:26:08 +02:00
Hitonabi f74f4e54f6 fix(api): Job-Meta (poster_path) an die Jobliste durchreichen
Ampel / ampel (push) Successful in 32s
GET /jobs nutzt response_model=List[Job]; das Job-Model hatte kein meta-Feld,
also schnitt FastAPI die Disc-Metadaten (poster_path) weg -> Dashboard.tsx
bekam job.meta = undefined -> Filmstreifen-Platzhalter statt TMDB-Poster,
sowohl in der Jobliste als auch im aktiven Rip-Header (beide aus /jobs).

Additiv: meta: Optional[Dict] ins Job-Model + in _job_row_to_model parsen
(json.loads wie im Detail-Endpunkt, defensiv gegen kaputtes JSON). Keine
UI-Aenderung noetig -- posterUrl() rendert dann die vorhandenen Poster.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 00:03:41 +02:00
Hitonabi 0832ef777c chore(handoff): portabel machen + Doku/Onboarding glaetten (Review-Umsetzung)
Ampel / ampel (push) Successful in 27s
Aus dem Weitergabe-Review. Kein Verhaltenswechsel fuer den Ursprungs-Host
(alle Defaults = bisheriges Verhalten):

Portabilitaet:
- Laufwerk-Geraeteknoten via .env parametrisiert (OPTICAL_SR/OPTICAL_SG,
  Defaults sr0/sg1) - der #1-Blocker: auf Fremdhosts liegt das sg-Node woanders.
- deploy.sh generisch (RIPPY_VM/RIPPY_REPO_URL/... aus der Umgebung) - keine
  festen Besitzer-Adressen (interne IP + DDNS) mehr im Repo.
- POSTGRES_PASSWORD in compose durchverdrahtet (Default rippy) - war No-op.

Aufraeumen (toter/irrefuehrender Code):
- udev/ entfernt (fehlende Regeldatei, falscher Container-Name; durch ioctl-
  Polling ersetzt - reine Altlast).
- docker/postgres/*, docker/redis/*, init.sql entfernt (nie eingebunden).

Onboarding:
- FirstRunWizard verdrahtet: App.tsx fragt GET /setup und zeigt den
  Einrichtungs-Assistenten beim ersten Start (war gebaut, aber nie gerendert).

Doku:
- README: Laufwerk-Abschnitt auf .env, ENV-Tabelle (OPTICAL_*/POSTGRES),
  rshared-Anleitung, neuer Abschnitt "Haertung fuer fremde/exponierte Netze".
- config_validation.py: TMDB Pflicht->empfohlen, Zeichensalat-Tippfehler gefixt.
- .env.example: TMDB-Wording, OPTICAL_*-Variablen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:32:59 +02:00
Hitonabi 6a082cf77c fix(mounts): mounten() idempotent - stale/tote Mounts vor Re-Mount loesen
Ampel / ampel (push) Successful in 28s
Folgefix zum remount-Vorfall 24.07.: mounten() fiel bei einem TOTEN Mount
(os.path.ismount wirft OSError) auf den echten `mount` durch und stapelte auf die
Leiche. Ueber viele Neustarts (via rshared propagiert, ueberlebt Container-Recreate)
wuchs das auf 12 Schichten; die tote oberste blockierte jeden Zugriff (ls-Timeout,
obwohl SMB-445 offen) -> Medien-Mount unbrauchbar.

Fix: _stale_mounts_loesen(ziel) loest per lazy `umount -l` alle Schichten, bevor neu
gemountet wird -> kein Stapeln mehr, Re-Mount idempotent. Ein gesunder Mount wird
weiterhin frueh erkannt (os.path.ismount) und unangetastet gelassen.

Tests (test_mounts_helpers.py): Loesch-Schleife bis leer (monkeypatch) + Verdrahtung.
Ruff gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 22:04:00 +02:00
Hitonabi 46a8f50c34 fix(api): remount nicht-blockierend (Netz-Mount darf API-Start nicht haengen)
Ampel / ampel (push) Successful in 28s
Vorfall 24.07.: Beim API-Start blockierte der synchrone CIFS-Schreibtest in
alle_remounten()/mounten() im Kernel (wait_for_response), als der SMB-Server
langsam war -> ~5 min "Waiting for application startup", kein Endpoint bedient
(bis der soft-Mount per Timeout abbrach). startup_event() lief isoliert sauber,
also war es der blockierende Netz-Mount, nicht die App-Logik.

Fix: remount als Hintergrund-Task (asyncio.create_task) statt await -> die API
kommt sofort hoch, die Mounts stellen sich her sobald der Server antwortet.
Test (test_api_smoke.py): haelt den Nicht-blockierend-Vertrag per Quelltext-
Inspektion fest, im Stil der anderen Verdrahtungs-Tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:50:32 +02:00
Hitonabi 3f47b504c2 feat(api): MakeMKV-Beta-Key automatisch erneuern
Ampel / ampel (push) Successful in 28s
Der kostenlose Beta-Key wechselt ~monatlich und laeuft zum Monatsende ab; bisher
musste er von Hand nachgetragen werden, sonst blockt Blu-ray-Ripping irgendwann
still. Neu: taeglicher Forum-Abgleich (t=1053) -> Key in die Settings
(makemkvAppKey). Der Worker liest ihn VOR jedem Rip (tasks.py) -> wirkt ohne
Rebuild ab dem naechsten Rip.

- docker/api/makemkv_key.py: extract_key (pure, testbar) + fetch_current_key +
  refresh_once/refresh_loop; loggt in die App-Logs (add_log).
- docker/api/main.py: refresh_loop als startup-Task.
- docker/api/test_makemkv_key.py: Parser-Tests - fingen den Bug, dass Beta-Keys
  laenger als 64 Zeichen sind (Regex {50,} statt {64}).

Betrifft nur die Software-Lizenz (oeffentlicher Gratis-Key), NICHT Disc-Schluessel
- die zieht MakeMKV via LibreDrive selbst. Ruff gruen, 4 Tests gruen, Live-Fetch
gegen das echte Forum verifiziert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 21:27:38 +02:00
Hitonabi 71c618073e docs: Anleitung auf .exe-Installer, SAVEPOINT v3.9
Ampel / ampel (push) Successful in 36s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:20:28 +02:00
Hitonabi 20cdfedb85 feat(installer): fertige .exe mit Rippy-Icon + ohne Konsolenfenster (statt .vbs)
Ampel / ampel (push) Successful in 28s
Commander-Einwand: .vbs ist abgekuendigt (Win11 24H2 Feature-on-Demand) und
wird oft als gefaehrlich geflaggt. Loesung: echte .exe.
- RippyWorkerSetup.exe (50 KB): install-gui.ps1 via ps2exe kompiliert —
  eingebettetes Rippy-Disc-Icon (deploy/worker-windows/rippy.ico), kein
  Konsolenfenster (-noConsole), WinForms (-STA). E2E getestet: Fenster
  oeffnet, conhost-Zaehler unveraendert (kein Konsolenfenster).
  Vorgebaut auf Windows (build-exe.ps1) + committet — eine Windows-.exe
  kann nicht von Linux (Rippy-Host) gebaut werden. WICHTIG: friert KEINE
  Versionen ein — HandBrake wird zur Laufzeit (GitHub latest) und der
  Worker-Code live von Rippy geholt; nur bei GUI-Aenderungen neu bauen.
- API: GET /worker-setup/windows-exe (FileResponse); Dockerfile kopiert die
  .exe ins Image. .gitattributes: *.exe/*.ico als binary.
- UI (Worker -> Windows): 'Installer herunterladen (.exe)' als Haupt-Weg
  (generisch, GUI fragt Adresse ab; die einzutragende IP wird als Hinweis
  angezeigt). .vbs-Erzeugung entfernt. CLI bleibt Profi-Alternative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:15:42 +02:00
Hitonabi b1d9ece9b2 feat(installer): kein Konsolenfenster (.vbs-Starter) + Rippy-Icon im Installer-Fenster
Ampel / ampel (push) Successful in 28s
Commander-Wunsch: kein Konsolenfenster + Icon.
- Starter ist jetzt eine .vbs (statt .bat): wscript startet PowerShell mit
  Fensterstil 0 = KOMPLETT versteckt, kein Konsolenfenster. Nur das
  WinForms-Installer-Fenster erscheint. Download-Datei: rippy-worker-setup.vbs
  (weiter CLIENT-seitig mit eingetragener LAN-IP erzeugt).
- install-gui.ps1: Fenster-Icon = Rippy-Disc (zur Laufzeit gezeichnet,
  Indigo-Kreis + weisses Loch) — erscheint in Titelleiste + Taskleiste.
  (Die Datei selbst kann Windows-bedingt kein Icon tragen — nur .exe/.lnk.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 19:03:19 +02:00
Hitonabi b337525bed docs+ui: grafischen Windows-Installer in Anleitung, SAVEPOINT v3.8
Ampel / ampel (push) Successful in 28s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:52:57 +02:00
Hitonabi 4fb29d16cf feat(worker): grafischer Windows-Installer (GUI statt CLI)
Ampel / ampel (push) Successful in 28s
Commander-Wunsch: die CLI-Installation ist grausam. Jetzt:
- install-gui.ps1 (WinForms, ASCII-only): Fenster mit Feldern (Rippy-IP,
  Worker-Name, Autostart-Checkbox), Install-Knopf mit Live-Log, danach
  'Worker starten'-Knopf. Gleiche Schritte wie der CLI-Installer
  (Worker-Code + HandBrake latest + venv + Tray/Start/Uninstall).
  Braucht nur Python 3.10+, keine externe GUI-Runtime.
- API: GET /worker-setup/windows-gui serviert die GUI; Dockerfile kopiert
  sie ins worker_dist/.
- UI (Worker-Tab, Windows): 'Grafischen Installer herunterladen (.bat)' als
  Haupt-Weg — die .bat wird CLIENT-seitig mit der eingetragenen LAN-IP
  erzeugt (Blob-Download), Doppelklick laedt+startet die GUI von Rippy.
  Der CLI-Befehl bleibt als Profi-Alternative.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:47:57 +02:00
Hitonabi 57171b4c88 ui(system): Versionslage exakt darstellen — MakeMKV aktualisierbar, Docker-HandBrake bewusst Debian-stabil
Ampel / ampel (push) Successful in 28s
Commander-Wunsch: das UI soll die Werkzeug-Versionslage genau so zeigen,
wie sie ist. Klarstellung im System-Tab:
- Hinweis bei 'Installierte Werkzeuge': MakeMKV aktuell haltbar (wichtig
  wegen Schluessel-DB), HandBrake im Docker-Worker bewusst Debian-stabil
  (kein separates Update), native Worker holen die neueste.
- Update-Check: MakeMKV mit gruenem '✓ aktuell' bzw. Update-Befehl (echtes
  To-do). HandBrake NICHT mehr als 'Update noetig' alarmieren — neutral als
  'Debian-Version, bewusst so' + Info, dass die nativen Worker die neueste
  nutzen.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:42:01 +02:00
Hitonabi 3aef344b95 docs: SAVEPOINT v3.7 — Mount-Reparatur, HandBrake-Konsistenz, Encoder-Wahl (live bewiesen)
Ampel / ampel (push) Successful in 28s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:34:12 +02:00
Hitonabi dc1ceda0c2 fix(capabilities): Node-Zuordnung eindeutig bei mehreren Workern pro Host
Ampel / ampel (push) Successful in 28s
Laufen zwei Worker auf demselben Rechner (Befund 24.07.: alter + neuer
Windows-Worker auf TobisNicerPC), war die Node-Zuordnung mehrdeutig — der
Hostname-Match traf irgendeinen. Jetzt disambiguiert der Name-Teil des
Celery-Knotens (WORKER_NAME), sonst Fallback auf den ersten Host-Treffer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:33:22 +02:00
Hitonabi 2de0899ac6 fix(installer): install.ps1 ASCII-sauber (Gedankenstrich zerschoss das Skript)
Ampel / ampel (push) Successful in 28s
PowerShell 5.1 liest .ps1 als ANSI, nicht UTF-8. Der Gedankenstrich '—' in
einem Write-Host-String dekodierte zu 'â€"' — das eingebettete Anfuehrungs-
zeichen zerriss die Zeichenkette und der Parser scheiterte (unerwartetes
Token). Alle Nicht-ASCII-Zeichen durch ASCII ersetzt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:27:51 +02:00
Hitonabi 47d07d6168 fix(mounts): Reparatur crashte an makedirs + reachable-Check auf 3s begrenzt
Ampel / ampel (push) Successful in 28s
- mounten(): FileExistsError von os.makedirs abgefangen und ismount OSError
  toleriert — ein toter Mount taeuschte isdir, die Reparatur brach ab.
- ist_erreichbar(): 'timeout 3 ls' statt os.listdir — der direkte Zugriff
  blockierte sonst ~10s bis zum CIFS-Timeout (traege Speicherziele-Seite).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:23:12 +02:00
Hitonabi 4ba02047db Drei Praxis-Bugs: tote NAS-Mounts reparierbar, HandBrake-Versionen konsistent, Encoder-Wahl beim Rip
Ampel / ampel (push) Successful in 28s
1) Speicher-Mounts robust (Befund: toter CIFS-Mount nach NAS-Ausfall/Rebuild —
   mounted:false, verschwand aus 'Verfuegbare Ziele', Neu-Anlegen -> 409, man
   sass fest):
   - mounts.py: ist_erreichbar() (listdir, soft-Mount bricht schnell ab),
     ist_gemountet() faengt OSError toter Mounts, aushaengen() mit
     umount -l Fallback, reparieren() (lazy abhaengen + frisch mounten).
   - /storage-mounts liefert 'reachable'; POST bei existierendem Namen:
     aktiv -> 409, tot -> automatische Reparatur mit neuen Angaben;
     neuer POST /storage-mounts/{name}/repair (gespeicherte Zugangsdaten).
   - /storage-targets crasht nicht mehr an totem Mount (os.path.ismount
     OSError abgefangen).
   - UI: eigene 'Netzwerk-Mounts'-Liste mit Status (aktiv/nicht erreichbar/
     getrennt) + Reparieren- und Entfernen-Knopf — tote Mounts sind sichtbar
     und wiederherstellbar statt zu verschwinden.

2) HandBrake-Versionen konsistent (Befund: Docker 1.6.1, Windows-Skript
   fest 1.9.2, Update-Check meldet 1.11.2 — verwirrend):
   - Windows-Installer zieht jetzt DYNAMISCH die neueste Version (GitHub
     latest, Fallback 1.11.2) — passt zum Update-Check.
   - Update-UI erklaert klar: Docker = stabiles Debian-Paket (bewusst aelter,
     kein Fehler), Windows = neueste. MakeMKV-Update zeigt den Befehl.

3) Encoder-/Worker-Auswahl beim Rip (Feature):
   - Celery worker_direct=True: jeder Worker konsumiert zusaetzlich seine
     Direkt-Queue. API-Helper transcode_queue(node) routet gezielt an den
     gewaehlten Worker, faellt aber sicher auf die geteilte transcode-Queue
     zurueck, wenn er offline ist (kein Haengenbleiben).
   - /capabilities liefert den Celery-Node je Worker; POST /jobs nimmt
     transcode_node (-> Job-meta); rip_disc + retry-transcode routen danach.
   - Rip-Dialog: Encoder-/Worker-Dropdown, sichtbar ab 2 Online-Workern.
   - ping_worker-Task zum Verifizieren des gezielten Routings.
   - Nebenfund gefixt: DeviceDiscovery leitete die Titel-Auswahl (titles)
     gar nicht an die API weiter — jetzt titles + transcode_node.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 18:19:57 +02:00
Hitonabi fe1387b1ec fix(ui): typLabel in DeviceDiscovery definiert (Laufwerks-Details-Button gefixt) & Settings Tab-Wechsel robuster gemacht
Ampel / ampel (push) Successful in 28s
2026-07-24 17:04:18 +02:00
Hitonabi 67a4a76afa fix(ui): Worker-Adressfeld nicht mehr mit window.location vorbelegen
Ampel / ampel (push) Successful in 28s
Befund (Commander, Screenshot): Das Feld 'Rippy-Adresse' fuellte sich
automatisch mit window.location.hostname — also der Adresse, ueber die man
das UI geoeffnet hat (bei ihm die eigene PC-IP hinter einem Forward/Proxy,
192.168.178.98). Das ist NICHT zwingend die direkte LAN-IP der
Rippy-Maschine, die der Worker fuer Redis/Postgres (6379/5432) braucht —
der generierte Befehl zeigte damit auf den falschen Host ('Verbindung
verweigert').

Fix:
- Kein Auto-Default mehr (Feld leer); leeres Feld -> sichtbarer
  Platzhalter <RIPPY-IP> im Befehl (erkennbar unvollstaendig statt still
  falsch).
- Label auf 'LAN-IP der Rippy-Maschine (Docker-Host)' geschaerft +
  dauerhaft sichtbarer Hilfetext: die IP der Rippy-Maschine, NICHT dieses
  PCs, und auch dann die direkte LAN-IP wenn das UI ueber Domain/Proxy
  geoeffnet wurde.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:53:12 +02:00
Hitonabi b130eeb967 feat(ui): Master-Panel 'Ripping Operations' DIREKT unter Laufwerke verschoben
Ampel / ampel (push) Successful in 28s
2026-07-24 16:44:33 +02:00
Hitonabi bf80f9dfd4 refactor(ui): Current Job, Ripping Queue und Job-Historie in ein einziges Master-Panel bündig unter Laufwerke zusammengeführt
Ampel / ampel (push) Successful in 27s
2026-07-24 16:43:10 +02:00
Hitonabi e0864828c2 style(ui): Ripping Queue Kachel per flex-1 vertikal gestreckt fuer exakt buendigen unteren Abschluss mit Recently Ripped
Ampel / ampel (push) Successful in 28s
2026-07-24 16:41:26 +02:00
Hitonabi 60abf42359 feat(ui): Laufwerke & im Laufwerk erkannte Disc nach ganz oben verschoben, Ripping Queue exakt nach SC1 aufgebaut
Ampel / ampel (push) Successful in 28s
2026-07-24 16:39:56 +02:00
Hitonabi e18315814c feat(ui): 5-Kachel-Banner ohne schwarze Loecher gefuellt, Vollstaendige Job-Tabelle mit TMDB-Beschreibungen wiederhergestellt
Ampel / ampel (push) Successful in 28s
2026-07-24 16:36:12 +02:00
Hitonabi 9b19b74d39 fix(ui): 100% Dark Mode erzwungen (White Mode entfernt), Echte API-Daten fuer Server Status gebunden & Tabs-Breite/Scrollbalken behoben
Ampel / ampel (push) Successful in 28s
2026-07-24 16:33:50 +02:00
Hitonabi 9bc8ad11b2 fix(ui): Zentrierung fuer Anleitung & Einstellungen gefixt, zufaellige Mockup-Filme durch echte API-Daten ersetzt
Ampel / ampel (push) Successful in 28s
2026-07-24 16:31:45 +02:00
Hitonabi 0f20d9d8f4 feat(ui): Rebuild Layout EXACTLY 1:1 matching SC1 Mockup (Top Header Nav, 5-Panel Movie Wall, 2-Column Dashboard Grid with Sparkline & Recently Ripped)
Ampel / ampel (push) Successful in 28s
2026-07-24 16:25:21 +02:00
Hitonabi f0a8455fc2 style(ui): Visuelle Hero Showcase Banner & Poster Cards Grid auf dem Dashboard
Ampel / ampel (push) Successful in 28s
2026-07-24 16:22:40 +02:00
Hitonabi 3643f74eb1 style(ui): Visuelles High-End Overhaul fuer Cinematic Cinema OS (3D Spin-Disc, Hero Banner, Radial Gauges & Poster Grid)
Ampel / ampel (push) Successful in 27s
2026-07-24 16:18:58 +02:00
Hitonabi b8162753a9 chore: Single Source of Truth = main (stable-Branch + Gruen-Gate abgeschafft)
Ampel / ampel (push) Successful in 27s
Commander-Entscheid 24.07.: nur noch EIN Branch. Der stable-Zwischenbranch
war vestigial — die VM deployt ohnehin aus main (git pull), das Gruen-Gate
hat den Live-Deploy nie real gegated.

- ci.yml: Beförderungs-Schritt (push -> stable) entfernt; die Ampel prueft
  nur noch (Ruff/pytest/Vite-Build), Rot heisst weiterhin: nicht deployen.
- deploy.sh: klont/resettet auf main statt stable.
- AGENTS §C, README (Entwicklung), DESIGN-2.0-Briefing: auf main-only
  umgeschrieben. Design-2.0-Briefing als ERLEDIGT markiert.
- SAVEPOINT v3.6.

Branches stable / design-2.0 / kernumbau-2026-07-23 werden nach diesem
Push geloescht (Inhalte vollstaendig in main).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 16:13:10 +02:00
Hitonabi bd00d4c728 refactor(ui): Dashboard, DeviceDiscovery & Settings auf Cinematic Cinema OS umgestellt
Ampel / ampel (push) Successful in 27s
2026-07-24 16:04:50 +02:00
Hitonabi 90b708302e refactor(ui): Modals & Unterseiten auf Primitives & dark: umgestellt 2026-07-24 16:04:46 +02:00
Hitonabi 7004d798d6 feat(ui): Primitives & Design Tokens fuer Cinematic Cinema OS 2026-07-24 16:04:41 +02:00
Hitonabi 7b6f836475 docs: Design-2.0-Briefing zur Uebergabe an Gemini + ROADMAP-Zeiger
Ampel / ampel (push) Successful in 27s
Vollstaendige Auftragsbeschreibung fuer den UI-Umbau: 428 theme-Ternaries
-> Tailwind-dark: + UI-Primitives, Ist-Zustand vermessen (Datei-Inventar
mit Ternary-Zahlen), harte Regeln (nur UI, Funktion 1:1, Deutsch, Ampel
gruen), empfohlener Weg, Definition of Done, Copy-Paste-Startprompt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:56:44 +02:00
Hitonabi fdbe1748e5 docs: SAVEPOINT — Windows-Worker Tray + Deinstallation E2E bewiesen
Ampel / ampel (push) Successful in 27s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:48:53 +02:00
Hitonabi 404a002d5c Windows-Worker: Tray-Symbol + rueckstandsfreie Deinstallation
Ampel / ampel (push) Successful in 27s
- tray.py (nur nativer Windows-Worker; Docker unberuehrt): pystray+Pillow-
  Tray neben der Uhr — Status, Worker starten/stoppen, Rippy oeffnen,
  Log anzeigen, Beenden (stoppt den Worker mit). Celery laeuft als
  Kind-Prozess ohne Konsolenfenster (CREATE_NO_WINDOW), Log in worker.log.
- install.ps1 erzeugt jetzt start-tray.bat (pythonw, empfohlen),
  start-worker.bat (Konsole/Debug) und uninstall.ps1: stoppt alle
  Prozesse aus dem Ordner, loescht die Autostart-Aufgabe, meldet den
  Worker per DELETE /workers/{name} in Rippy ab und entfernt den Ordner
  komplett. -Autostart registriert die Tray-Variante (onlogon).
- UI-Beschreibung + Anleitung entsprechend ergaenzt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:44:29 +02:00
Hitonabi 3987c31b1e Update-Check im System-Tab, Worker-Adressfeld mit Heimnetz-Warnung, Save-Knopf raus aus dem Worker-Tab
Ampel / ampel (push) Successful in 28s
1. Update-Erkennung (Commander-Frage 'wie kriegen wir Updates mit?'):
   GET /system/updates vergleicht installierte Versionen (Worker-Info) mit
   makemkv.com (Versionsnummer der Download-Seite) und der HandBrake-
   GitHub-Release-API (releases/latest), 12 h gecacht. Einstellungen ->
   System: 'Auf Updates prüfen' + fertiger Update-Befehl bei MakeMKV.
   Selbst-Update gibt es bewusst NICHT (waere Docker-Socket-Zugriff);
   dafuer ist MAKEMKV_VERSION jetzt per .env uebersteuerbar — Update ohne
   Code-Aenderung. HandBrake-Abweichung wird als Info dargestellt
   (Debian-Paket hinkt der offiziellen Version bewusst hinterher).
2. Worker-Anbindung: editierbares Adressfeld statt stillschweigend
   window.location.hostname — Befund: ueber die externe Domain kopierte
   Befehle liefen in den Reverse-Proxy (401) und Worker koennen Redis/
   Postgres eh nur im Heimnetz erreichen. Amber-Warnung bei oeffentlich
   aussehender Adresse, Anleitung ergaenzt.
3. 'Einstellungen speichern' ist im Worker-Tab ausgeblendet — dort gibt
   es nichts zu speichern, der Knopf verwirrte nur.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:39:20 +02:00
Hitonabi 76b4286ad3 Windows-Worker: Pfad-Mapping fuer echte Transcodes + leere-Titel-Meldung + E2E-Beweis dokumentiert
Ampel / ampel (push) Successful in 27s
- pfad_lokal (tasks.py, mit Tests): RIPPY_PATH_MAP uebersetzt
  Container-Pfade (/app/temp, /app/media) auf Netzlaufwerke des nativen
  Workers — ohne Mapping unveraendert (Docker-Worker).
- Rip-Dialog: 'done' mit 0 Titeln bekommt Klartext (Disc vermutlich
  nicht entschluesselbar) statt leerer Tabelle.
- SAVEPOINT: E2E-Beweis des nativen Windows-Workers dokumentiert
  (test-windows-nativ online, HandBrake 1.9.2, echte LAN-IP).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:29:56 +02:00