feat(ui): die Oberflaeche weiss jetzt, worauf sie laeuft

Commander-Befund 28.08.2026, zum Windows-Fenster:

    Worker erreichbar:   0 von 1
    Kein Worker antwortet — Pruefen: docker compose ps
    Container-Platte:    unbekannt
    Freigaben:           keine eingehaengt

Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Sein Urteil: "Du hast
ja quasi nur rippy genommen und die docker installation fuer Windows gebaut.
Das gilt fuer die ganze standalone version fuer Windows, auch fuer die
settings und die Anleitung usw."

## Die Ursache war nicht die Anzeige

Die naheliegende Reparatur waere ein `if (windows)` an dreissig Stellen
gewesen -- dieselbe Falle noch einmal, nur mit einer zweiten Sorte Vermutung.

Es fehlte etwas anderes: Das UI hat nie erfahren, worauf es laeuft. config.py
kennt das Profil seit V2-2, weitergegeben wurde es nie. Also hat das UI
angenommen.

Neu: GET /betrieb meldet FAEHIGKEITEN, keinen Modus-Namen.

    externe_worker        Gibt es andere Maschinen, die Jobs uebernehmen?
    freigaben_einhaengen  Kann Rippy Netzwerk-Freigaben selbst einhaengen?
    container_pfade       Sind Pfade wie /app/media ueberhaupt gemeint?
    werkzeuge_verwalten   Kann Rippy MakeMKV/HandBrake selbst beschaffen?

Ein Modus-Name wuerde das UI zwingen, aus einem Namen auf Verhalten zu
schliessen -- und das bricht beim naechsten Betriebsfall: Ein
Docker-All-in-One hat Container-Pfade, aber keinen zweiten Worker.

## Was sich sichtbar aendert (auf Windows nachgemessen)

    Server-Status  "Rippy arbeitet: auf diesem Rechner" statt Worker-Zaehler
                   "Platz fuer Rippy: 59.2 von 232 GB frei" statt "unbekannt"
                   Freigaben-Block faellt weg
    Einstellungen  kein Reiter "Worker"; Speicherziele BLEIBT (dort steht die
                   Ablage -- "wo will ich das hinspeichern" war die Frage),
                   aber mit Pfadfeld statt Container-Auswahl und ohne die
                   Maske zum Einhaengen
    Anleitung      kein docker, kein Encoding-Worker-Abschnitt, stattdessen
                   der echte Ordner (C:\Users\...\Videos\Rippy)

## Zwei Fehler, die dabei aufgefallen sind

* MEDIA_ROOT = "/app/media" war in main.py fest verdrahtet. shutil.disk_usage
  warf unter Windows, die Liste blieb leer -- daher "unbekannt", obwohl auf
  dem Laufwerk 59 GB frei waren. Eine Nichtauskunft, die wie eine Auskunft
  aussieht. platz_orte() liefert die Orte jetzt je Betrieb, und ein noch
  nicht angelegter Ordner faellt auf das naechste vorhandene Elternteil
  zurueck.
* Der SSE-Schnappschuss enthielt den Server-Zustand NICHT, und der Waechter
  schickt ihn nur alle 15 Sekunden. Nach jedem Neuladen stand deshalb bis zu
  eine Viertelminute "unbekannt" da. Jetzt ist er im Schnappschuss, und das
  UI uebernimmt ihn auch von dort.

Dazu: der Windows-Skip in test_api_smoke.py ist weg. Er stammte aus der Zeit
vor V2-4, als main.py fcntl brauchte; seit der Treiberwahl ueber den Port
laedt es auf beiden Plattformen (57 Routen, gemessen). Damit laufen 20 Tests
mehr auch lokal statt nur auf der Ampel.

