fix(ui+api): das "Neuladen" war HTTP 429 - und "Neu" komprimierte immer
Ampel / ampel (push) Successful in 30s
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>
This commit is contained in:
@@ -54,6 +54,17 @@ from ripping import (
|
||||
|
||||
API_URL = os.getenv("API_URL", "http://api:8000")
|
||||
|
||||
# Phasen-Marke in den Job-Metadaten: war der RIP fertig, als es schiefging?
|
||||
#
|
||||
# Sobald `status = "failed"` in der Zeile steht, ist die Phase sonst
|
||||
# unwiederbringlich fort — und genau die entscheidet, was danach hilft: Nach
|
||||
# einem toten Transcode genügt „Neu komprimieren"; nach einem toten Rip liegt
|
||||
# nur ein Bruchstück da (Vorfall 26.07.2026: 5,1 GB von rund 40 GB) und es muss
|
||||
# neu gerippt werden. Gelesen wird die Marke in api/phasen.py — der Name steht
|
||||
# in beiden Dateien und wird von test_phasen.py gegeneinander geprüft, weil es
|
||||
# kein geteiltes Paket zwischen den Containern gibt.
|
||||
RIP_FERTIG = "rip_fertig"
|
||||
|
||||
|
||||
def _transcode_queue(node: str):
|
||||
"""Ziel-Queue für die Kompression (siehe celery_client.transcode_queue):
|
||||
@@ -473,6 +484,9 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
||||
return {"status": "error", "error": fehler, "disc_type": disc_type}
|
||||
|
||||
db.update_job(job_id, status="running", disc_type=disc_type)
|
||||
# Ab hier gilt: der Rip läuft, ist aber NICHT fertig. Stirbt der Job jetzt,
|
||||
# ist jede Roh-Datei ein Bruchstück (siehe RIP_FERTIG oben).
|
||||
db.meta_merken(job_id, **{RIP_FERTIG: False})
|
||||
db.add_log("info", "worker", f"Job {job_id}: {disc_type}-Rip gestartet ({device_path})")
|
||||
|
||||
letzter = [-1]
|
||||
@@ -655,6 +669,9 @@ def rip_disc(self, device_path: str, job_id: str, target_dir: str = None):
|
||||
# freier Worker, inkl. Remote-GPU).
|
||||
ziel_queue = _transcode_queue(meta.get("transcode_node"))
|
||||
db.update_job(job_id, status="transcoding", progress=0)
|
||||
# Der Rip ist durch. Ab jetzt ist ein Fehlschlag mit „Neu komprimieren"
|
||||
# zu heilen, ohne die Disc noch einmal zu lesen.
|
||||
db.meta_merken(job_id, **{RIP_FERTIG: True})
|
||||
gezielt = meta.get("transcode_node") and ziel_queue != "transcode"
|
||||
db.add_log("info", "worker",
|
||||
f"Job {job_id}: Rip fertig, Kompression eingereiht"
|
||||
@@ -843,6 +860,10 @@ def transcode_files(self, job_id: str, raw_dir: str, final_dir: str):
|
||||
|
||||
os.makedirs(final_dir, exist_ok=True)
|
||||
db.update_job(job_id, status="transcoding", progress=0, error=None)
|
||||
# Wer hier ankommt, hat vollständige Quelldateien — sonst wäre oben schon
|
||||
# abgebrochen worden. Das vermerkt die Marke auch für BESTANDSJOBS, die vor
|
||||
# ihrer Einführung gerippt wurden und sie darum noch nicht tragen.
|
||||
db.meta_merken(job_id, **{RIP_FERTIG: True})
|
||||
db.add_log(
|
||||
"info", "worker",
|
||||
f"Job {job_id}: Kompression gestartet ({len(quellen)} Datei(en), "
|
||||
|
||||
Reference in New Issue
Block a user