Commit Graph
308 Commits
Author SHA1 Message Date
HitonabiandClaude Opus 5 288f9eeb8d feat(tools): Werkzeuge finden, holen und aktuell halten — Grundlage fuer Windows
Ampel / ampel (push) Failing after 47s
WAS: rippy/tools/ mit katalog.py (finden + Version) und beschaffen.py
(herunterladen, installieren, Update-Stand). ripping.py und caps.py fragen
den Katalog statt shutil.which.

DER BEFUND, DER DAS NOETIG MACHT: Der Bestand suchte AUSSCHLIESSLICH mit
shutil.which(). Im Container stimmt das — dort liegen beide Werkzeuge in
/usr/local/bin. Unter Windows findet es NICHTS, auch wenn alles installiert
ist: Windows-Programme liegen in Program Files, nicht im PATH. Am
28.08.2026 auf dem Commander-PC nachgesehen — MakeMKV lag unter
"C:\Program Files (x86)\MakeMKV\makemkvcon64.exe", which sah es nicht.

Fuer einen Rippy, der unter Windows eigenstaendig arbeiten soll, ist das der
Unterschied zwischen "laeuft" und "laeuft nicht". Vorher/nachher gemessen:

  vorher   check_makemkv_installed() -> False   (obwohl installiert)
  nachher  check_makemkv_installed() -> True
           build_makemkv_cmd()[0]    -> C:\Program Files (x86)\MakeMKV\makemkvcon64.exe

SUCHREIHENFOLGE, mit Begruendung im Modul: eingestellt > Rippys eigener
Werkzeug-Ordner > PATH > bekannte Installationsorte > Uninstall-Zweig der
Registry. Rippys eigener Ordner steht VOR dem System, damit eine selbst
gepflegte Fassung eine alte Systeminstallation schlaegt.

VERSION: HandBrakeCLI kann --version. makemkvcon KANN DAS NICHT (usage.txt
kennt keinen solchen Schalter) — die Version kommt aus dem Uninstall-Zweig.
Dort steht bei MakeMKV zwar eine Version, aber InstallLocation ist LEER
(nachgesehen); deshalb wird der Ordner ersatzweise aus DisplayIcon bzw.
UninstallString abgeleitet. Der Test dafuer hat sofort einen Fehler
gefunden: '"C:\...\uninstall.exe" /S' liess sich mit einem blossen
strip('"') nicht aufloesen.

BESCHAFFUNG, an echter Hardware gemessen:
  HandBrake  GitHub-Release-API -> 1.11.2, HandBrakeCLI-1.11.2-win-x86_64.zip
             geladen, entpackt, gestartet: meldet sich als 1.11.2. 2,8 s.
  MakeMKV    makemkv.com antwortete mit HTTP 525 (Cloudflare) — auch mit
             Browser-Kennung. Dieselbe Sperre, die im Projekt schon den
             Docker-Bau lahmlegt. Deshalb: Abruf wird versucht, ein
             Fehlschlag wird im Klartext gemeldet, und eine selbst geholte
             Datei laesst sich weiterhin verwenden. Ein Update-Server, der
             heute antwortet, darf keine Startbedingung fuer morgen sein.

DREI FALLEN, GEGEN DIE ES TESTS GIBT:
- Die .sig-Datei liegt im Release direkt neben dem ZIP und ist 566 Bytes
  gross. Wer nur nach "win" filtert, laedt sie.
- "1.9.2" ist als Zeichenkette GROESSER als "1.11.2". Ein Update-Hinweis, der
  ab Version zehn dauerhaft in die Irre fuehrt — und genau dort ist
  HandBrake gerade.
- Eine Fehlerseite kommt oft mit HTTP 200. Sie darf keine funktionierende
  Installation ersetzen: erst laden, pruefen, dann tauschen.

Und: Ist die neueste Version unbekannt (Quelle tot), steht NICHT "Update
verfuegbar" da. Eine Nichtauskunft ist keine Aussage.

GEMESSEN: ruff sauber, 489 Tests gruen + 15 uebersprungen (vorher 456).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:12:21 +02:00
HitonabiandClaude Opus 5 059183c651 feat(windows): V2-4 fertig — RippySetup.exe, Taskmanager-Eintrag, Deinstallation
Ampel / ampel (push) Failing after 46s
WAS: Eine 28,6-MB-Datei, drei Betriebsarten. Doppelklick installiert,
`--dienst` laeuft im Hintergrund, `--deinstallieren` raeumt sich weg.
Dazu packaging/windows/build.py, das sie baut.

DIE ZWEI COMMANDER-WUENSCHE, GEMESSEN:

1. EINTRAG IM TASKMANAGER
     ProcessName   Rippy
     Beschreibung  Rippy — automatisches Ripping
     Produkt       Rippy
     Version       2.0.0
   Der Prozessname kommt vom Dateinamen (bei der Installation kopiert sich
   die Datei als Rippy.exe), die Beschreibung aus einer eingebetteten
   Versions-Ressource. Ohne die steht dort nichts, und Windows SmartScreen
   sieht eine EXE ohne Herausgeberangabe genauso misstrauisch wie ein Mensch.

2. EINTRAG IN PROGRAMME UND FEATURES
   Aus der Registry zurueckgelesen, nicht behauptet:
     DisplayName      Rippy
     DisplayVersion   2.0.0
     Publisher        Rippy
     InstallLocation  <Zielordner>
     UninstallString  "<pfad>\Rippy.exe" --deinstallieren
     EstimatedSize    29241  (KiB)
     NoModify/NoRepair 1
   HKCU statt HKLM: Der Eintrag erscheint genauso in "Apps & Features", die
   Installation laeuft aber ohne UAC-Abfrage durch.

VOLLER KREISLAUF GEMESSEN: installieren -> Rippy.exe + rippy.ico liegen da,
Registry-Eintrag da, Autostart da; Dienst starten -> nach 1 s bereit, alle
sieben Endpunkte HTTP 200, Oberflaeche kommt; deinstallieren -> nach ~4 s
sind Datei, Ordner, Registry-Eintrag, Autostart UND das Aufraeum-Skript weg.

VIER FEHLER, DIE NUR DIE FERTIGE EXE ZEIGT:

1. rippy/store baute die Engine BEIM IMPORT, mit PostgreSQL als Vorgabe.
   Im Windows-Paket gibt es psycopg2 bewusst nicht -> die EXE starb sofort.
   Die Engine entsteht jetzt beim ersten Zugriff. Und der Fehler war
   grundsaetzlicher als der fehlende Treiber: Ein Modul, das beim Import
   schon eine Verbindung aufbaut, laesst sich gar nicht mehr umstellen —
   store.verbinden() kaeme immer zu spaet.

2. celery_client baute den Broker-Client beim Import. Dasselbe Muster, und
   `from celery_client import celery_client` loeste es sogar dann aus, wenn
   nie ein Rip angestossen wird. Jetzt traege, mit KeinBroker als klarer
   Ansage statt eines Importfehlers.

3. Die Selbstloeschung beim Deinstallieren ging als EIN Argument an cmd.
   Python maskiert dabei die inneren Anfuehrungszeichen — cmd suchte einen
   Dateinamen mit Backslashes davor, fand nichts und meldete nichts (>nul).
   Registry und Autostart waren weg, die 28-MB-Datei blieb liegen: ein still
   scheiternder Hintergrundprozess, wie ihn AGENTS.md beschreibt. Jetzt ein
   Aufraeum-Skript, das 15-mal versucht (die Onefile-EXE laeuft als ZWEI
   Prozesse; der Starter haelt die Datei nach dem Ende noch offen).

4. deinstallieren() raeumte den STANDARD-Ordner auf statt des tatsaechlichen.
   Wer nach --ziel D:\Rippy installiert hatte, dessen Dateien waeren
   geblieben, waehrend ein fremder Ordner angefasst worden waere. Der Pfad
   steht in der Registry (InstallLocation) und wird jetzt dort gelesen.

WAS NOCH NICHT GEHT, ausdruecklich: Ein RIP laesst sich unter Windows noch
nicht anstossen — die Zustellung laeuft weiter ueber Celery. Die Umstellung
auf die LocalQueue ist V2-5. Oberflaeche, Laufwerks-Erkennung, Auswurf und
alles Lesende laufen.

GEMESSEN: ruff sauber, 456 Tests gruen + 15 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 11:00:39 +02:00
HitonabiandClaude Opus 5 e5254c23f0 feat(daemon): V2-4 (Teil 3) — rippyd laeuft unter Windows, ohne Docker
Ampel / ampel (push) Failing after 46s
WAS: `rippyd` startet Rippy als EINEN Prozess — SQLite statt Postgres,
LocalQueue statt Celery/Redis, In-Process-Bus statt Redis Pub/Sub, UI von
FastAPI statt von nginx. Dieselbe API, dasselbe UI, derselbe Rip-Code.

GEMESSEN (Windows 11, frische Python-3.12-Umgebung, ohne Docker/Postgres/Redis):

  bereit nach            1 s
  /api/health            200      /api/settings          200
  /api/devices           200      /api/logs              200
  /api/jobs              200      /api/capabilities      200
  /api/health/vorraete   200      /  (Weboberflaeche)    200

  Ereignis-Waechter      gesund: true, alter_sekunden: 0.5
  SSE-Strom              liefert `event: snapshot` mit vollem Zustand
  SQLite                 angelegt, WAL aktiv, alle Tabellen da