Ampel lokal: 654 gruen, ruff sauber. Docker-Zweig durch Unit-Tests gedeckt,
am echten Container noch nicht gegengeprueft -- das kommt beim Deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-08-28 13:45:00 +02:00
co-authored by Claude Opus 5
parent a5e64f8e56
commit 66642b8d93
10 changed files with 899 additions and 53 deletions
+101 -8
View File
@@ -453,9 +453,45 @@ async def _snapshot() -> dict:
# „konnte nicht nachsehen" ist etwas anderes als „es gibt keine".
# Deshalb None und nicht [] — das UI behält dann seinen alten Stand.
zustand["devices"] = None
# ⚠️ Der Server-Zustand gehört MIT in den Schnappschuss (Befund 28.08.2026).
#
# Er fehlte, und der Wächter schickt ihn nur alle 15 Sekunden. Ein frisch
# geladenes Dashboard stand deshalb bis zu einer Viertelminute auf
# „Platz für Rippy: unbekannt" — und das sieht aus wie eine Auskunft,
# obwohl es keine ist. Genau das hat der Commander im Windows-Fenster
# gesehen.
#
# Mit Zeitgrenze, aus demselben Grund wie bei den Laufwerken: `disk_usage`
# auf einer toten Netzfreigabe hängt im Kernel.
try:
zustand.update(await asyncio.wait_for(system_stand_fuer_snapshot(),
timeout=5))
except Exception: # noqa: BLE001
# Bewusst breit und bewusst still: Der Schnappschuss ist die Grundlage
# fuer ALLES im UI. Lieber ohne Platzangabe (das UI zeigt dann seinen
# alten Stand) als gar kein Schnappschuss.
pass
return zustand
async def system_stand_fuer_snapshot() -> dict:
"""Platz, Worker und Ablageziele — dieselben Felder wie im Wächter-Takt.
Getrennte Funktion, damit `_snapshot` und der Wächter dasselbe liefern.
Zwei Wege, die dieselbe Kachel füllen, aber verschiedene Felder schicken,
wären eine Anzeige, die je nach Zeitpunkt anders aussieht.
"""
info, ablagen = await asyncio.gather(
system_info(), storage_targets(), return_exceptions=True)
stand = {}
if not isinstance(info, Exception):
stand["info"] = info
if not isinstance(ablagen, Exception):
stand["ablagen"] = ablagen
return stand
def _sse_rahmen(ereignis: dict) -> str:
"""Ein Ereignis im SSE-Format. `id:` ist die Basis der Wiederaufnahme."""
nutzlast = json.dumps(ereignis, ensure_ascii=False)
@@ -1846,6 +1882,45 @@ async def browse_mkdir(request: MkdirRequest):
# ehrlich beantworten.
@app.get("/betrieb")
async def betrieb_auskunft():
"""In welchem Betrieb laeuft Rippy — und was kann dieser Betrieb?
## Warum es diese Route gibt (Commander-Befund 28.08.2026)
Im Windows-Fenster stand auf der Server-Status-Kachel:
Worker erreichbar: 0 von 1
Kein Worker antwortet — Pruefen: docker compose ps
Container-Platte: unbekannt
Freigaben: keine eingehaengt
Kein Satz davon ergibt auf einem Windows-PC einen Sinn. Es gibt keinen
Container, kein `docker compose`, keinen zweiten Worker — Rippy rippt
dort selbst.
Die Ursache war nicht die Anzeige, sondern was ihr fehlte: **Das UI hat
nie erfahren, worauf es laeuft.** `config.py` kennt das Profil seit
V2-2, weitergegeben wurde es nie. Also hat das UI angenommen.
Geantwortet wird mit FAEHIGKEITEN, nicht mit einem Modus-Namen — sonst
muesste die Oberflaeche aus einem Namen auf Verhalten schliessen, und
das bricht beim naechsten Betriebsfall. Begruendung vollstaendig in
`src/rippy/betrieb.py`.
"""
from rippy import betrieb as betriebs_auskunft
from rippy import config as rippy_config
try:
werte = rippy_config.laden()
except Exception: # noqa: BLE001
# Eine unlesbare Konfiguration darf die Oberflaeche nicht lahmlegen —
# sie bekommt dann die Vorgaben, und die stimmen fuer den haeufigsten
# Fall. Der Fehler faellt an anderer Stelle laut auf.
werte = {}
return betriebs_auskunft.auskunft(werte)
@app.get("/system/werkzeuge")
async def system_werkzeuge():
"""Was ist installiert, wo, in welcher Fassung — und gibt es Neueres?
@@ -2119,17 +2194,35 @@ async def system_info():
Worker, freier Platz auf Media- und Arbeits-Volume, MakeMKV-Key-Status."""
def sammle():
info = {"api_version": app.version, "plaetze": [], "workers": db.list_workers()}
for name, pfad in (("Media (/app/media)", MEDIA_ROOT),
("Arbeitsverzeichnis (/app/temp)", "/app/temp")):
# ⚠️ NICHT fest /app/media und /app/temp (Befund 28.08.2026).
#
# Beide Pfade gibt es unter Windows nicht. `shutil.disk_usage` warf,
# die Liste blieb leer, und im Windows-Fenster stand "Platz fuer
# Rippy: unbekannt" — obwohl auf dem Laufwerk natuerlich Platz war.
# Eine Nichtauskunft, die wie eine Auskunft aussieht.
from rippy import betrieb as betriebs_auskunft
from rippy import config as rippy_config
try:
werte = rippy_config.laden()
except Exception: # noqa: BLE001
werte = {}
for ort in betriebs_auskunft.platz_orte(werte):
# Frisch installiert gibt es den Ablage-Ordner noch nicht. Dann
# das naechste vorhandene Elternverzeichnis messen: Der Nutzer
# will wissen, ob auf dem LAUFWERK Platz ist.
pfad = betriebs_auskunft.naechster_vorhandener(ort["pfad"])
if not pfad:
continue
try:
nutzung = shutil.disk_usage(pfad)
info["plaetze"].append({
"name": name,
"frei_gb": round(nutzung.free / 1024**3, 1),
"gesamt_gb": round(nutzung.total / 1024**3, 1),
})
except OSError:
pass
continue
info["plaetze"].append({
"name": ort["name"],
"frei_gb": round(nutzung.free / 1024**3, 1),
"gesamt_gb": round(nutzung.total / 1024**3, 1),
})
einstellungen = db.get_settings()
info["makemkv_key_ui"] = bool((einstellungen.get("makemkvAppKey") or "").strip())
info["webhook_gesetzt"] = bool((einstellungen.get("notificationWebhook") or "").strip())