fix(ui): Datum 1.1.1970, Disc nicht erkannt, kein Abbrechen — eine Ursache je Fall
Ampel / ampel (push) Successful in 1m16s
Ampel / ampel (push) Successful in 1m16s
Vier Befunde aus einem Bildschirmfoto vom 29.08.2026.
## 1. „das Datum ist falsch" — und der Typ auch
TYP „DISC" STARTZEIT „1.1.1970, 01:00:00" STATUS „running"
⚠️ Das war MEIN Fehler von heute Vormittag. Ich hatte `type`, `device` und
`startTime` in das Job-Ereignis aufgenommen — mit Feldnamen, **die es in der
Datenbankzeile nicht gibt**. Dort heissen sie `disc_type` und `created_at`;
die Uebersetzung macht `_job_row_to_model`, und sie bildet auch `running` auf
`processing` ab.
Die Folge war schlimmer als das Problem davor: Statt eines FEHLENDEN Feldes
kam ein LEERES. `new Date(null)` ist der 1.1.1970, und „DISC" war der
Rueckfall, den ich zwei Stunden vorher fuer genau diesen Fall eingebaut hatte.
Aus „offensichtlich kaputt" war „sieht plausibel aus" geworden.
Und mein Test hat es nicht gefunden, weil ich seine Beispielzeile selbst
erfunden habe — mit meinen falschen Feldnamen. Ein Test, der dieselbe Annahme
macht wie der Code, prueft nichts. Der neue geht von der ECHTEN Zeile aus.
Der Waechter bekommt jetzt dieselbe Uebersetzung eingespritzt, die auch
`/jobs` benutzt.
## 2. „Es ist eine Disk im laufwerk, aber er erkennt sie dort nicht"
Die Laufwerksliste wurde an DREI Stellen gebaut: `/devices` haengte die
erkannte Disc an, der Ereignis-Waechter und der SSE-Schnappschuss nicht. Das
Dashboard liest den Schnappschuss — und schloss aus der fehlenden Disc auf ein
leeres Laufwerk, waehrend ihr Titel eine Zeile weiter oben stand.
Jetzt gibt es `laufwerke_mit_disc()`, und ein Test zaehlt die Aufrufe von
`device_info` — bei zwei ist er rot.
## 3. „man kann einen rip garnicht abbrechen"
Dieselbe Ursache wie 1: Es gab genau EINEN Abbrechen-Knopf, in der Kachel
„laufender Job". Die erscheint nur bei Status `processing`; der Strom lieferte
`running`. Keine Kachel, kein Knopf, kein Weg zurueck — und aus demselben
Grund stand „Aktiv (0)", waehrend der Rip lief.
Zusaetzlich gibt es den Knopf jetzt in der Job-Zeile selbst, wo man ihn sucht.
## 4. „bei ‚Platz für rippy' sollte eher das arbeitsverzeichnis und der
## Ablagepfad sein"
Dort stand eine nackte Zahl fuer den ERSTEN Ort. Welcher Ordner das war, sah
man nicht — und die zweite Zeile fiel ganz weg, wenn beide auf derselben
Platte lagen. Das stimmt fuer die ZAHL und war der falsche Schluss fuer den
ORT. Jetzt stehen beide da, mit Pfad, plus der Hinweis, dass der Platz
geteilt wird.
Im Browser gegengeprueft: „Disc erkannt — wartet auf ‚Rippen starten'",
BLU-RAY mit richtigem Datum, beide Pfade, keine Konsolenfehler.
852 Tests gruen, ruff sauber.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b2acddbdfa
commit
7b0c41ddfe
+17
-7
@@ -233,14 +233,24 @@ def platz_orte(werte: dict, container: bool = None,
|
||||
ablage = ablage_vorgabe(werte, container, system)
|
||||
temp = arbeits_vorgabe(werte, container, system)
|
||||
orte = [{"name": "Ablage", "pfad": ablage}]
|
||||
# Nur wenn es ein ANDERES Laufwerk ist, lohnt eine zweite Zeile. Zweimal
|
||||
# dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskünfte und ist eine.
|
||||
# ## Warum beide Orte IMMER dastehen (Commander 29.08.2026)
|
||||
#
|
||||
# Ueber `pfade`, nicht ueber `os.path`: Auf dem Linux-Runner der Ampel
|
||||
# kannte `splitdrive` kein `D:` und hielt zwei verschiedene Laufwerke
|
||||
# fuer eines.
|
||||
if not pfade.gleiches_laufwerk(temp, ablage):
|
||||
orte.append({"name": "Arbeitsverzeichnis", "pfad": temp})
|
||||
# > „bei ‚Platz für rippy' sollte eher das arbeitsverzeichnis und der
|
||||
# > Ablagepfad sein."
|
||||
#
|
||||
# Hier stand vorher: zweite Zeile nur, wenn es ein ANDERES Laufwerk ist —
|
||||
# mit der Begründung, zweimal dieselbe Zahl unter zwei Namen sehe aus wie
|
||||
# zwei Auskünfte und sei eine. Das stimmt für die ZAHL und war der falsche
|
||||
# Schluss für den ORT: Auf seinem Bildschirm stand nur „Platz für Rippy:
|
||||
# 59,9 von 232 GB frei" — welche Ordner das sind, war nicht zu sehen.
|
||||
#
|
||||
# Beides gehört hin, und beides mit Pfad. Dass die Zahl dieselbe ist,
|
||||
# steht jetzt ausdrücklich dabei (`gleiches_laufwerk`), statt die Zeile
|
||||
# wegzulassen.
|
||||
gleich = pfade.gleiches_laufwerk(temp, ablage)
|
||||
orte.append({"name": "Arbeitsverzeichnis", "pfad": temp,
|
||||
"gleiches_laufwerk": gleich})
|
||||
orte[0]["gleiches_laufwerk"] = gleich
|
||||
return orte
|
||||
|
||||
|
||||
|
||||
@@ -301,13 +301,37 @@ def test_laufwerke_werden_seltener_abgefragt_als_jobs():
|
||||
|
||||
|
||||
def _db_zeile():
|
||||
"""Eine Job-Zeile, wie die API sie liefert (Felder aus /jobs)."""
|
||||
"""Eine Job-Zeile, NACHDEM `job_form` sie umgewandelt hat.
|
||||
|
||||
⚠️ Hier stand dieselbe Zeile mit denselben Feldnamen — nur habe ich sie
|
||||
mir ausgedacht. Die echte Datenbankzeile heisst `disc_type` und
|
||||
`created_at`, nicht `type` und `startTime`. Der Test hat meine Annahme
|
||||
also bestaetigt statt sie zu pruefen, und beim Commander stand danach
|
||||
„1.1.1970" in der Jobliste.
|
||||
|
||||
Der Waechter bekommt seit dem 29.08.2026 die UMGEWANDELTE Zeile — die
|
||||
Umwandlung selbst prueft `test_api_smoke.py`, wo sie wohnt.
|
||||
"""
|
||||
return {"id": "j1", "type": "bluray", "device": r"\\.\G:",
|
||||
"startTime": "2026-08-29T12:07:02", "endTime": None,
|
||||
"status": "processing", "progress": 12, "title": "Evangelion 2.22",
|
||||
"error": None, "meta": {"confidence": 0.3}}
|
||||
|
||||
|
||||
def test_ohne_umwandlung_kommt_die_rohe_zeile_NICHT_durch():
|
||||
"""Der Waechter darf DB-Feldnamen nicht weiterreichen.
|
||||
|
||||
Die rohe Zeile hat `disc_type`/`created_at`. Wer sie ungewandelt
|
||||
durchreicht, schickt dem UI lauter leere Pflichtfelder — und leer ist
|
||||
schlimmer als fehlend: `new Date(null)` ist der 1.1.1970, sieht also aus
|
||||
wie eine Auskunft.
|
||||
"""
|
||||
roh = {"id": "j1", "disc_type": "bluray", "status": "running",
|
||||
"created_at": "2026-08-29T12:07:02", "progress": 12}
|
||||
kurz = _job_kurz(roh)
|
||||
assert kurz["type"] is None and kurz["startTime"] is None
|
||||
|
||||
|
||||
def test_ein_job_ereignis_traegt_alles_was_die_oberflaeche_braucht():
|
||||
"""DER Waechter. Fehlt hier ein Feld, ist der Bildschirm beim Commander
|
||||
leer — ohne Fehlermeldung."""
|
||||
|
||||
@@ -106,6 +106,27 @@ def _job_kurz(zeile: dict) -> dict:
|
||||
Empfänger zum Raten.** `type` und `device` ändern sich über die Lebenszeit
|
||||
eines Jobs nie — sie kosten hier nichts und ersparen dem Empfänger die
|
||||
Frage, ob er gerade einen ganzen oder einen halben Job vor sich hat.
|
||||
|
||||
## ⚠️ NACHTRAG 29.08.2026 — beim ersten Anlauf falsch gemacht
|
||||
|
||||
Hier stand `zeile.get("startTime")` und `zeile.get("type")`. **Diese
|
||||
Felder gibt es in der Datenbankzeile nicht.** Sie heißen dort `created_at`
|
||||
und `disc_type`; die Übersetzung macht `_job_row_to_model` in `main.py`,
|
||||
und sie bildet auch `running` auf `processing` ab.
|
||||
|
||||
Die Folge war schlimmer als das Problem davor: Statt eines fehlenden
|
||||
Feldes kam ein leeres. Im Bildschirmfoto des Commanders stand
|
||||
|
||||
TYP „DISC" STARTZEIT „1.1.1970, 01:00:00" STATUS „running"
|
||||
|
||||
— `new Date(null)` ist der 1. Januar 1970, und „DISC" war der Rückfall,
|
||||
den ich zwei Stunden vorher für genau diesen Fall eingebaut hatte. Aus
|
||||
„offensichtlich kaputt" war „sieht plausibel aus" geworden.
|
||||
|
||||
Und mein Test hat es NICHT gefunden, weil ich seine Beispielzeile selbst
|
||||
geschrieben habe — mit meinen erfundenen Feldnamen. Ein Test, der dieselbe
|
||||
Annahme macht wie der Code, prüft nichts. Deshalb wird die Zeile jetzt
|
||||
zuerst durch dieselbe Umwandlung geschickt, die auch `/jobs` benutzt.
|
||||
"""
|
||||
return {
|
||||
"type": zeile.get("type"),
|
||||
@@ -214,7 +235,14 @@ class Waechter:
|
||||
|
||||
def __init__(self, store, bus, takt: float = TAKT_SEKUNDEN,
|
||||
laufwerke_lesen=None, laufwerks_takt: float = LAUFWERKS_TAKT_SEKUNDEN,
|
||||
system_lesen=None, system_takt: float = SYSTEM_TAKT_SEKUNDEN):
|
||||
system_lesen=None, system_takt: float = SYSTEM_TAKT_SEKUNDEN,
|
||||
job_form=None):
|
||||
# `job_form` uebersetzt eine DB-Zeile in die Form, die das UI kennt
|
||||
# (disc_type -> type, created_at -> startTime, running -> processing).
|
||||
# Eingespritzt, weil die Uebersetzung in main.py wohnt und dieses
|
||||
# Paket nichts von der API weiss. Ohne sie schickte der Waechter die
|
||||
# ROHE Zeile — mit Feldnamen, die es im UI nicht gibt (29.08.2026).
|
||||
self._job_form = job_form or (lambda z: z)
|
||||
self._store = store
|
||||
self._bus = bus
|
||||
self._takt = takt
|
||||
@@ -267,7 +295,8 @@ class Waechter:
|
||||
"""
|
||||
gesendet = 0
|
||||
|
||||
jetzt = {z["id"]: _job_kurz(z) for z in self._store.list_jobs(limit=100)}
|
||||
jetzt = {z["id"]: _job_kurz(self._job_form(z))
|
||||
for z in self._store.list_jobs(limit=100)}
|
||||
erster_lauf = not self._jobs and self._letzte_log_id is None
|
||||
if not erster_lauf:
|
||||
for typ, job_id, daten in unterschiede(self._jobs, jetzt):
|
||||
|
||||
@@ -120,13 +120,18 @@ def test_ohne_windows_gibt_es_keine_laufwerke():
|
||||
assert betrieb.windows_laufwerke(art=None, buchstaben=[]) == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("werte,container,erwartet", [
|
||||
({}, True, 2), # Media + /app/temp
|
||||
({"storage": {"medien": "D:\\M"}}, False, 1), # gleiches Laufwerk
|
||||
({"storage": {"medien": "D:\\M", "temp": "E:\\R"}}, False, 2),
|
||||
@pytest.mark.parametrize("werte,container", [
|
||||
({}, True), # Media + /app/temp
|
||||
({"storage": {"medien": "D:\\M"}}, False), # gleiches Laufwerk
|
||||
({"storage": {"medien": "D:\\M", "temp": "E:\\R"}}, False),
|
||||
])
|
||||
def test_platz_orte_zaehlen_nicht_doppelt(werte, container, erwartet):
|
||||
"""Zweimal dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskuenfte
|
||||
und ist eine."""
|
||||
def test_es_stehen_immer_beide_orte_da(werte, container):
|
||||
"""Ablage UND Arbeitsverzeichnis — in jedem Betrieb, mit Pfad.
|
||||
|
||||
Geaendert am 29.08.2026 auf Wunsch des Commanders: „bei ‚Platz für rippy'
|
||||
sollte eher das arbeitsverzeichnis und der Ablagepfad sein." Vorher fiel
|
||||
die zweite Zeile weg, wenn beide auf derselben Platte lagen.
|
||||
"""
|
||||
orte = betrieb.platz_orte(werte, container=container, plattform_name="win32")
|
||||
assert len(orte) == erwartet
|
||||
assert len(orte) == 2
|
||||
assert all(o["pfad"] for o in orte), orte
|
||||
|
||||
@@ -165,19 +165,45 @@ def test_docker_misst_weiter_die_container_pfade():
|
||||
assert [o["pfad"] for o in orte] == ["/app/media", "/app/temp"]
|
||||
|
||||
|
||||
def test_gleiches_laufwerk_wird_nicht_doppelt_gezaehlt():
|
||||
"""Zweimal dieselbe Zahl unter zwei Namen sieht aus wie zwei Auskuenfte
|
||||
und ist eine."""
|
||||
def test_beide_orte_stehen_da_MIT_pfad():
|
||||
"""Geaenderte Regel (Commander 29.08.2026).
|
||||
|
||||
> „bei ‚Platz für rippy' sollte eher das arbeitsverzeichnis und der
|
||||
> Ablagepfad sein."
|
||||
|
||||
Vorher fiel die zweite Zeile weg, wenn beide auf demselben Laufwerk
|
||||
lagen — mit der Begruendung, zweimal dieselbe Zahl sei eine Auskunft. Das
|
||||
stimmt fuer die ZAHL und war der falsche Schluss fuer den ORT: Auf seinem
|
||||
Bildschirm stand nur „Platz für Rippy: 59,9 von 232 GB frei", und welche
|
||||
Ordner gemeint waren, sah man nicht.
|
||||
"""
|
||||
orte = betrieb.platz_orte({"storage": {"medien": r"D:\Filme"}},
|
||||
container=False, plattform_name="win32")
|
||||
assert len(orte) == 1, orte
|
||||
assert [o["name"] for o in orte] == ["Ablage", "Arbeitsverzeichnis"]
|
||||
assert orte[0]["pfad"] == r"D:\Filme"
|
||||
assert orte[1]["pfad"] == r"D:\Filme\_arbeit"
|
||||
|
||||
|
||||
def test_zweites_laufwerk_bekommt_eine_eigene_zeile():
|
||||
def test_gleiches_laufwerk_wird_als_solches_GEMELDET():
|
||||
"""Statt die Zeile wegzulassen: ausdruecklich sagen, dass es dieselbe
|
||||
Platte ist. Sonst wirken zwei gleiche Zahlen wie ein Fehler."""
|
||||
orte = betrieb.platz_orte({"storage": {"medien": r"D:\Filme"}},
|
||||
container=False, plattform_name="win32")
|
||||
assert all(o["gleiches_laufwerk"] for o in orte)
|
||||
|
||||
|
||||
def test_zwei_laufwerke_werden_als_getrennt_gemeldet():
|
||||
orte = betrieb.platz_orte(
|
||||
{"storage": {"medien": r"D:\Filme", "temp": r"E:\Arbeit"}},
|
||||
container=False, plattform_name="win32")
|
||||
assert len(orte) == 2, orte
|
||||
assert not any(o["gleiches_laufwerk"] for o in orte)
|
||||
|
||||
|
||||
def test_im_container_bleibt_es_bei_den_zwei_container_orten():
|
||||
"""Der Docker-Weg darf sich durch die Aenderung NICHT verschieben."""
|
||||
orte = betrieb.platz_orte({}, container=True, plattform_name="linux")
|
||||
assert [o["pfad"] for o in orte] == ["/app/media", "/app/temp"]
|
||||
|
||||
|
||||
def test_noch_nicht_angelegter_ordner_faellt_auf_das_laufwerk_zurueck():
|
||||
|
||||
Reference in New Issue
Block a user