ZWEI FEHLER, DIE NUR DER ECHTE LAUF ZEIGT:

1. STARLETTE REICHT LIFESPAN NICHT AN MOUNT-UNTERANWENDUNGEN WEITER.
   Das UI ruft /api/..., im Docker-Betrieb entfernt der nginx das Praefix.
   Ohne nginx haengt die API als Unteranwendung unter /api — und ihr
   startup_event lief nie. Folge: db.init_db() nicht ausgefuehrt,
   "no such table: settings", HTTP 500 auf /jobs, /settings, /logs,
   /capabilities, und der SSE-Strom blieb stumm. /api/health antwortete
   trotzdem mit 200, weil die Route keine Datenbank braucht — der
   Fehlschlag sah also aus wie "laeuft". Behoben ueber einen eigenen
   lifespan, der den der Unteranwendung mit ausloest.

2. EIN print() HAT DEN GANZEN START UMGEBRACHT.
   UnicodeEncodeError: 'charmap' codec can't encode characters
   -> ERROR: Application startup failed. Exiting.
   Windows-Konsolen arbeiten mit cp1252; Rippys Meldungen sind deutsch und
   voller Umlaute und Warnzeichen. Das Bittere: Es war nicht die WARNUNG,
   die den Start verhindert hat, sondern der VERSUCH, sie auszugeben.
   Behoben mit encoding="utf-8" UND errors="replace" — eine Ausgabe darf
   unter keinen Umstaenden etwas abbrechen koennen.

BEFUND ZUR HARDWARE (kein Codefehler): Nach dem Auswurf-Test hat sich das
USB-Laufwerk vom Bus getrennt. Windows kennt es nur noch als Karteileiche
(Get-PnpDevice -Class CDROM -> Status "Unknown", Win32_CDROMDrive leer),
weder ein Laufwerksbuchstabe noch \.\CdRom0..4 sind da. Die leere
Geraeteliste ist damit die RICHTIGE Antwort. Bei bus-versorgten
USB-Laufwerken ist das bekannt — der Auswurf zieht kurz mehr Strom.

GEMESSEN: ruff sauber, 440 Tests gruen + 15 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:34:51 +02:00
HitonabiandClaude Opus 5 9d95c95d79 feat(drives): Windows-Treiber an echter Disc BEWIESEN
Ampel / ampel (push) Failing after 46s
WAS: Die Hardware-Schicht des Windows-Treibers ist nicht mehr nur gebaut,
sondern gemessen. Messwerte stehen im Modul-Docstring, der gemessene
BD-50-Wert als Testfall in test_cdrom.py.

GEMESSEN (28.08.2026, LG BU40N extern, Laufwerk G:, echte BD-50):

  drive_status()                 -> 4 (CDS_DISC_OK), sofort
  IOCTL_CDROM_DISK_TYPE          -> DATA_TRACK
  IOCTL_DISK_GET_LENGTH_INFO     -> 48 149 364 736 Bytes = 44,84 GiB
  disc_status()                  -> 101 (CDS_DATA_1)
  detect_disc_type()             -> 'bluray'
  device_info()['status']        -> 'ready'
  device_info()['type']          -> 'bluray'

  auswerfen_versuchen()          -> True, 2,41 s
  drive_status() davor / danach  -> 4 / 1

DIE LETZTE ZEILE IST DER EIGENTLICHE BEWEIS. Der Auswurf haengt nicht am
Rueckgabewert des Steuercodes, sondern am ZUSTAND davor und danach: Die
Disc war drin (4) und ist danach draussen (1). Genau das fehlte in v1
monatelang — dort quittierte das ioctl Erfolg, die Schublade blieb zu, und
im Log stand "Disc ausgeworfen".

Die 44,84 GiB pruefen die Schwelle gleich mit: Eine BD-50 liegt knapp unter
den 55 GiB, ab denen classify() auf 'uhd' geht, und wird korrekt als
'bluray' eingeordnet. Wer die Schwelle senkt, macht aus jeder BD-50 eine
UHD — und Rippy waehlte dann das falsche Kompressions-Preset. Der Testfall
haelt den gemessenen Wert fest.

Offen bleibt allein die Ereignis-Erkennung (WM_DEVICECHANGE) — die wird
erst gebaut.

