+ {/*
+ Jeder Ort mit seinem PFAD (Commander 29.08.2026): „bei
+ ‚Platz für rippy' sollte eher das arbeitsverzeichnis und
+ der Ablagepfad sein." Vorher stand hier eine nackte Zahl
+ für den ERSTEN Ort — welcher Ordner das war, sah man nicht.
+ */}
+ {!betrieb.kann.container_pfade && (systemInfo?.plaetze || []).map(ort => (
+
+
+
+
+ {ort.name}:
+
+ 0 && ort.frei_gb < 60
+ ? 'text-amber-400 font-bold' : 'text-slate-200'}>
+ {ort.frei_gb > 0 ? `${ort.frei_gb} von ${ort.gesamt_gb} GB frei` : 'unbekannt'}
+
+
+ {ort.pfad && (
+
+ {ort.pfad}
+
+ )}
+
+ ))}
+ {!betrieb.kann.container_pfade
+ && (systemInfo?.plaetze || []).length > 1
+ && systemInfo?.plaetze?.every(o => o.gleiches_laufwerk) && (
+ /* Ehrlich sagen, dass die Zahl zweimal dieselbe ist —
+ statt die zweite Zeile wegzulassen. */
+
+ Beide auf demselben Laufwerk — der Platz wird geteilt.
+
+ )}
+
- {betrieb.kann.container_pfade ? 'Container-Platte:' : 'Platz für Rippy:'}
+ Container-Platte:
0 && freiGb < 60 ? 'text-amber-400 font-bold' : 'text-slate-200'}>
{freiGb > 0 ? `${freiGb} von ${gesamtGb} GB frei` : 'unbekannt'}
diff --git a/src/rippy/betrieb.py b/src/rippy/betrieb.py
index 84ed97c..442d45d 100644
--- a/src/rippy/betrieb.py
+++ b/src/rippy/betrieb.py
@@ -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
diff --git a/src/rippy/bus/test_waechter.py b/src/rippy/bus/test_waechter.py
index 1f6a769..1f5d047 100644
--- a/src/rippy/bus/test_waechter.py
+++ b/src/rippy/bus/test_waechter.py
@@ -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."""
diff --git a/src/rippy/bus/waechter.py b/src/rippy/bus/waechter.py
index 652317a..f31103a 100644
--- a/src/rippy/bus/waechter.py
+++ b/src/rippy/bus/waechter.py
@@ -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):
diff --git a/src/rippy/test_arbeitsort.py b/src/rippy/test_arbeitsort.py
index 830ee20..99bf0cb 100644
--- a/src/rippy/test_arbeitsort.py
+++ b/src/rippy/test_arbeitsort.py
@@ -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
diff --git a/src/rippy/test_betrieb.py b/src/rippy/test_betrieb.py
index e379617..472f984 100644
--- a/src/rippy/test_betrieb.py
+++ b/src/rippy/test_betrieb.py
@@ -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():