Nebenbei: Beim Eintragen der Messwerte sind drei echte NUL-Bytes in die
Datei geraten (eine Escape-Ebene im Schreib-Skript verschluckt). Python
lehnt so eine Datei rundheraus ab ("source code string cannot contain null
bytes") — im Binaermodus repariert und gegengeprueft.

GEMESSEN: ruff sauber, 430 Tests gruen + 14 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 10:04:29 +02:00
HitonabiandClaude Opus 5 3d7f3b1680 feat(drives): V2-4 (Teil 2) — main.py laedt unter Windows, Treiber gemessen
Ampel / ampel (push) Failing after 46s
WAS: rippy/drives/__init__.py waehlt den Treiber fuer DIESE Maschine.
main.py und prescan.py fragen ihn, statt fest den Linux-Treiber zu
importieren. Damit ist der Blocker fuer den nativen Windows-Betrieb weg.

GEMESSEN, vorher (frische Python-3.12-Umgebung unter Windows):
  >>> import main
  ModuleNotFoundError: No module named 'fcntl'

GEMESSEN, nachher:
  main.py laedt unter Windows: JA
    Treiber:   rippy.drives.windows
    Routen:    61
    Laufwerke: ['\\.\G:']

Der Import stand in rippy/drives/linux.py -> detection.py -> fcntl. Genau
dafuer gibt es den Port: Der Aufrufer sagt WAS, nicht WIE. test_treiberwahl.py
haelt ausserdem fest, dass BEIDE Treiber denselben Satz Funktionen anbieten —
sonst faellt eine fehlende erst im Betrieb als AttributeError auf, und zwar
auf der anderen Plattform.

DER TREIBER AN ECHTER HARDWARE (LG BU40N extern, Laufwerk G:, LEER):
  GetDriveTypeW("G:\\")              -> 5 (DRIVE_CDROM)
  list_optical_devices()             -> ['\\.\G:']
  CreateFileW(r"\.\G:")             -> Handle 380
  CHECK_VERIFY2 bei leerem Laufwerk  -> ERROR_NOT_READY (21)
  drive_status()                     -> 1 (CDS_NO_DISC)
  detect_disc_type()                 -> 'no_disc'
  device_info()['status']            -> 'empty'
  MEDIA_REMOVAL (entriegeln)         -> Erfolg,   0,1 ms
  EJECT_MEDIA                        -> Erfolg, 664,8 ms

Die 664,8 ms sind der Beleg, dass wirklich etwas passiert ist: Ein
abgelehnter Steuercode kommt in rund 2 ms zurueck.

BEFUND, DER EIN PLACEBO VERHINDERT HAT: IOCTL_STORAGE_LOAD_MEDIA (Schublade
einziehen) antwortet auf diesem Laufwerk mit ERROR_INVALID_FUNCTION. Bei
flachen und externen Laufwerken ist das die Regel. Deshalb gibt es bewusst
KEINE einziehen()-Funktion — ein Knopf "Schublade schliessen", der auf der
Haelfte aller Laufwerke stillschweigend nichts tut, waere genau die Sorte
Placebo, die in Etappe 19 aufgeraeumt wurde.

Dazu eine DeprecationWarning behoben: '\G' im Docstring ist keine gueltige
Escape-Sequenz und waere in einer kuenftigen Python-Version ein harter Fehler.

NOCH NICHT gemessen: das Verhalten MIT eingelegter Disc (Typ-Erkennung ueber
die Groesse, und ein Auswurf, bei dem wirklich etwas drin ist).

GEMESSEN: ruff sauber, 429 Tests gruen + 13 uebersprungen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:55:15 +02:00
HitonabiandClaude Opus 5 b723fee3a2 feat(drives): V2-4 (Teil 1) — Windows-Laufwerkstreiber, ohne Hardware geprueft
Ampel / ampel (push) Successful in 45s
WAS: rippy/drives/windows.py mit denselben zwei Vertraegen wie die
Linux-Fassung (auswerfen_versuchen gibt False zurueck, eject wirft).
Dazu win_ioctl.py: die Win32-Steuercodes, HERGELEITET statt abgeschrieben.

WARUM ctypes und nicht pywin32: 20 MB zusaetzliche Abhaengigkeit, die in
jedes PyInstaller-Paket muss und bei jedem Python-Wechsel neu passen will.
Gebraucht werden fuenf Funktionen aus kernel32 — die spricht ctypes direkt an.

DIE STEUERCODES WERDEN AUSGERECHNET, NICHT ABGETIPPT: win_ioctl.py enthaelt
das CTL_CODE-Makro aus winioctl.h als Funktion; jede Konstante wird daraus
gebildet. test_win_ioctl.py rechnet das Ergebnis gegen die in der
Microsoft-Dokumentation stehenden Zahlen. Grund: Ein falscher IOCTL-Code
meldet kein "unbekannter Befehl", sondern ERROR_INVALID_FUNCTION — und das
liest sich wie "dieses Laufwerk kann das nicht". Man sucht dann am Geraet
statt an einer Zahl. AGENTS Regel D, ernst genommen.

DIE ZWEI PORT-REGELN GELTEN AUCH HIER:
- Auswerfen heisst entriegeln, auswerfen, NACHSEHEN. Unter Linux wurde am
  26.07.2026 gemessen, dass ein verriegeltes Laufwerk den Auswurf mit ERFOLG
  quittiert und nichts tut. IOCTL_STORAGE_EJECT_MEDIA meldet ebenfalls nur
  die Annahme des Befehls — also wird auch hier nachgesehen
  (CHECK_VERIFY2 muss ERROR_NOT_READY liefern).
- Ein unzugaengliches Laufwerk ist UNBEKANNT, nicht leer. device_info kennt
  deshalb drei Zustaende, nicht zwei.

EIN FEHLER, DEN DIE TESTS SOFORT GEFANGEN HABEN: Win32Fehler setzte
self.winerror VOR super().__init__(). Nachgemessen: Ein zweiargumentiges
OSError.__init__(code, text) setzt winerror wieder auf None. Damit erkannte
_medium_da ERROR_NOT_READY nicht mehr, hielt jedes leere Laufwerk fuer einen
echten Fehler und meldete "unbekannt" statt "leer". Vier Tests rot, Ursache
in einer Minute gemessen statt geraten.

⚠️ WAS HIER NICHT BEWIESEN IST: dass echte Hardware sich so verhaelt. Das
Laufwerk ist derzeit nirgends angeschlossen. Der Ablauf, die
Fehlerunterscheidung und die Typ-Zuordnung sind gegen ein nachgebautes
Laufwerk geprueft (51 Tests, laufen auf jeder Plattform) — die Hardware-
Schicht gilt bis zu einer Messung als GEBAUT, nicht als BEWIESEN.

GEMESSEN: ruff sauber, 400 Tests gruen + 4 uebersprungen (vorher 378).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:45:43 +02:00
HitonabiandClaude Opus 5 4337253797 docs: SAVEPOINT v4.0-beta — V2-0 bis V2-3 stehen, Laufwerk fehlt an der VM
Ampel / ampel (push) Successful in 41s
Vier Etappen gebaut und deployt, Polling ist weg (121 Anfragen/min je Tab
-> ~2 einmalige Abrufe), SSE-Strom live belegt.

Wichtig fuer die naechste Sitzung: Das optische Laufwerk haengt NICHT mehr
an der VM. Rippy laeuft als reine Komprimier-Maschine. Und der Vorfall,
der das aufgedeckt hat, ist dokumentiert — ein devices:-Eintrag in compose
ist eine Startbedingung, und die hat api, worker und ui stillgelegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:37:23 +02:00
HitonabiandClaude Opus 5 05ab655116 fix: ein fehlendes Laufwerk darf Rippy nicht am Starten hindern
Ampel / ampel (push) Successful in 40s
WAS: Die `devices:`-Eintraege fliegen aus docker-compose.yml. Was dieser
Host wirklich hat, ermittelt deploy/geraete-override.sh und schreibt es in
docker-compose.override.yml — die zieht Compose von allein dazu, der
Befehl bleibt `docker compose up -d`.

WARUM (Vorfall heute, 28.08.2026): Das USB-Laufwerk haengt nicht mehr an
der VM. Ein ganz normaler Deploy legte daraufhin api, worker UND ui still:

  Error response from daemon: error gathering device information while
  adding custom device "/dev/sr0": no such file or directory

Ein `devices:`-Eintrag ist eine STARTBEDINGUNG — und keiner der drei
Container braucht zum STARTEN ein Laufwerk. Rippy war unten, und die
Meldung stand mitten im Build-Rauschen (dieselbe Klasse wie das geschluckte
`|| echo` beim .env-Kopieren am 26.07.).

DER WIDERSPRUCH WAR AELTER ALS DER VORFALL: install.sh sagt bei fehlendem
Laufwerk ausdruecklich "die Installation laeuft trotzdem durch; diese
Maschine dient dann als reine KOMPRIMIER-Maschine" — die Compose-Datei sah
das anders. Eine Maschine ohne Laufwerk ist ein VORGESEHENER Betriebsfall:
der Windows-Worker und der GPU-Encoder sind genau das.

Das Skript uebernimmt die Erkennung aus install.sh unveraendert: sr- und
sg-Knoten ueber die SCSI-Adresse in /sys abgeglichen, nicht geraten. Und
es schreibt die Override AUCH dann, wenn kein Laufwerk da ist — sonst
bliebe eine alte Datei mit /dev/sr0 liegen und der Fehler waere derselbe,
nur schwerer zu finden, weil sie gitignored ist und in keinem Diff auftaucht.

deploy.sh ruft es vor dem Start auf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:35:48 +02:00
HitonabiandClaude Opus 5 95705c8d88 feat(ui): V2-3 (Teil 2) — das UI haengt am Ereignis-Strom, Taktgeber raus
Ampel / ampel (push) Successful in 47s
WAS: EventStreamProvider haelt EINE SSE-Verbindung fuer die ganze
Anwendung. Dashboard, Log-Kasten, Log-Seite, Laufwerksliste und
Worker-Liste beziehen ihren Zustand daraus. Sieben von neun setInterval
sind weg.

GEMESSEN AN DEN TAKTGEBERN, je offenem Tab:
  Dashboard      5 Endpunkte / 4 s   75/min  ->  0
  Log-Kasten     2 Endpunkte / 5 s   24/min  ->  0
  Laufwerke      1 Endpunkt  / 5 s   12/min  ->  0
  Log-Seite      1 Endpunkt  /10 s    6/min  ->  0 (+1 Abruf beim Oeffnen)
  Worker-Liste   1 Endpunkt  /15 s    4/min  ->  0 (+1 Abruf beim Oeffnen)
                                     ------
                                     121/min ->  ~2 einmalige Abrufe

Zwei Taktgeber bleiben bewusst: FirstRunWizard (laeuft nur VOR der
Einrichtung) und RipTargetModal (nur solange der Dialog offen ist).

DIE REGEL IST UMGEZOGEN, NICHT VERSCHWUNDEN: Ein Abriss ist keine
Aussage ueber die Welt. Der Provider BEHAELT bei einem Fehler den letzten
Stand und setzt nur `verbunden` auf false; es wird nie eine Liste geleert.
Jede Komponente uebernimmt einen Wert nur, wenn er wirklich da ist —
`devices === null` heisst "konnte nicht nachsehen", nicht "keine
Laufwerke". Das war der Fehler hinter "wird oft neu geladen".

EIN PLACEBO WENIGER: Oben rechts stand ein fest verdrahtetes "ONLINE" mit
pulsierendem Punkt — es leuchtete gruen, auch wenn die API tot war. Jetzt
zeigt es LIVE oder VERBINDUNG WEG, und im Tooltip steht, wann die letzte
Meldung kam.

DER SERVER SCHIEBT JETZT AUCH DEN SERVER-ZUSTAND: Neuer Ereignistyp
system.status (Hardware, Worker, Ablageziele) im 15-Sekunden-Takt des
Waechters — EINMAL im Server statt 15/min je Tab. Nur mitgeschickte
Schluessel werden uebernommen; ein fehlender heisst "behalte deinen Stand".

DAZU EIN FORMATFEHLER GEFUNDEN UND BEHOBEN: /logs bildet ts -> timestamp
ab, mein Snapshot lieferte die rohe DB-Zeile. Das UI haette "Invalid Date"
gezeigt — und zwar NUR im Live-Betrieb, nicht beim manuellen Neuladen.
Jetzt gibt es _log_zeile() einmal, benutzt von beiden.

UND EINEN ZWEITEN: system_lesen lief per asyncio.get_event_loop() in einem
Worker-Thread — dort ist das NICHT die laufende Schleife. Die Coroutine
waere nie gelaufen. Die Schleife wird jetzt im Startup festgehalten.

GEMESSEN: ruff sauber, 378 Tests + 3 uebersprungen, `npm run build`
durch (1650 Module). Der Live-Beweis steht noch aus — er kommt mit dem
Deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:32:50 +02:00
HitonabiandClaude Opus 5 f097a0b59d feat(bus): Waechter — Worker-Aenderungen werden zu Ereignissen
Ampel / ampel (push) Successful in 43s
WAS: rippy/bus/waechter.py sieht im Takt von 1 s in der Datenbank nach und
meldet Aenderungen an den Bus: job.created / job.phase / job.progress /
job.finished und log.line. 12 Tests, ohne Datenbank lauffaehig.

WARUM ES IHN BRAUCHT: Der Bus verteilt innerhalb EINES Prozesses. Der
Worker ist aber ein eigener Prozess, im Docker-Betrieb ein eigener
Container, im verteilten Betrieb ein anderer Rechner. Ohne Bruecke wuesste
die API nichts vom Fortschritt eines Rips.

WARUM UEBER DIE DATENBANK und nicht per HTTP-Rueckruf oder Redis:
- HTTP-Rueckruf: jeder Fortschrittswert haengt an einer Verbindung, ein
  kurzer API-Neustart liesse Ereignisse verschwinden, und der Worker
  braeuchte zusaetzliche Konfiguration.
- Redis Pub/Sub: sauber, kommt im verteilten Betrieb (V2-5) — aber der
  Standalone-Betrieb hat bewusst KEIN Redis, das war der Punkt von V2-2.
- Die Datenbank ist ohnehin die Wahrheit ueber den Job-Zustand, ist in
  JEDEM Modus da, und der Worker schreibt dort sowieso hin.

"Ist das nicht wieder Polling?" Doch — aber einmal, lokal und billig:
vorher N Tabs x 9 Endpunkte alle 4-5 s ueber HTTP durch nginx durch das
Rate-Limit; jetzt EINE Abfrage pro Sekunde von vier Spalten. Das Ziel war
nie "nirgendwo nachfragen", sondern: der Browser fragt nicht mehr.

DER TEST HAT EINEN ECHTEN FEHLER GEFUNDEN: Der Docstring versprach "erst
neue Jobs, dann Aenderungen", die Schleife lieferte aber die Reihenfolge
des dicts — ein Client haette ein job.progress fuer einen Job bekommen
koennen, den er noch gar nicht kennt. Jetzt drei getrennte Durchlaeufe.
Das Versprechen war richtig, der Code nicht.

Und er stirbt nicht still: Fehler gehen ins Log UND als system.notice an
den Bus (erster Fehlschlag, danach jeder zehnte), und lebt_seit_sekunden()
macht sein Alter abfragbar — AGENTS.md, "wer einen Vorrat anlegt, macht
sein Alter abfragbar".

GEMESSEN: ruff sauber, 371 Tests gruen + 3 uebersprungen (vorher 359).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:23:03 +02:00
HitonabiandClaude Opus 5 1fbe2cfc55 fix(test): Snapshot-Tests brauchten eine Datenbank — die Ampel hat keine
Ampel / ampel (push) Successful in 40s
WAS: test_snapshot_* ersetzen den Store per monkeypatch, statt
db.list_jobs() wirklich aufzurufen.

WARUM ROT (Lauf 170): Die Ampel startet KEINE Postgres. Mein neuer Test
war der erste im ganzen Repo, der eine Datenbank angefasst hat — alle
anderen in test_api_smoke.py pruefen nur Importe und Routen. Auf der VM
und lokal waere es nie aufgefallen: dort ist eine DB da bzw. der ganze
Test wird uebersprungen (kein fcntl unter Windows).

Geprueft wird die Snapshot-LOGIK ("konnte nicht nachsehen" ergibt None,
nicht []), nicht die Datenbank. Also gehoert die Datenbank da nicht rein.

Dazu ein zweiter Test: der Snapshot muss alle vier Bereiche liefern
(jobs/workers/logs/devices) — ein Client, dem einer fehlt, muesste den
Rest raten und fiele auf Polling zurueck.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:20:58 +02:00
HitonabiandClaude Opus 5 55db9eb13f feat(api): V2-3 (Teil 1) — Ereignis-Bus und SSE-Endpunkt /api/v2/events
Ampel / ampel (push) Failing after 41s
WAS: rippy/bus mit Ereignis-Schema, In-Process-Treiber und Ringpuffer.
Dazu der SSE-Strom in der API: erst ein Snapshot, danach nur Deltas,
mit lueckenloser Wiederaufnahme ueber Last-Event-ID.

WARUM: Ein offener Tab plus ein Worker verursachen heute rund 133
Anfragen pro Minute (Dashboard 75 + Log-Kasten 24 + Laufwerke 12 +
Worker-Liste 4 + Log-Seite 6 + Tray 12). Das Rate-Limit stand einmal
UNTER dieser Zahl — daher die sich leerende Job-Liste im Sekundentakt.
Mit einer offenen Verbindung sind es null.

DREI EIGENSCHAFTEN, ALLE AUS v1-FEHLERN:

1. Der Bus traegt NUR Nachrichten ueber Aenderungen, nie den Zustand.
   Wer den Zustand will, fragt den Store. Damit kann ein verpasstes
   Ereignis auch keinen Zustand loeschen — anders als beim fuenffachen
   `catch(() => [])` im alten UI, wo jeder fehlgeschlagene Abruf
   "es gibt keine Jobs" bedeutete.
2. Eine zu grosse Luecke wird ANGESAGT, nicht verschluckt. nachliefern()
   gibt None ("hol dir ein ganzes Bild") statt [] ("nichts verpasst") —
   dieselbe Unterscheidung wie timeout-Rueckgabe 124 bei den Netzpfaden.
   Stillschweigend weiterzumachen waere schlimmer: Das UI hielte sich
   fuer aktuell und waere es nicht.
3. Der Snapshot meldet unlesbare Laufwerke als None, nicht als leere
   Liste. "Konnte nicht nachsehen" ist etwas anderes als "gibt es nicht".

Ein unbekannter Ereignistyp fliegt beim Senden HOCH statt durchzugehen.
Ein Tippfehler waere sonst der stillste aller Fehlschlaege: Nachricht
raus, kein Empfaenger, nirgends ein Hinweis.

Ein langsamer Zuhoerer (Tab im Hintergrund, lahmes Handy) bremst den
Sender nicht — er wird markiert und bekommt beim naechsten Mal einen
Snapshot. Ein Rip darf nicht auf einen Browser warten.

GEMESSEN: ruff sauber, 359 Tests gruen + 3 uebersprungen (vorher 346).
13 neue Bus-Tests laufen auf jeder Plattform; die vier SSE-Tests haengen
an main.py und laufen damit in der Ampel.

NOCH OFFEN in V2-3: das UI auf den Strom umstellen (neun setInterval)
und die Ereignisse an den Zustandsaenderungen ausloesen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:09:33 +02:00
HitonabiandClaude Opus 5 d2645f87e8 feat(core): V2-2 — SQLite hinter demselben Port, Auftrags-Queue mit Lease
Ampel / ampel (push) Successful in 39s
WAS: Der Store spricht jetzt beide Datenbanken (PostgreSQL wie bisher,
SQLite fuer den Standalone-Betrieb). Dazu die LocalQueue: Auftraege als
Tabelle, Vergabe per bedingtem UPDATE, Lease statt Zombie-Jagd. Und die
Konfigurationsschicht mit EINER Praezedenz fuer alle drei Betriebsarten.

WARUM: Ohne SQLite und ohne broker-lose Queue gibt es keinen Standalone-
Betrieb — und ohne den keine Windows-App und keine Headless-Variante.
Beides haengt an dieser Etappe.

DER GRUNDSATZ (KONZEPT-V2.md §3.1): Die Datenbank ist die Wahrheit ueber
den Job-Zustand, der Broker ist nur der Wecker. Daraus folgt die ganze
Queue: Ein Auftrag ist eine Zeile mit claimed_by und lease_until, ein
Knoten uebernimmt ihn per bedingtem UPDATE (es gewinnt genau EINER, auch
wenn zehn gleichzeitig fragen), und er haelt ihn per Lease am Leben.
Laeuft die Lease ab, ist der Auftrag frei — egal ob Absturz, Netzausfall
oder gezogener Stecker.

Damit gibt es den Zustand "laeuft, aber niemand arbeitet daran" nicht
mehr, den v1 mit zombies.py (206 Zeilen + 263 Zeilen Tests) nachtraeglich
einsammeln musste. Er kann hoechstens 60 Sekunden bestehen und heilt sich
dann selbst. Beides ist geprueft: zwei Knoten bekommen NICHT denselben
Auftrag, und eine abgelaufene Lease gibt ihn wirklich wieder her.

KEINE QUEUE-BIBLIOTHEK (Taskiq/ARQ/Celery-lite), Begruendung im
Modul-Docstring: Der teure Teil ist kein Task, sondern ein 30-90-Minuten-
Subprozess mit Fortschritts-Parsing. Was die Bibliotheken loesen, ist
nicht das Problem; was das Problem ist, muss man ohnehin selbst bauen.

ABWEICHUNG VOM ENTWURF — KEIN ALEMBIC (in KONZEPT-V2.md §3.2 vermerkt):
Die Datenbank auf der VM gibt es schon, Alembic muesste sie erst
stempeln — ein Handgriff auf einer laufenden Installation, den beim
naechsten Deploy jemand vergisst. Und es gibt hier nichts zu
versionieren: drei nachgetragene Spalten. store.migrieren() fragt
stattdessen per inspect() nach und legt nur Fehlendes an; das laeuft auf
beiden Dialekten. Der alte Weg (ADD COLUMN IF NOT EXISTS) war korrekte
Postgres-Syntax und waere auf SQLite gebrochen.

EIN KONSTRUKTIONSFEHLER, SELBST GEFUNDEN: lokal.py hatte anfangs
`from rippy.store import engine` — ein Import bindet den Wert EINMAL, ein
spaeteres store.verbinden() waere nie angekommen, und das Modul haette
weiter mit der alten Datenbank gesprochen. Jetzt wird store.engine zur
Aufrufzeit gelesen; die Warnung steht an beiden Stellen im Code.

GEMESSEN: ruff sauber, 346 Tests gruen + 3 uebersprungen (vorher 301).
Neu: 30 Tests gegen ECHTES SQLite (kein Fake) — inklusive der Gegenprobe,
dass WAL und busy_timeout wirklich gesetzt sind, und dass migrieren()
eine weggenommene Spalte zurueckholt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:03:46 +02:00
HitonabiandClaude Opus 5 7558be4854 fix: CRLF in gui.py wiederherstellen (Zeilenenden-Falle in V2-1)
Ampel / ampel (push) Successful in 39s
WAS: docker/worker/gui.py war CRLF und ist beim Umstellen des db-Imports
komplett auf LF gekippt — 1520 Zeilen Diff-Rauschen fuer EINE geaenderte
Zeile. Zeilenenden zurueckgedreht, der Inhalt bleibt.

WARUM ES PASSIERT IST: Mein Umschreib-Helfer las mit
io.open(p, encoding="utf-8") — im Textmodus wandelt Python CRLF beim LESEN
zu LF — und schrieb mit newline="" zurueck, also LF. Betroffen war nur
gui.py; die uebrigen angefassten Dateien sind ohnehin LF.

Richtig waere newline="" AUCH beim Lesen gewesen (dann bleiben die
Zeilenenden im String stehen) oder gleich der Binaermodus.

Das steht so schon in den Projekt-Notizen und ist mir trotzdem passiert,
weil der Helfer generisch war und die Datei nicht danach aussah.
Gegenprobe: `file docker/worker/gui.py` sagt wieder "with CRLF line
terminators", und `git diff HEAD~2 -- gui.py` zeigt genau eine Zeile.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:55:21 +02:00
HitonabiandClaude Opus 5 dd1d0b7365 refactor(core): V2-1 — die vier Ports, ein Store, ein Laufwerks-Treiber
Ampel / ampel (push) Successful in 40s
WAS: rippy/ports.py beschreibt Store/Queue/Bus/Drives als Protocol. Zwei
weitere Doppelungen sind zusammengelegt: db.py (API+Worker) wird
rippy/store, und der Auswurf (api/devices.py + ripping.wirf_disc_aus)
wird rippy/drives/linux. Verhalten unveraendert.

WARUM: Drei Betriebsarten tragen nur, wenn ein Modus die Auswahl der
Treiber hinter vier Nahtstellen ist statt ein eigener Codestand
(KONZEPT-V2.md §1). Diese Etappe zieht die Nahtstellen ein, ohne schon
einen zweiten Treiber zu haben — die kommen in V2-2 (SQLite/LocalQueue)
und V2-4 (Windows).

DIE UNANGENEHMERE DOPPELUNG WAR DER AUSWURF: Er stand zweimal da, mit
UNTERSCHIEDLICHEN Vertraegen — devices.eject wirft OSError, ripping.
wirf_disc_aus gibt False zurueck und wirft nie. Beides ist richtig fuer
seine Seite (Browser-Meldung gegen "ein Rip stirbt nicht an einer
klemmenden Schublade"). Jetzt liegt EINE Mechanik darunter
(auswerfen_mit_grund) und beide Vertraege unveraendert darueber.
Die API-Fassung war ausserdem NIE getestet — jetzt schon, inklusive
"reicht ENOENT/EPERM unveraendert weiter".

get_settings hatte den einzigen echten Verhaltensunterschied der beiden
db.py: die Worker-Fassung schluckte jeden Fehler und gab {} zurueck.
Nicht still entschieden, sondern sichtbar gemacht — der Parameter
bei_fehler_leer steht jetzt in der Signatur, mit der offenen Frage im
Docstring. {} heisst fuer den Aufrufer "nichts gesetzt", nicht "konnte
nicht nachsehen"; das ist dieselbe Klasse wie catch(() => []) im alten
UI. Zu entscheiden in V2-2.

ZWEITER BEINAHE-FEHLER DIESER ETAPPE: linux.py importierte detection
auf Modulebene — und das zieht fcntl. Damit waere ripping.py und ueber
es der NATIVE WINDOWS-WORKER nicht mehr ladbar gewesen. Diesmal haben
die Tests es sofort gefangen (4 Sammelfehler). Behoben an der Wurzel:
Konstanten und die reine classify() leben jetzt in drives/cdrom.py,
ganz ohne fcntl. Nebengewinn — die classify-Tests liefen bisher NUR in
der Ampel ("erst nach dem Push bewiesen") und laufen jetzt ueberall.

Ausserdem: .dockerignore-Testmuster brauchen **, sonst greifen sie nur
in der obersten Ebene. Im laufenden Container nachgezaehlt: 30 test_*.py
lagen in den Images.

GEMESSEN: ruff sauber, 301 Tests gruen + 1 uebersprungen (vorher 290;
+4 neue eject-Tests, +7 classify-Tests die jetzt lokal laufen). Kein
Modul liegt mehr doppelt im Repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:54:28 +02:00
HitonabiandClaude Opus 5 edfe0d4313 docs: SAVEPOINT fuer v4.0-alpha — v2-Konzept entschieden, Etappe V2-0 gruen
Ampel / ampel (push) Successful in 39s
WAS: Neuer Kopfeintrag mit dem gemessenen Zustand (b98dc5e, Ampel gruen,
290 Tests, 0 doppelte Module), den drei Commander-Entscheiden, der
gebauten Etappe V2-0 und dem naechsten Schritt.

WARUM: AGENTS-Workflow Punkt 5 — die naechste Sitzung faengt nicht bei
Null an. Besonders wichtig diesmal, weil der Deploy auf die VM bewusst
aussteht und beim ersten Deploy nach V2-0 zwei Dinge gezielt zu pruefen
sind (liegt /app/rippy in beiden Containern, enthaelt das Worker-Zip
den Ordner).

Dokumentiert ist ausserdem der Fehler, den der Umbau fast ausgeliefert
haette: ein "try/except ImportError" um den detection-Import in
tasks.py haette einen falschen Modulpfad STILL geschluckt und auf Linux
jeden Rip verweigert. Die Lehre steht dabei — bei einem Modul-Umzug
reicht grep auf Zeilenanfaenge nicht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:41:29 +02:00
HitonabiandClaude Opus 5 b98dc5eef5 refactor(core): V2-0 — gemeinsames Paket rippy statt drei Zwillingsdateien
Ampel / ampel (push) Successful in 41s
WAS: Neues Paket src/rippy (core/drives/rip). detection.py,
makemkv_daten.py und notify.py lagen je ZWEIMAL im Repo — unter
docker/api UND docker/worker, byte-identisch. Jetzt gibt es sie einmal;
beide Container importieren dieselbe Datei. Verhalten unveraendert.

WARUM: Es gab kein geteiltes Paket zwischen den Containern, deshalb die
Kopien. docker/api/db.py sagt es im Kopfkommentar selbst: "Wer die
Struktur aendert, aendert BEIDE Dateien." Ein Waechter-Test
(test_zwillinge_sind_byteweise_identisch) hat das mechanisch
abgesichert — er war noetig, weil die Konstruktion falsch war. Erste
Etappe des v2-Plans (KONZEPT-V2.md §8.3).

IM EINZELNEN:
- src/rippy/{core/notify, drives/detection, rip/makemkv_daten}.py
- conftest.py in der Wurzel legt src/ auf den sys.path (die Ampel ruft
  pytest dort auf).
- Beide Dockerfiles kopieren src/rippy nach /app/rippy — /app ist
  Arbeitsverzeichnis und uvicorn-App-Dir, also ohne PYTHONPATH findbar.
- worker_setup_paket packt das Paket ausdruecklich mit ins Zip: die
  Schleife sah nur die oberste Ebene, ein Unterordner waere nie
  mitgekommen und der Windows-Worker beim Start gestorben.
- Die zwei Test-Dateien fuer makemkv_daten sind zu einer verschmolzen
  (die Faelle aus beiden). Damit entfaellt auch der importlib-Umweg im
  Worker-Test: der war noetig, weil bei "pytest -q" aus der Wurzel
  docker/api zuerst eingesammelt wird und jeder weitere Import nur noch
  den sys.modules-Cache trifft — die Worker-Kopie wurde also nie
  angefasst. Netto -13 doppelte Tests, +2 neue (siehe unten).

EIN FEHLER, DEN DER UMBAU FAST AUSGELIEFERT HAETTE: In tasks.py steht
der detection-Import in einem "try/except ImportError" — absichtlich,
denn der native Windows-Worker hat kein fcntl und soll trotzdem
starten. Das except verschluckt aber JEDEN ImportError, auch einen
falschen Modulpfad. Auf Linux haette der Worker ab sofort still
detect_disc_type=None gesetzt und jeden Rip verweigert, ohne dass
irgendwo ein Fehler stuende — genau die Klasse "still scheiternder
Hintergrund-Prozess" aus AGENTS.md. Gefunden ueber eine zweite Suche
mit eingerueckten Treffern.

Waechter dagegen: src/rippy/test_paket.py prueft mit
importlib.util.find_spec (fuehrt nichts aus, laeuft also auch auf
Windows ohne fcntl), dass es die drei Modulpfade wirklich gibt. Beim
Wegnehmen von detection.py gegengeprueft: Test wird rot, nach
Wiederherstellen gruen.

GEMESSEN: ruff sauber, 290 Tests gruen + 1 uebersprungen (lokal, ohne
die zwei Linux-only-Module). Kein Modul liegt noch doppelt im Repo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:38:29 +02:00
HitonabiandClaude Opus 5 20675a98fa docs: Konzept fuer Rippy v2 + drei Commander-Entscheide (28.08.2026)
WAS: KONZEPT-V2.md neu — Systemarchitektur, Daten-/Queue-Strategie, die
drei Betriebsmodi, API-/Event-Design, Migrationsplan V2-0 bis V2-7.
KONZEPT.md §10 und ROADMAP.md ziehen nach.

WARUM: Rippy soll drei Betriebsarten bekommen statt einer — Docker,
native Windows-App ohne Docker, Headless-Linux-Dienst; alle mit
demselben Webinterface. Das traegt technisch nur, wenn es EINEN Kern
gibt, dessen Betriebsmodus nur die Auswahl der Treiber hinter vier
Ports ist (Store, Queue, Bus, Drives).

Drei Entscheide des Commanders sind eingearbeitet:

- Reihenfolge: Echtzeit (SSE statt Polling) VOR der Windows-App. Die
  Windows-App braucht SQLite und die lokale Queue ohnehin.
- Disc-Schluessel: automatischer Abruf MIT Rueckfallebene, als Kette in
  drei Stufen (eigener Bestand -> Abruf -> Import von Hand). Das
  verschiebt die Grenze aus KONZEPT.md §10 vom 25.07.2026 bewusst —
  deshalb steht sie dort jetzt ausdruecklich fortgeschrieben statt
  still ersetzt (AGENTS Regel B). Bezugsadresse leer vorbelegt,
  Fehlschlag laut, Bestand wird nie still ueberschrieben.
- Speicherziele: der Host mountet, Rippy prueft und erzeugt die
  kopierbare Zeile. Raeumt die URSACHE der CIFS-Ausfaelle ab (Befund
  26.07.2026: die Verbindung lebt in der Netz-Namespace des
  api-Containers und stirbt mit ihm) statt weiter das Symptom zu
  heilen. SYS_ADMIN, DAC_READ_SEARCH, apparmor:unconfined und die
  Mount-Wache fallen damit weg.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:38:03 +02:00
Hitonabi a14449ef85 fix(worker-ui): fix syntax error in gui.py and ruff error in test_verwaltung.py
Ampel / ampel (push) Successful in 39s
2026-07-27 17:17:30 +02:00
Hitonabi 4343b0901d feat(worker-ui): adjust thumbnails, log view, and button logic to match web dashboard
Ampel / ampel (push) Failing after 40s
2026-07-27 17:16:00 +02:00
Hitonabi c3e77399ad fix(worker): mock ntpath.dirname in test_erreichbarkeit so it passes on Linux CI runner
Ampel / ampel (push) Failing after 39s
2026-07-27 17:13:45 +02:00
Hitonabi 75426224d1 fix(worker): ensure sys.path in test_verwaltung for pytest importlib mode
Ampel / ampel (push) Failing after 39s
2026-07-27 17:11:11 +02:00
Hitonabi e5ff5373bb fix(worker): restore pure logic module verwaltung.py and its tests (SoC)
Ampel / ampel (push) Failing after 39s
2026-07-27 17:06:38 +02:00
Hitonabi 9b30c0a490 chore: remove obsolete test_verwaltung.py
Ampel / ampel (push) Failing after 40s
2026-07-27 17:02:37 +02:00
Hitonabi 061f79ecda fix: ruff linter errors (multiple statements, bare except, unused vars)
Ampel / ampel (push) Failing after 38s
2026-07-27 17:00:52 +02:00
Hitonabi a32a1bb075 fix: bare except in main.py
Ampel / ampel (push) Failing after 38s
2026-07-27 16:56:36 +02:00
Hitonabi 32979d7bde docs: SAVEPOINT update fuer Etappe 25 (Windows Worker in Flet)
Ampel / ampel (push) Failing after 40s
2026-07-27 16:53:59 +02:00
Hitonabi d15f4b2ab6 feat: Thumbnails, bunte Logs und Button-Status im Worker UI 2026-07-27 16:17:30 +02:00
Hitonabi a221a161f4 feat: Tray-Icon fuer Worker GUI (pystray) — Fenster schliessen minimiert ins Tray 2026-07-26 22:33:54 +02:00
Hitonabi 264b2c77f0 chore: finale exe mit venv --clear fix neu gebaut 2026-07-26 22:32:12 +02:00
Hitonabi 16ea54e35d fix: venv --clear bei Reinstall, verhindert veraltete Paketversionen 2026-07-26 22:30:12 +02:00
Hitonabi 81ba6bd1c6 fix: don't override pinned flet==0.23.2 with unpinned pip install 2026-07-26 22:24:20 +02:00
Hitonabi 06c1718a52 fix: installer buttons (Durchsuchen/Freigabe holen), Worker-Start via os.startfile, Shortcut via PowerShell 2026-07-26 22:15:50 +02:00
Hitonabi de500f9890 feat: complete rewrite of worker installer in Flet (standalone exe) 2026-07-26 22:07:24 +02:00
Hitonabi 9a151f3d8b feat: Worker V2 Flet GUI mit Feature-Parität und neuem Installer 2026-07-26 22:01:27 +02:00
Hitonabi e165e2a0d3 feat: Komplettierung der aktuellen Rippy-Etappe
Ampel / ampel (push) Failing after 29s
- Versionsanzeige für den Windows-Worker im UI inkl. Prüfung
- Absicherung der install.sh gegen fehlendes systemd
- Serien-Episoden-Erkennung und Laufzeitabgleich anhand TMDB-Daten (Heuristik)
- Umstellung der Windows-Worker-Installation auf SchTasks (Dienst-Ersatz)
- Kodi-Bibliotheks-Refresh über JSON-RPC integriert
2026-07-26 21:13:27 +02:00
Hitonabi d15900d1eb fix(audio): nur die beste Tonspur pro Sprache verlustfrei kopieren
Ampel / ampel (push) Failing after 29s
2026-07-26 20:55:57 +02:00
HitonabiandClaude Opus 5 4cb1fab416 docs(savepoint): die Kette ist zum ersten Mal ganz durchgelaufen
Ampel / ampel (push) Failing after 30s
40,9 GB Rip -> Auswurf -> Kompression auf dem Windows-PC -> 12,29 GB abgelegt,
Roh-Verzeichnis danach automatisch aufgeraeumt. Die Sprachwahl am ERGEBNIS
nachgeprueft (HandBrakeCLI --scan auf die abgelegte Datei): nur noch deutsche
Ton- und Untertitelspuren, kein Japanisch, keine unbenannten - vorher meldete
der Disc-Scan deu 4x, jpn 2x, und 4x.

Ein Fund aus dem Ergebnis, bewusst NICHT still repariert: Der Ton ist fuenfmal
MP3 2.0 mit 160 kbps. Das Preset "HQ 1080p30 Surround" schreibt laut
--preset-export zwei Regeln vor (av_aac stereo 160 / Surround 640); angewandt
wurde nur die erste, auf jede behaltene Spur. Der Surround-Ton eines Presets
namens "Surround" faellt damit still weg. Warum der Encoder MP3 statt av_aac
wurde, ist ungeklaert - Rippy uebergibt keinen Audio-Schalter. Als offener
Punkt notiert statt geraten; die Preset-Wahl ist eine Entscheidung des
Commanders.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 18:52:12 +02:00
HitonabiandClaude Opus 5 4915bba0b3 docs(savepoint): der volle Durchlauf - vier Belege und ein gefundener Fehler
Ampel / ampel (push) Failing after 30s
Belegt: Auswurf im automatischen Weg (bisher nur der UI-Knopf), Phasen-Marke
springt bei der Uebergabe auf true, Sprachwahl kommt beim externen Encoder an
("Ton: deu, Untertitel: deu"), Mount-Wache heilt in 4,2 s.

Gefunden: Jeder erste Film in einer neuen Ablage war unerreichbar - die Pruefung
liess nur eine fehlende Ordner-Ebene durch, obwohl makedirs die ganze Kette
anlegt. Der Fehler wartete seit v3.17 darauf, dass jemand eine frische Ablage
benutzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:47:27 +02:00
HitonabiandClaude Opus 5 96c400f2f2 fix(worker): erster Film in einer neuen Ablage war immer unerreichbar
Ampel / ampel (push) Failing after 30s
Im ersten vollstaendigen Durchlauf aufgefallen: Der Rip lief sauber durch (40 GB
Akira, Marke rip_fertig sprang korrekt auf true), die Kompression brach 0,2 s
spaeter ab mit "Dieser Worker erreicht die Ziel (fertige Datei) nicht:
\\192.168.178.62\rippy\movies\Akira (1988)".

Die Freigabe war erreichbar. Es fehlten zwei noch nie angelegte Ordner - movies
und der Filmordner darin. Die Pruefung liess aber nur EINE fehlende Ebene durch
(sie sah nach dirname), obwohl gleich darauf os.makedirs die ganze Kette anlegt.
Damit war jeder ERSTE Film in einer neuen Ablage systematisch unerreichbar.
Jetzt zaehlt, ob irgendein Vorfahre existiert (erster_vorhandener_ordner,
begrenzt auf 12 Stufen, damit auf einer toten Freigabe nicht endlos geklopft
wird).

Zweiter Fehler in derselben Meldung: Sie behauptete "RIPPY_PATH_MAP ist gesetzt,
deckt diesen Pfad aber nicht ab" - und nannte im selben Satz den korrekt
uebersetzten UNC-Pfad. Wer dem folgte, suchte in der Karte statt in der
Freigabe. Die Karte wird nur noch beschuldigt, wenn sie den Pfad wirklich nicht
angefasst hat; sonst steht da, was zutrifft: uebersetzt, aber gerade nicht
erreichbar. Dazu der Artikel-Fehler "erreicht die Ziel" behoben.

Diese Pruefung hatte KEINEN einzigen Test - deshalb kam beides durch. Jetzt 12
(301 gruen). Beim Schreiben ist noch ein dritter Fehler aufgefallen:
`isdir=os.path.isdir` als Vorgabewert bindet die Funktion beim IMPORT, ein
Ersetzen geht danach ins Leere. Aufloesung jetzt beim Aufruf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:43:51 +02:00
HitonabiandClaude Opus 5 936b0837bf docs(savepoint): Uhrzeit-Meldung erledigt - mit dem Beweis, der zaehlt
Ampel / ampel (push) Successful in 32s
Die "Substantial drift 7200 seconds"-Meldung kam nicht alle paar Sekunden,
sondern einmal pro Absender-Hostname (celery memoized nach hostname) - und der
Container-Hostname wechselt bei jedem Neubau. Die vier Meldungen im Log sind
genau vier Deploys.

Der Beweis, dass sie weg ist: Um 14:57:33 synchronisierte sich der Worker mit
celery@d160eb2f3afd, dem frischen Container nach dem Deploy. Ein NEUER Hostname
umgeht die Merk-Sperre, die Warnung haette also feuern muessen. Sie kam nicht.
Dazu gemessen: TZ=UTC wirkt auf Windows wirklich (time.timezone -3600 -> 0), und
die celery-Zeitstempel im lokalen Worker-Log stehen in UTC.

Ausserdem festgehalten: Der Windows-Worker laeuft auf gemischten Dateistaenden
(tasks.py 13:19, ripping.py 14:06, zombies.py 14:17) - die Sprachwahl ist
vollstaendig drin, meta_merken fehlt noch. Es fehlt eine Versionsanzeige, die
so etwas sichtbar macht, statt es nur beim Nachsehen auf dem PC zu finden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:23:14 +02:00
HitonabiandClaude Opus 5 a9cfcb272a docs(konzept): Rate-Limit 100/min -> 600/min als Abweichung vermerkt
Ampel / ampel (push) Successful in 32s
Regel B: keine stille Aenderung an einer KONZEPT-Vorgabe. Das Muss-Feature
"Rate-Limiting pro Client-IP" bleibt, nur die Zahl aendert sich - mit der
Rechnung, die zeigt warum: 100/min lag unter Rippys eigener Last (ein offener
Tab braucht 111/min, ein Windows-Worker 12 dazu). Festgehalten durch
test_grenze_deckt_die_eigene_last_ab, damit die Zahl nicht unbemerkt zurueck
unter die Grundlast wandert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:09:23 +02:00
HitonabiandClaude Opus 5 163216a68e docs(savepoint): v3.20 - die Bremse war das Problem, nicht die Last
Ampel / ampel (push) Successful in 31s
SAVEPOINT v3.20 mit den Messwerten: /jobs und /capabilities byteweise identisch
ueber zwanzig Sekunden (es lud also nichts neu), 97 Antworten mit HTTP 429 im
nginx-Log, 812 von 876 Anfragen scheinbar von einer IP, und die Rechnung, die
zeigt warum: ein offener Tab braucht 123 Anfragen/min, erlaubt waren 100.

Dazu drei neue Lehren in AGENTS.md:
- Ein verpasster Abruf ist keine Nachricht ueber die Welt (`catch(() => [])`).
- Eine eigene Schutzbremse gegen die eigene Last rechnen - und jedes Greifen
  protokollieren, sonst ist sie unsichtbar.
- Eine geschluckte Warnung ist eine Falle (`|| echo` in einem 200-Zeilen-Log).
- Und: wenn der Commander eine Korrelation nennt, ist das eine Spur, auch wenn
  seine vermutete Erklaerung daneben liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 17:06:56 +02:00
HitonabiandClaude Opus 5 9156e8a4a9 fix(deploy): leere Argumente und eine fehlende .env brachen das Deploy - still
Ampel / ampel (push) Successful in 30s
Zwei Funde beim Deployen, beide fuer die Weitergabe relevant:

1. `./deploy.sh` ohne Argumente - der dokumentierte Normalfall - endete mit
   "line 5: $4: unbound variable". Leere Argumente ueberleben den Weg durch ssh
   nicht: Die Gegenseite bekommt die Befehlszeile als EINEN String und parst sie
   neu, wobei "" ersatzlos verschwindet. ${4:-} statt "$4".

2. Fehlte die .env-Quelle, war das eine geschluckte Warnung. Auf der Ziel-VM lag
   die echte .env unter ~/projects/rippy/.env, der Default zeigte auf
   ~/rippy/.env. Das `cp` schlug also jedes Mal fehl, die Warnung scrollte im
   Build-Rauschen vorbei, und gebaut wurde mit der Kopie im Klon: zwei Tage alt,
   darin ein MAKEMKV_URL_BASE-Notbehelf auf einen web.archive.org-Schnappschuss
   (liefert inzwischen 525, waehrend makemkv.com wieder 200 gibt) und ein
   JWT_SECRET_KEY aus der Zeit vor dem Auth-Rueckbau. Jeder worker-Build brach
   daran ab, und die Ursache stand nirgends.

Jetzt: .env uebernommen -> es steht da. Quelle fehlt, Klon hat eine -> laute
Warnung samt Dateidatum und Kandidatenliste. Keine von beiden -> Abbruch, statt
mit leerem DB-Passwort zu bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:57:12 +02:00
HitonabiandClaude Opus 5 bfb13f44a5 fix(ui+api): das "Neuladen" war HTTP 429 - und "Neu" komprimierte immer
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>
2026-07-26 16:49:10 +02:00
HitonabiandClaude Opus 5 f7555a17d1 fix(zombies): die Fehlermeldung unterscheidet jetzt Rip von Kompression
Ampel / ampel (push) Successful in 29s
Nachtrag, direkt am echten Fall aufgefallen. Die Zombie-Erkennung hat den toten
Rip gefunden (Beleg: nach 140 s noch "processing 12 %", nach 160 s "failed") -
aber der Fehlertext sagte:

  "Die Rohdateien wurden NICHT geloescht: mit 'Neu komprimieren' laeuft die
   Kompression erneut, ohne die Disc noch einmal zu rippen."

Der Satz war fuer einen toten TRANSCODE geschrieben, wo die Roh-MKV vollstaendig
ist. Seit "running" mit zu den Arbeitsstati gehoert, traf er auch abgebrochene
RIPS - und da ist er schlicht falsch: Die Datei ist ein Bruchstueck (im Vorfall
5,1 GB von rund 40), und wer dem Rat folgt, komprimiert einen Film, der bei 12 %
aufhoert.

Jetzt unterscheidet der Text die Phase:
  Rip tot        -> "Der Rip war bei 12 % - die Roh-Datei ist UNVOLLSTAENDIG ...
                     Richtig ist: Disc wieder einlegen und neu rippen."
  Transcode tot  -> "Der Rip war fertig, nur die Kompression nicht ..."

Die deutschen Anfuehrungszeichen haben dabei zum vierten Mal an einem Tag
zugeschlagen (in DOPPELT gequoteten Python-Strings beendet das schliessende
Zeichen den String). Die betroffenen Zeilen sind jetzt einfach gequotet, wie es
der Bestand ohnehin macht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:23:15 +02:00
HitonabiandClaude Opus 5 19f3dc5330 fix(zombies): "running" fehlte - ein abgestuerzter RIP wurde NIE gefunden
Ampel / ampel (push) Successful in 30s
Der Commander: "Ausserdem ist gerade mitten im Rip das Laufwerk ausgegangen...
glaube ich zumindest." Gemessen war es etwas anderes, und der Fund ist groesser
als der Vorfall.

WAS MESSBAR WAR:
  Job 2182d525   status=running progress=12   (Rip gestartet 14:00:24)
  makemkvcon     laeuft NICHT (per /proc geprueft, PID gegengeprueft)
  Rohdatei       5.167.382.528 Bytes, waechst in 10 s nicht
  Laufwerk       Status 4 (Disc drin), /dev/sr0 + /dev/sg1 da
  Worker-Start   14:07:02  <- mein `docker compose up -d --build`

Das Laufwerk ist also NICHT ausgegangen. Der Rip wurde von MEINEM Deploy
getoetet: `up -d --build` baut den worker-Container neu, und der laufende Rip
stirbt mit ihm. Genau davor warnt der SAVEPOINT seit v3.18 - die Warnung half
nichts, weil sie niemand liest und nichts sie prueft.

DER EIGENTLICHE FUND: Die Zombie-Erkennung lief um 14:09:04 und meldete
`{'geprueft': 0, 'aufgeraeumt': []}` - obwohl der tote Job direkt vor ihr lag.
Ursache:

  ARBEITS_STATI = ("ripping", "transcoding", "canceling")   # zombies.py
  db.update_job(job_id, status="running", ...)              # tasks.py - der Rip

Der Rip setzt "running", gesucht wurde "ripping". Dieser Wert steht
ausschliesslich in Celerys Task-META und NIE in einer Job-Zeile (nachgeprueft:
kein einziger Schreiber im ganzen Baum). Die Zombie-Erkennung aus v3.14 wurde
gebaut, um genau einen abgestuerzten Rip zu finden - und hat ihn nie gesehen.

Besonders tueckisch: `geprueft: 0` sah bei jedem Worker-Start wie "nachgesehen,
alles gesund" aus, waehrend sie nach einem Status suchte, den es nicht gibt.
Deshalb blieb der Job auf "processing 12 %" stehen - mit einer Restzeit-Schaetzung
von 1 h 30 min obendrauf, die es fuer einen toten Prozess nicht geben duerfte.

GEBAUT:
  * "running" in ARBEITS_STATI.
  * Ein Test, der das Auseinanderlaufen MECHANISCH verhindert: Er liest tasks.py,
    sammelt jeden Status, den der Worker per db.update_job in eine Job-Zeile
    schreibt, und verlangt, dass jeder davon entweder ein Arbeitsstatus oder ein
    Endzustand ist. Ein Kommentar haette das nicht verhindert.
  * GET /health/arbeit + eine Sperre in deploy.sh: Laeuft ein Job, bricht der
    Deploy ab (uebersteuerbar mit RIPPY_TROTZDEM=1 - dann ist es eine
    Entscheidung und kein Versehen). Ein Satz Code gegen eine verlorene Stunde.
  * Server-Status zeigt jetzt die eingehaengten FREIGABEN mit freiem Platz, nicht
    nur die Container-Platte (Commander-Wunsch). Genau dort liegen die Rohdaten,
    und bei externem Encoden muessen sie dort liegen - wer wissen wollte, ob noch
    Platz fuer eine Disc ist, sah die falsche Zahl.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:16:05 +02:00
HitonabiandClaude Opus 5 6438beec05 fix(worker): kein CMD-Blitzen mehr - und die 7200-Sekunden-Meldung ist erklaert
Ampel / ampel (push) Successful in 31s
Zwei Meldungen, beide auf dieselbe Wurzel zurueckgefuehrt: Der Worker startet
Konsolenprogramme, und das hat unter Windows Nebenwirkungen.

1. "Es geht immer alle paar Sekunden ne CMD auf." Kein Geist, sondern der
   HERZSCHLAG. Der Worker laeuft als pythonw.exe, also ohne eigene Konsole. Wer
   ohne Konsole ein Konsolenprogramm startet, bekommt von Windows eine NEUE - und
   die ist sichtbar. `capture_output=True` hilft nicht: es leitet die Datenstroeme
   um, unterdrueckt aber kein Fenster. Und caps.py fragt jede Minute
   `HandBrakeCLI --help`, `--preset-list` und `--version` ab, dazu makemkvcon aus
   der Schluessel-Automatik: vier bis fuenf Fenster pro Minute, in Schueben.

   Neues winlauf.py haelt CREATE_NO_WINDOW an EINER Stelle; alle elf Aufrufe in
   caps.py, ripping.py und schluessel.py benutzen es. Auf Linux ist der Wert 0 und
   creationflags=0 eine Nulloperation - im Worker-Container gegengeprueft, damit
   derselbe Code auf beiden Seiten laeuft.

2. Die 7200 Sekunden sind CELERYS EIGENE UMRECHNUNG, nicht die Uhren. Erst fiel
   auf, dass die Sekundenbruchteile nur 40 ms auseinanderliegen (.177269 vs
   .134799) - derselbe Augenblick, zweimal verschieden gerechnet. Die Quelle sagt
   warum (celery/utils/time.py):

     def utcoffset():                    # Sekunden WEST von UTC, in Stunden
         return time.altzone // 3600     # CEST -> -2 ;  UTC -> 0
     def adjust_timestamp(ts, offset, here=utcoffset):
         return ts - (offset - here()) * 3600

   Absender VM (offset 0), Empfaenger Windows-PC (here() = -2):
   ts - (0 - (-2)) * 3600 = ts - 7200. Celery verschiebt den empfangenen Stempel
   also selbst und vergleicht ihn dann mit der eigenen Uhr. Die Annahme dahinter -
   Ereignisse truegen Ortszeit - stimmt nicht, `Event()` stempelt Epoch.

   Abhilfe gemessen, nicht geraten: Mit TZ=UTC meldet Windows utcoffset 0
   (timezone=0, isdst=0 - auf dem Commander-PC geprueft), damit wird die
   Umrechnung zur Nulloperation. Beide .bat-Dateien setzen es jetzt. Nebeneffekt
   ist erwuenscht: Der Worker rechnet dann in derselben Zone wie die VM und wie
   Rippys UI.

   Fuer Rippy war die Meldung ohnehin folgenlos - /capabilities fragt per
   Celery-Ping (Frage/Antwort, ohne Zeitstempel), nicht ueber die
   Gossip-Ereignisse. Aber eine Warnung, die bei jedem Blick ins Log steht und
   nichts bedeutet, kostet Vertrauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 16:05:54 +02:00
HitonabiandClaude Opus 5 5f01aa89fc fix(deinstaller): er brauchte Adminrechte und hatte keine
Ampel / ampel (push) Successful in 30s
Commander: "Ich kann den worker uebrigens immernoch nicht deinstallieren."

Der Screenshot zeigt ZWEI Dinge. Das erste ist erklaerbar: Es lief noch der ALTE
uninstall.ps1 von der letzten Installation - Zeile 2 mit dem kaputten
`if (-not $Force) { if ([System.Windows.Forms.MessageBox]...` . Meine Reparatur
schreibt die Datei erst beim naechsten Installer-Lauf neu.

Das zweite ist MEIN Fehler, und er haette auch die neue Fassung erwischt:

  Remove-Item : Das Element C:\Program Files\Rippy Worker\doc\LICENSE
  kann nicht entfernt werden: Der Zugriff auf den Pfad wurde verweigert.

Der Standard-Zielordner liegt unter "C:\Program Files", und der gehoert nicht dem
Benutzer. Die Installer-.exe ist mit -requireAdmin gebaut und fragt beim Start per
UAC - der Deinstaller hatte nichts Vergleichbares. Er war damit im STANDARDFALL
nutzlos, und mein `rd /s /q` waere genauso aufgelaufen.

Jetzt erhoeht er sich selbst: Schreibprobe im Ordner, Admin-Abfrage, und nur wenn
beides fehlt, ein Neustart per RunAs mit -Force (damit die Bestaetigung nicht
zweimal kommt). GEMESSEN statt angenommen - wer den Worker ins eigene Profil
installiert hat, bekommt gar keine UAC-Frage. An seinem echten Ordner
gegengeprueft: schreibbar=False, istAdmin=False -> Entscheidung "erhoehen".

Der erzeugte Deinstaller ist diesmal komplett nachgestellt worden: parst, und die
Reihenfolge stimmt (Add-Type Zeile 7, MessageBox Zeile 10, Schreibprobe,
Erhoehung, erst dann Loeschen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:55:05 +02:00
HitonabiandClaude Opus 5 f146f33f5d feat(installer): Verknuepfung auf dem Desktop - mit Rippy-Icon
Ampel / ampel (push) Successful in 30s
Commander: "Und wenn man den worker installiert braucht man auch eine exe auf dem
Desktop abgelegt wird zum starten you know?"

Berechtigt: Die Startdateien lagen nur im Programmordner. Wer den Worker von Hand
starten wollte, musste erst "C:\Program Files\Rippy Worker" suchen - und genau
das war heute noetig, als der Worker stand.

Jetzt legen beide Installer eine Verknuepfung "Rippy Worker" auf den Desktop
(GUI mit Haekchen, Kommandozeile per -KeineDesktopVerknuepfung abschaltbar). Drei
Details, die den Unterschied machen:

  * Das ICON wird mit ausgeliefert. rippy.ico lag im Repo, ging aber nie an die
    Zielmaschine - die Verknuepfung haette das Batch-Standardsymbol getragen und
    waere zwischen den anderen Icons unfindbar gewesen. Jetzt im Worker-Paket.
  * WindowStyle 7 (minimiert): start-tray.bat startet pythonw, also ohne
    Konsolenfenster - aber die .bat selbst blitzt sonst kurz auf. Gilt jetzt auch
    fuer die Autostart-Verknuepfung, wo derselbe Blitz war.
  * Der DEINSTALLER nimmt sie mit, an beiden moeglichen Orten (eigener Desktop
    und Desktop aller Benutzer). "Rueckstandsfrei entfernbar" ist ein
    MUSS-Kriterium; ein totes Symbol auf dem Desktop waere genau so ein Rueckstand.

Dieselbe Rechte-Ueberlegung wie beim Autostart: bei einer Installation unter
"Programme" auf den Desktop ALLER Benutzer, sonst auf den eigenen - bei einer
UAC-Erhoehung ueber ein fremdes Admin-Konto waere der eigene der falsche.

Layout headless gerendert und angesehen (Fenster 648 -> 672 px, nichts
ueberlappt), beide .ps1 mit echtem PowerShell 5.1 geprueft, .exe neu gebaut.
Und die CRLF-Falle aus dem Savepoint hat prompt wieder zugeschlagen: Mein
Python-Rewrite machte aus install-gui.ps1 LF - zurueckgedreht, `file` bestaetigt
BOM und CRLF fuer beide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-26 15:46:15 +02:00