Files
rippy/KONZEPT-WINDOWS.md
T
HitonabiandClaude Fable 5 a0cff205ab docs(konzept): Entscheid 4 — Worktree aufgeloest, Branch normal ausgecheckt
Der doppelte Checkout (Worktree + Hauptordner) blockierte den Start neuer
Sitzungen auf dem Branch. Der Branch selbst und main bleiben unveraendert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-30 16:25:58 +02:00

1132 lines
60 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# KONZEPT — Rippy für Windows
> **Rippy v5.** Ein eigenständiges Windows-Programm, gebaut mit Electron und
> TypeScript. Grüne Wiese: kein Docker-Code, kein Python, keine Container-Reste.
>
> Stand: 30.08.2026, **zweite Fassung** · Branch `worktree-windows-electron`
**Was die zweite Fassung vom selben Tag unterscheidet:** Das Konzept wurde
geprüft, tagesaktuell gegenrecherchiert und nachgemessen. Vier Dinge haben
sich geändert: (1) Ein Faktenfehler ist korrigiert — das Gitea des Commanders
ist entgegen der ersten Fassung **öffentlich erreichbar**, die Update-Frage
löst sich damit fast von selbst (§ 3.6, Entscheid 6). (2) Die letzte
technische Unbekannte ist gemessen — `node:sqlite` funktioniert in Electron 44,
im echten Einsatzkontext (§ 3.4). (3) Die Disc-Erkennung ist vom Kopf auf die
Füße gestellt — der Prüf-Takt ist Plan A, das Windows-Ereignis die spätere
Verfeinerung (§ 5). (4) Drei Entscheide des Commanders sind eingearbeitet:
Update-Quelle, Zielgruppe, und der Zeitpunkt des Aufräumens (Entscheide 6, 7
und 9).
---
## 0. Leseanleitung
| § | Frage |
|---|-------|
| 1 | Warum wird neu gebaut statt repariert? |
| 2 | Was hat der Commander entschieden? |
| 3 | Was wurde gemessen, bevor entschieden wurde? |
| 45 | Wie ist das Programm aufgebaut, und was passiert, wenn eine Disc reingeht? |
| 69 | Features, Oberfläche, Setup, Werkzeuge |
| 10 | Was kommt aus dem alten Rippy mit? |
| 11 | Was aus `main` entfernt wird |
| 1214 | Etappen, Risiken, was NICHT gebaut wird |
**Was dieses Dokument NICHT tut:** Es erfindet keine Messungen. Alles, was hier
als Zahl oder Verhalten steht, ist entweder in § 3 an dieser Maschine gemessen,
im alten Rippy belegt (`SAVEPOINT.md`, `AGENTS.md`), am 30.08.2026 gegen das
Netz geprüft oder ausdrücklich als **Annahme** markiert. Die Trennung ist
wichtig, weil dieses Projekt schon mehrfach teuer dafür bezahlt hat, dass
jemand eine plausible Erklärung für eine gemessene gehalten hat. Die erste
Fassung dieses Konzepts hat es selbst vorgemacht: Sie behauptete, Gitea sei
von außen nicht erreichbar — eine Messung von dreißig Sekunden zeigte das
Gegenteil (§ 3.6).
---
## 1. Warum neu gebaut wird
Die heutige Windows-Fassung ist **kein Windows-Programm**. Sie ist der
Docker-Code in einer EXE:
```
RippySetup.exe (PyInstaller)
├─ docker/api/ → FastAPI, uvicorn auf 127.0.0.1:7788
├─ docker/worker/ → der Celery-Worker, nur ohne Celery
├─ docker/ui/dist/ → dieselbe React-Oberfläche wie im Browser
└─ pywebview → ein Fenster davor
```
Das Ergebnis steht im `SAVEPOINT.md`: **elf Release-Kandidaten in drei Tagen.**
Und die Funde sind fast alle vom selben Typ — Linux-Annahmen, die unter Windows
etwas anderes bedeuten:
| Fund | Was passierte |
|------|---------------|
| `timeout 4 ls -d <pfad>` als Verzeichnisprüfung | Unter Windows gibt es `timeout.exe`, aber sie kennt weder `ls` noch `-d`. Antwort war **„weg" für jedes Verzeichnis** — Rohdaten waren grundsätzlich unsichtbar. Dazu blitzten Konsolenfenster auf. |
| `shutil.which("makemkvcon")` | Findet unter Windows nie etwas — Programme liegen in `Program Files`, nicht im PATH. Die 4K-Schlüssel-Automatik lief deshalb **nie** an. |
| `os.path.isdir("/app")` | Rippy hielt sich für einen fremden Docker-Worker. Zweimal gefunden, an zwei Stellen. |
| Routen `/dev/{name}` | Das UI ruft mit `G` auf, gebaut war `/dev/G`. **Auswerfen und Disc-Scan antworteten immer mit 404.** |
| `F:\` wurde zu `F:` | Unter Windows ist `F:` ohne Trenner der *aktuelle Ordner* auf F, nicht dessen Wurzel. 16,5 GB Rohschnitt unsichtbar; der Dialog bot nur „Neu rippen" an. |
| `makemkvcon` überlebte den Elternprozess | Windows räumt Kindprozesse nicht auf. Zwei Waisen hielten das Laufwerk fest. |
| Zwei Speicher für dieselben Ordner | Die Oberfläche schrieb `outputDir` in die Datenbank, der Betrieb las `storage.medien` aus einer Konfigurationsdatei, die niemand schrieb. Eingestellt `E:\Rippy`, angezeigt `C:\Users\...\Videos\Rippy`. |
Diese Liste ist nicht das Problem — sie ist das **Symptom**. Jeder einzelne
Fund war eine Stelle, an der Code, der für einen Linux-Container geschrieben
wurde, unter Windows etwas anderes bedeutet. Solange die Grundlage ein
umgebogener Container ist, ist der nächste Fund dieser Sorte nur eine Frage der
Zeit.
**Die Entscheidung ist deshalb nicht „das ist schlecht gebaut", sondern: das
ist am falschen Ort gebaut.** Ein Windows-Programm fängt bei Windows an, nicht
bei einem Container, dem man Windows beibringt.
---
## 2. Die Entscheidungen (Commander, 30.08.2026)
Neun Entscheide, alle am selben Tag — die Entscheide 6, 7 und 9 in ihrer
heutigen Form aus der Prüfung der ersten Fassung. Sie stehen hier, damit
später nachvollziehbar ist, was Entscheidung war und was Vorschlag.
### Entscheid 1 — Alles TypeScript/Node
Es gibt **keinen Python-Anteil** in Rippy für Windows. Kein Sidecar, keine
mitgelieferte Laufzeit, keine IPC-Brücke zwischen zwei Sprachen. Eine Sprache,
eine Werkzeugkette, ein Setup.
*Was das kostet:* Die rund 18.000 Zeilen Python werden nicht übernommen. Was
übernommen wird, ist ihr **Wissen** — die Parser-Regeln, die gemessenen
Fallen, die Kommandozeilen (§ 10). Das ist der teure Teil an ihnen, und der
wandert vollständig mit.
*Was das bringt:* Der ganze Fehlertyp aus § 1 kann nicht mehr entstehen. Es
gibt keinen Container-Pfad, den jemand vergessen könnte, weil es nie einen gab.
### Entscheid 2 — Die Docker-Version bleibt unangetastet
Rippy auf der VM läuft weiter wie heute. Es wird nicht angefasst, nicht
migriert, nicht mitgepflegt. Zwei getrennte Produkte, keine geteilten Module.
*Begründung:* Geteilte Module zwischen zwei Betriebssystemen sind genau die
Konstruktion, an der v1 gelitten hat — drei Dateien lagen byte-identisch
doppelt im Repo, und `db.py` schrieb es sogar in den Kopfkommentar: *„Wer die
Struktur ändert, ändert BEIDE Dateien."*
### Entscheid 3 — Reiner Einzelplatz
Ein Rechner macht alles: erkennen, rippen, komprimieren, ablegen. Kein Server,
keine Netz-Queue, keine Worker-Verwaltung, keine Pfad-Übersetzung zwischen
Maschinen.
*Was damit ersatzlos wegfällt:* Celery, Redis, Postgres, die Worker-Tabelle,
`worker_direct`, die Lease-Mechanik über Netz, `RIPPY_PATH_MAP`, die
Zombie-Erkennung, die Mount-Wache, das Rate-Limit (es gibt keinen fremden
Client mehr), `/worker-setup/*`, die Encoder-Auswahl je Knoten.
Grob geschätzt sind das **40 % der heutigen Komplexität** — und zwar der Teil,
der die meisten Fehlerbilder erzeugt hat.
*Was bleibt:* Die Ablage darf trotzdem auf dem NAS liegen. Ein UNC-Pfad
(`\\nas\medien`) oder ein verbundenes Netzlaufwerk ist für Rippy nur ein Pfad.
Windows mountet, Rippy prüft — dieselbe Trennung wie in Entscheid 3 des
v2-Konzepts, nur ohne Container drumherum.
### Entscheid 4 — Eigener Branch im selben Gitea-Repo
Der Branch `worktree-windows-electron` trägt v5. `main` bleibt unberührt und
jederzeit deploybar — deploybar heißt: Die VM zieht von Gitea, nicht von
diesem Ordner. Welcher Branch hier gerade ausgecheckt ist, ist für den Deploy
egal.
*Nachtrag 30.08.2026:* Anfangs lebte der Branch in einem Git-Worktree unter
`.claude/worktrees/windows-electron`, damit die Konzept-Arbeit `main` nicht
anfassen musste. Der Worktree ist aufgelöst — Git erlaubt denselben Branch
nur an einem Ort, und der doppelte Checkout blockierte den Start neuer
Sitzungen auf dem Branch. Seitdem wird v5 normal im Projektordner
ausgecheckt; der Branch-Name erinnert noch an die Herkunft.
### Entscheid 5 — Kein Code-Signing-Zertifikat
*„ist egal, brauchen wir nich"*
Die EXE bleibt unsigniert. Zwei Folgen, beide bewusst getragen:
1. **SmartScreen.** Beim ersten Start eines Downloads zeigt Windows den
Hinweis „Der Computer wurde geschützt"; über **Weitere Informationen →
Trotzdem ausführen** geht es weiter. Das gehört in die Anleitung, nicht
unter den Teppich — samt der Angabe, woran man die echte Datei erkennt
(Größe und Prüfsumme stehen beim Release).
2. **Updates ohne Echtheitsprüfung.** Ohne Signatur kann `electron-updater`
ein heruntergeladenes Update nicht auf Echtheit prüfen. Die Absicherung
liegt damit vollständig beim Transportweg — deshalb die HTTPS-Pflicht in
§ 6.8, Regel 1.
### Entscheid 6 — Updates aus dem öffentlichen Gitea
*„Gerne das Gitea Release, mit Möglichkeit dass auch EXTERNE diese Updates
fahren können"* — und in der zweiten Fassung konkretisiert: **das öffentliche
Gitea direkt, kein Spiegel.**
Die erste Fassung hielt das für ein Infrastruktur-Problem („Gitea ist von
außen nicht erreichbar") und entwarf drei Auswege. Die Messung in § 3.6 hat
das erledigt: **Gitea ist längst öffentlich erreichbar**
`https://git.tobisniceshomelab.ddnsfree.com`, mit gültigem TLS-Zertifikat,
Lesen ohne Anmeldung. Die HTTPS-Pflicht aus § 6.8 ist damit erfüllbar, und
zwar für den Commander **und** für Externe, ohne dass irgendetwas neu
aufgebaut wird.
Was bleibt:
- Die Update-Adresse steht in der Konfiguration, vorbelegt mit den Releases
dieses Gitea. Wer einen eigenen Server hat, trägt seine eigene Adresse ein —
`electron-updater` braucht nur einen statischen HTTPS-Ort.
- Jedes Release besteht aus **drei Dateien**, nicht zwei: `latest.yml`
(die Update-Auskunft), das Setup und die `.blockmap` (macht aus einem
Voll-Download einen Differenz-Download).
- ⚠️ **Ein Handgriff für Etappe W-6:** Gitea-Release-Downloads hängen an
ihrem Tag (`/releases/download/v5.0.1/...`) — `electron-updater` braucht
für `latest.yml` aber eine **feste** Adresse. Ob Gitea 1.27 das
GitHub-Muster `releases/latest/download/...` bedient, ist ungeprüft (2024
war es dort noch ein offener Wunsch). Falls nein, ist die Rückfallebene
simpel: ein festes Update-Tag, dessen drei Dateien der Bau bei jedem
Release ersetzt. Beides ist eine Bau-Einstellung, keine Programmänderung.
### Entscheid 7 — Der alte Windows-Weg wird entfernt, sofort, auch aus `main`
*„Nein — direkt weg, auch aus dem Original Repo. Das neue Standalone Windows
Rippy wird ein komplett neues Produkt und kein Aufbau auf die
Docker-Architektur."*
Die PyInstaller-Fassung bleibt **nicht** parallel installierbar, und ihr Code
verlässt das Repo. Die erste Fassung hatte das Aufräumen auf „erst wenn v5
installierbar ist" gelegt; der Commander hat bei der Prüfung der zweiten
Fassung anders entschieden: **sofort, nicht erst nach Etappe W-6.**
Was das bedeutet, gehört ehrlich hingeschrieben: Zwischen dem Aufräumen und
W-6 liefert das Repo **kein installierbares Windows-Rippy**. Die auf dem
Commander-PC installierte rc11-Fassung läuft lokal weiter, bekommt aber keine
Reparaturen mehr; wer in der Zwischenzeit rippen will, nutzt sie oder die
Docker-Version. Das ist der Preis für ein Repo ohne Zwitter — und er ist
bewusst bezahlt.
**Das ist kein Widerspruch zu Entscheid 2.** „Docker bleibt unangetastet" meint
die Docker-*Funktion*; der Windows-Standalone-Zweig ist gerade nicht Docker,
sondern der Fremdkörper darin. Was genau entfernt wird, steht in § 11 — mit
einer Unterscheidung, die beim Aufräumen entscheidend ist.
### Entscheid 8 — Der Remote-Worker bleibt bei Docker
*„Das soll NICHT Teil des neuen Standalone-Produktes sein, da dieser NUR für
die Docker-Variante verwendet wird."*
Der „Rippy Worker" auf einem Hilfsrechner ist eine Funktion der
Docker-Installation, kein Standalone-Rippy. Er wird weder abgeräumt noch nach
v5 übernommen. Warum das so ist und was genau er tut, steht in § 11.1 — dort,
wo die Unterscheidung gebraucht wird.
### Entscheid 9 — Zielgruppe: Bekannte mit Link
Neu in der zweiten Fassung: Rippy v5 wird **kein öffentliches Produkt**. Der
Commander gibt die Download-Adresse gezielt weiter — Familie, Freunde. Keine
öffentliche Ankündigung, kein Support-Anspruch Fremder.
Konsequenzen:
- Anleitung, SmartScreen-Erklärung und Prüfsummen bleiben **schlank** — sie
müssen einen wohlwollenden Bekannten durchbringen, kein anonymes Publikum.
- Die **Lizenztexte gehören trotzdem dazu.** Weitergabe an Bekannte ist
Weitergabe: HandBrake (GPL-2) und FLAC (BSD-artig) verlangen Lizenztext und
Quellverweis im Paket (§ 9). Das ist keine Formalie für später, sondern
Bestandteil von Etappe W-6.
- Das Repo bleibt lesbar wie bisher; beworben wird nichts. Sollte v5 eines
Tages doch öffentlich werden, ändert sich am Programm nichts — nur an
Anleitung und Sorgfaltstiefe.
---
## 3. Was gemessen wurde, bevor entschieden wurde
Electron bedeutet: Die Arbeit macht Node, nicht Python. Damit hängt das ganze
Konzept an einer Frage, die vorher niemand geprüft hat:
> **Kann Node überhaupt alles, was Rippy am Laufwerk braucht?**
Wäre die Antwort nein, wäre das Konzept wertlos. Also gemessen — am
30.08.2026, auf diesem Rechner, am echten Laufwerk. Die Skripte liegen in
`beweise/` und sind dort nachstellbar dokumentiert.
### 3.1 Die Werkzeuglage
```
Node v24.19.0
npm 11.17.0
Electron 44.0.0 (Chromium 152.0.7977.54 · Node 24.18.1)
WebView2 151.0.4129.107
Laufwerk G: HL-DT-ST BD-RE BU40N USB Device (Medium eingelegt)
```
Zur Einordnung, am 30.08.2026 gegen das Netz geprüft: Electron 44 ist die
aktuelle stabile Linie, koffi 3.1.6 die aktuelle Version, Node 24 ist LTS.
Das Konzept baut also nicht auf Auslaufendem auf — was Electron-Versionen
betrifft, gilt aber die Pflege-Regel in § 6.8.
### 3.2 Win32 aus Node — die Laufwerks-Steuerung
`koffi` 3.1.6 installiert, dieselben Steuercodes wie in
`src/rippy/drives/win_ioctl.py` (die dort aus dem `CTL_CODE`-Makro hergeleitet
sind, nicht abgeschrieben). Nur lesende Aufrufe, nichts ausgeworfen:
```
1. GetLogicalDrives + GetDriveTypeW -> optische Laufwerke: [ 'G' ]
2. CreateFileW \\.\G: (Zugriff 0) -> offen
CreateFileW \\.\G: (GENERIC_READ) -> offen
3. IOCTL_STORAGE_QUERY_PROPERTY -> Hersteller: HL-DT-ST | Modell: BD-RE BU40N
| Rev: 1.03 | Serial: 0025114C0149
4. IOCTL_STORAGE_CHECK_VERIFY2 -> Medium eingelegt
5. IOCTL_CDROM_DISK_TYPE -> Win32-Fehler 50
6. IOCTL_DISK_GET_LENGTH_INFO -> 33.759.690.752 Bytes (33,76 GB) => Blu-ray
```
**Punkt 5 ist kein Fehlschlag, sondern eine Bestätigung.** Fehler 50 ist
`ERROR_NOT_SUPPORTED` — genau das, was auch der Python-Treiber an diesem
Laufwerk sieht (`SAVEPOINT.md`, rc11). Node verhält sich identisch. Und die
Lehre daraus steht schon im alten Code: **Die Disc-Einordnung darf sich nicht
auf `IOCTL_CDROM_DISK_TYPE` verlassen — die Größe ist der verlässliche Weg.**
### 3.3 Die Prozess-Leine — überlebt makemkvcon einen Absturz?
Der Befund aus rc10: Windows räumt Kindprozesse nicht auf. Ein hart beendeter
Rippy hinterlässt ein `makemkvcon`, das das Laufwerk festhält. Die Lösung dort
war eine Windows-Arbeitsgruppe (Job Object) mit
`JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE`.
In Node nachgestellt — Job Object gesetzt, Kind gestartet, Eltern mit
`taskkill /F` **ohne** `/T` abgeschossen (genau das, was bei einem Absturz und
beim Drüber-Installieren passiert):
```
1. CreateJobObjectW -> Arbeitsgruppe angelegt
2. SetInformationJobObject -> KILL_ON_JOB_CLOSE gesetzt
3. AssignProcessToJobObject -> dieser Prozess hängt in der Gruppe
4. Kindprozess gestartet, PID 16040
vor dem Abschuss: Eltern lebt: True Kind lebt: True
taskkill /F /PID 29488 -> ERFOLGREICH
danach: Eltern lebt: False Kind lebt: False
```
### 3.4 Datenbank ohne native Abhängigkeit — in Electron gemessen
Node 24 bringt SQLite mit (`node:sqlite`, `DatabaseSync`). Keine
`better-sqlite3`-Kompilierung, kein `node-gyp`, kein Visual-Studio-Build in der
Bau-Kette.
Die erste Fassung musste hier eine Warnung setzen: Electron kompiliert sein
Node mit eigenen Flags, und ob `node:sqlite` dort vorhanden ist, war nur für
Node pur gemessen. **Das ist erledigt — am 30.08.2026 in Electron 44.0.0
gemessen, und zwar im `utilityProcess`**, also exakt dem Kontext, in dem
`kern/speicher/db.ts` laufen wird (§ 4.1):
```
KERN-ANTWORT: {"ok":true,"wert":"geht","node":"24.18.1"}
Kern beendet mit 0
```
Anlegen, Schreiben, Lesen — alles grün. Die Rückfallebene `better-sqlite3`
wird nicht gebraucht. Skripte: `beweise/sqlite-main.js` + `sqlite-kern.js`.
### 3.5 Ein Nebenbefund für die Bau-Kette — zweimal bestätigt
`npm` 11 blockiert Install-Skripte fremder Pakete standardmäßig:
```
npm warn allow-scripts koffi@3.1.6 (install: node ./cnoke.cjs -P . --prebuild --release)
```
`koffi` lädt trotzdem — es bringt vorgebaute Binärdateien mit. **Electron
selbst trifft die Sperre härter:** Sein Install-Skript lädt die eigentliche
Programmdatei herunter; wird es geblockt, liegt nach `npm install` ein Paket
**ohne `dist/electron.exe`** da, und nichts startet. Bei der sqlite-Messung
(§ 3.4) selbst hineingelaufen; `node node_modules/electron/install.js` holt
die Binary nach. Beides gehört in die Bau-Anleitung, sonst sucht jemand eines
Tages an der falschen Stelle.
### 3.6 Die Update-Quelle — gemessen statt vermutet
Die erste Fassung behauptete: *„Gitea läuft auf `192.168.178.153` im Heimnetz.
Von außen ist das nicht erreichbar."* Am 30.08.2026 von außerhalb des
Heimnetzes gemessen:
```
https://git.tobisniceshomelab.ddnsfree.com/Hitonabi/rippy
-> lädt anonym, ohne Anmeldung, über den Reverse-Proxy des Commanders
-> TLS-Zertifikat gültig
-> Gitea 1.27.2 (/api/v1/version)
-> bisher 0 Releases, 0 Tags — der Release-Weg ist frei
```
Die interne Adresse `192.168.178.153:3000` existiert daneben weiter; für
Updates zählt die öffentliche. Damit ist Entscheid 6 keine Infrastruktur-Frage
mehr, sondern eine Bau-Einstellung. Offen bleibt nur die
Fest-Adresse-Feinheit für `latest.yml` (Entscheid 6, ⚠️-Punkt) — die wird in
W-6 am ersten echten Release gemessen.
---
## 4. Aufbau
### 4.1 Drei Teile, drei Prozesse
```
┌──────────────────────────────────────────────────────────────┐
│ Rippy.exe — Electron Main-Prozess │
│ ├─ Tray-Symbol, Fensterverwaltung, Autostart, Selbst-Update │
│ └─ hält die Prozess-Leine (§ 3.3) — ALLES stirbt mit ihm │
└───────────┬─────────────────────────────┬────────────────────┘
│ IPC │ MessagePort
┌───────────▼──────────────┐ ┌──────────▼───────────────────────┐
│ Fenster (Renderer) │ │ Kern (utilityProcess) │
│ React + Tailwind │◄──┤ die GANZE Arbeit │
│ zeigt an, sonst nichts │ │ ├─ Laufwerkswache (koffi/Win32) │
│ │ │ ├─ Ablauf-Steuerung │
│ kein Node-Zugriff │ │ ├─ Auftragstabelle (SQLite) │
│ (contextIsolation) │ │ └─ Werkzeuge als Kindprozesse: │
│ │ │ makemkvcon · HandBrakeCLI │
└──────────────────────────┘ └──────────────────────────────────┘
```
**Warum drei und nicht einer:**
1. **Ein Rip dauert 3090 Minuten.** Läuft er im Main-Prozess, friert die
Oberfläche ein — Electrons eigene Dokumentation sagt das ausdrücklich.
2. **Der Kern darf abstürzen, ohne das Fenster mitzureißen.** Dann steht dort
„Der Kern ist abgestürzt, ich starte ihn neu" statt eines toten Fensters.
3. **Das Fenster darf zugehen, ohne den Rip zu beenden.** Rippy lebt dann im
Tray weiter — genau wie heute, nur ohne den Umweg über einen HTTP-Server.
**Warum `utilityProcess` und nicht `child_process.fork`:** Electrons eigene
Empfehlung. `utilityProcess` kann einen `MessagePort` direkt zum Renderer
aufbauen — der Fortschritt eines Rips geht damit ohne Umweg über den
Main-Prozess ins Fenster. Und § 3.4 hat gezeigt: `node:sqlite` steht genau
dort zur Verfügung.
**Was es NICHT gibt:** keinen HTTP-Server, keinen Port, kein `localhost:7788`,
keine REST-API, kein SSE. Fenster und Kern reden über Electrons IPC. Ein
lokaler Webserver in einer Einzelplatz-Anwendung ist eine offene Tür ohne
Gegenwert — und er war der Grund für das Rate-Limit, den Proxy-Kummer und die
Polling-Last im alten Rippy.
### 4.2 Ordner
```
rippy-windows/
├── package.json
├── electron.vite.config.ts
├── src/
│ ├── haupt/ Electron Main
│ │ ├── index.ts Start, Einzelinstanz-Sperre, Leine setzen
│ │ ├── fenster.ts Fenster anlegen, Zustand merken
│ │ ├── tray.ts Symbol + Kontextmenü
│ │ ├── autostart.ts HKCU\...\Run
│ │ └── update.ts electron-updater
│ │
│ ├── kern/ utilityProcess — die Arbeit
│ │ ├── laufwerk/
│ │ │ ├── win32.ts DER EINZIGE ORT MIT koffi
│ │ │ ├── wache.ts Einwurf/Auswurf erkennen (§ 5, Schritt 1)
│ │ │ └── disc.ts Typ, Label, Größe, TOC
│ │ ├── metadaten/
│ │ │ ├── tmdb.ts tvdb.ts omdb.ts musicbrainz.ts
│ │ │ ├── discmerkmale.ts Label + Laufzeiten + BDMV-Titel
│ │ │ └── zuordnung.ts Treffer + Sicherheitsgrad
│ │ ├── rip/
│ │ │ ├── makemkv.ts Kommandobau + Aufruf
│ │ │ ├── parser.ts TINFO/SINFO/PRGV/MSG (rein, testbar)
│ │ │ ├── audiocd.ts Roh-Sektoren + FLAC
│ │ │ └── iso.ts Datendiscs 1:1 sichern
│ │ ├── komprimieren/
│ │ │ ├── handbrake.ts Kommandobau + Aufruf + Fortschritt
│ │ │ ├── encoder.ts GEMESSEN, nicht behauptet
│ │ │ └── presets.ts
│ │ ├── ablage/
│ │ │ ├── struktur.ts Jellyfin/Emby/Kodi/Plex
│ │ │ ├── nfo.ts poster.ts medienserver.ts
│ │ ├── ablauf/
│ │ │ ├── pipeline.ts die Verkettung — NUR HIER
│ │ │ ├── zustand.ts Zustandsmaschine
│ │ │ └── auftraege.ts Warteschlange, Slots
│ │ ├── werkzeuge/
│ │ │ ├── katalog.ts wo liegt makemkvcon/HandBrakeCLI/flac
│ │ │ ├── beschaffen.ts holen, prüfen, einrichten
│ │ │ ├── schluessel.ts 4K-Schlüsselkette, Beta-Key samt Ablaufdatum (§ 9.1)
│ │ │ └── leine.ts Job Object (§ 3.3)
│ │ └── speicher/
│ │ ├── db.ts SQLite (node:sqlite — § 3.4 gemessen)
│ │ ├── einstellungen.ts EINE Quelle (§ 6.7)
│ │ └── bibliothek.ts
│ │
│ ├── gemeinsam/ Typen + Ereignis-Schema — Fenster UND Kern
│ └── fenster/ React
├── bau/ electron-builder, NSIS-Skript, Icon
└── test/ Vitest
```
### 4.3 Sechs Regeln, die den Aufbau tragen
Jede ist aus einem bezahlten Fehler abgeleitet. Sie sind mechanisch prüfbar
und bekommen je einen Wächter-Test.
**R1 — Win32 lebt nur in `laufwerk/win32.ts`.**
Kein `koffi`-Import irgendwo sonst. Ein Wächter-Test greift bei jedem anderen
Fundort. *(Grund: In v1 rief `main.py` `eject()` direkt auf — deshalb brauchte
der API-Container Geräte-Zugriff und `SYS_ADMIN`.)*
**R2 — Ein Fehlschlag löscht nie einen Zustand.**
Kein `catch { return [] }`. Wer nichts Neues weiß, behält, was er wusste.
*(Grund: Fünfmal `catch(() => [])` im alten UI — jeder verpasste Abruf hieß
„es gibt keine Jobs", die Liste leerte sich im Sekundentakt. Der Commander
meldete es als „wird oft neu geladen".)*
**R3 — Auswerfen heißt: entriegeln, auswerfen, nachsehen.**
In dieser Reihenfolge. Kein Rückgabewert gilt als Beweis. *(Grund: `CDROMEJECT`
quittiert auf einem verriegelten Laufwerk Erfolg und tut nichts. MakeMKV
verriegelt die Tür während des Rips.)*
**R4 — Jede Hintergrundschleife meldet ihren Fehler.**
Kein stilles `catch {}`. *(Grund: Ein `except Exception: pass` in einer
Vorrats-Schleife hat einmal eine Stunde gekostet.)*
**R5 — Externe Schnittstellen werden gemessen, nicht erinnert.**
Jeder Kommandozeilen-Schalter, jeder IOCTL-Code trägt im Kommentar, wo er
herkommt. *(Grund: `--audio-codec` gibt es bei HandBrakeCLI nicht — es heißt
`-E`/`--aencoder`. Ein unbekannter Schalter ist für HandBrake kein Fehler:
Rückgabewert 0. Das kostete einen fertigen 16,5-GB-Rip, und ein Test hatte den
Fehler festgeschrieben statt ihn zu finden.)*
**R6 — Alles läuft an der Leine.**
Jeder Werkzeugaufruf hängt in der Arbeitsgruppe aus § 3.3. Ausgenommen ist nur
das Aufräum-Skript der Deinstallation — es muss Rippy überleben.
---
## 5. Der Ablauf — was passiert, wenn eine Disc reingeht
Der rote Faden des Commanders: **so viel Arbeit abnehmen wie möglich.**
```
1. Disc rein
│ Wache im 3-Sekunden-Takt (IOCTL_STORAGE_CHECK_VERIFY2 — § 3.2)
│ Verfeinerung später: WM_DEVICECHANGE-Ereignis (siehe unten)
2. Was ist das?
│ Größe (33,76 GB → Blu-ray) · Audio-TOC? · Dateisystem lesbar?
│ → Film / Serie / Musik / Daten
3. Was ist da drauf? ← DER SCHRITT, DER HEUTE ZU DÜNN IST
│ Disc-Label · Titel-Laufzeiten · BDMV-Titel · DVD-IFO · CD-DiscID
│ → TMDb / TVDb / OMDb / MusicBrainz
│ → Treffer mit SICHERHEITSGRAD
4. Sicher genug?
│ ja → sofort losrippen, nur Bescheid sagen (Zero-Click)
│ nein → fragen: Fenster hoch, Vorschläge zur Wahl
5. Rippen makemkvcon, verlustfrei, Fortschritt aus PRGV
6. Komprimieren HandBrakeCLI, Encoder GEMESSEN, Preset je Disc-Typ
7. Ablegen Zielstruktur · NFO · Poster/Fanart
8. Melden Medienserver anstoßen · Benachrichtigung · Auswerfen
9. Merken Bibliothek fortschreiben — beim nächsten Mal erkannt
```
**Warum der Takt Plan A ist und das Ereignis nicht.** Die erste Fassung
versprach „Ereignis, kein Poll" — die Prüfung hat die Reihenfolge umgedreht.
`WM_DEVICECHANGE` ist eine **Fenster**-Nachricht: Sie wird nur an Fenster mit
Nachrichtenschleife zugestellt, und der Kern hat keines. Beide Auswege sind
machbar, aber krumm — entweder fängt der Main-Prozess die Nachricht am
Fenster ab und muss dann die Frage „welches Laufwerk?" aus einer
Zeiger-Struktur klauben, oder der Kern baut sich per koffi ein unsichtbares
Nachrichten-Fenster samt eigener Schleife. Der 3-Sekunden-Takt dagegen ist
eine Zeile (`IOCTL_STORAGE_CHECK_VERIFY2`, in § 3.2 gemessen, praktisch
kostenlos), und bei einem Vorgang, der 3090 Minuten dauert, sind drei
Sekunden Erkennungsverzögerung bedeutungslos. Also: **Takt zuerst, Ereignis
als spätere Verfeinerung** — und wenn das Ereignis kommt, bleibt der Takt als
Rückfallebene bestehen, falls die Nachricht ausbleibt.
**Was an Schritt 3 neu ist.** Heute nimmt Rippy im Wesentlichen das Disc-Label
und die längste Laufzeit. Das reicht bei „PIRATES_OF_THE_CARIBBEAN", aber nicht
bei „LOGICAL_VOLUME_ID" — und dann fragt Rippy, obwohl die Antwort auf der
Disc steht. Vier Quellen kommen dazu:
| Quelle | Wo | Was sie liefert |
|--------|-----|-----------------|
| **BDMV-Metadaten** | `BDMV/META/DL/bdmt_*.xml` auf der Blu-ray | Der vom Studio hinterlegte Disc-Titel, oft mit Sprachvarianten. **Deutlich besser als das Volume-Label.** |
| **Titel-Struktur** | makemkvcon `info` | Die Anzahl und Länge der Titel ist ein Fingerabdruck: 1 langer + viele kurze = Film; 613 gleich lange = Serienstaffel. |
| **DVD-Struktur** | `VIDEO_TS`, IFO-Dateien | Kapitelzahl und Titelsatz-Aufbau |
| **CD-DiscID** | Audio-TOC | Bereits gebaut und bewiesen (rc8): Spurlage → MusicBrainz → Album, Interpret, Jahr, jeder Titel. |
Dazu ein **Sicherheitsgrad**, der ehrlich ist. Rippy sagt nicht „das ist
Akira", sondern „das ist sehr wahrscheinlich Akira (1988)" — und fragt nur,
wenn es das nicht sagen kann. Die Schwelle ist einstellbar; wer will, lässt
Rippy immer fragen, und wer will, lässt es immer durchlaufen.
**Und wenn Rippy sich irrt:** Der Titel lässt sich nachträglich ändern, und die
Ablage wandert mit. Das ist der Unterschied zwischen „falsch geraten" und
„falsch abgelegt".
---
## 6. Die Features
### 6.1 Automatische Erkennung (§ 5, Schritt 24)
Siehe oben. Kernsatz: **Rippy fragt nur, wenn es wirklich nicht weiß.**
### 6.2 Rippen
`makemkvcon` im Robot-Modus (`--robot --messages=-stdout --progress=-same`),
verlustfrei. Der Parser (TINFO/SINFO/PRGV/MSG) wandert aus dem Python-Code
nach TypeScript — er ist reine Textverarbeitung und damit vollständig
testbar, ohne Laufwerk.
Was aus dem alten Rippy zwingend mitkommt:
- **Lesefehler heißen Lesefehler.** MakeMKV kann 1 von 2 Titeln sichern und
sich trotzdem mit 0 beenden. Rippy warnt nach dem Rip — auch und gerade dann,
wenn er als Erfolg endete — und nennt die **MSG-Nummer**: MakeMKVs Texte sind
übersetzt, die Nummern nicht.
- **Erst nachsehen, dann rippen.** Liegt keine Disc drin, sagt Rippy das in
einem Satz, statt zwei Minuten in makemkvcon zu laufen und mit Code 11 zu
enden. Aber: Wenn das Nachsehen selbst scheitert, wird trotzdem gerippt —
„ich weiß es nicht" darf nie zu „es geht nicht" werden.
- **Die Ausgabe binär lesen.** makemkvcon mischt Kodierungen; ein `ü` im Pfad
hat die Kompression einmal getötet (`UnicodeDecodeError` auf Byte 0x81).
### 6.3 Audio-CDs
Der Weg aus rc8, bewiesen: Roh-Sektoren über Win32 lesen (eine Audio-CD hat
kein Dateisystem — die `Track01.cda` sind 44-Byte-Platzhalter), dann durch
`flac.exe`. Dazu MusicBrainz über die DiscID.
Zwei Fallen, beide gemessen und beide dokumentiert: Die DiscID rechnet **mit**
den 150 Frames Vorlauf, das Lesen **ohne**. Und die Leseadresse zählt in
2048er-Einheiten, obwohl ein Audio-Sektor 2352 Bytes hat.
### 6.4 ISO-Backup für Datendiscs *(neu)*
Die einzige echte Lücke gegenüber ARM. Wird eine Disc weder als Film noch als
Musik erkannt, wird sie 1:1 gesichert statt abgelehnt: sequenziell aus
`\\.\G:` lesen, Länge aus `IOCTL_DISK_GET_LENGTH_INFO` (in § 3.2 gemessen:
liefert exakte Bytes). Kein Fremdwerkzeug nötig.
### 6.5 Mehrere Laufwerke gleichzeitig *(neu)*
Laufwerke sind eigenständige Objekte mit stabiler Kennung (Seriennummer, nicht
Buchstabe — `0025114C0149` in § 3.2 gemessen). Ein Rip-Auftrag belegt genau
ein Laufwerk; die Zahl gleichzeitiger Rips und Kompressionen ist einstellbar.
> ⚠️ **Annahme, zu messen bevor sie festgeschrieben wird:** Mehrere
> `makemkvcon`-Prozesse teilen sich ein Datenverzeichnis
> (`_private_data.tar`, `settings.conf`). Der Entwurf sieht deshalb vor:
> **Rip-Phase parallel, Schlüssel-Phase serialisiert.** Das ist aus der
> Aktenlage abgeleitet, nicht gemessen — und gehört an zwei Laufwerken geprüft,
> bevor die Grenze im Code steht.
### 6.6 Bibliothek *(neu)*
Eine Ansicht über alles Gerippte: Cover, Größe, Datum, Ablageort, Dauer des
Vorgangs. Und der praktische Teil: **Rippy erkennt beim Einlegen, wenn eine
Disc schon einmal durchgelaufen ist** (über die Disc-Merkmale aus § 5) und
fragt nach, statt stumm zu doppeln.
### 6.7 Arbeitsverzeichnis und Ablage — eine Quelle
Der Fund aus rc11: Die Oberfläche schrieb in die Datenbank, der Betrieb las
aus einer Konfigurationsdatei, die niemand schrieb. Eingestellt `E:\Rippy`,
angezeigt `C:\Users\...\Videos\Rippy`.
In v5 gibt es **genau einen Speicher für Einstellungen** (`speicher/einstellungen.ts`,
SQLite). Ein Wächter-Test prüft mechanisch, dass kein zweiter Ort entsteht.
Dazu die Pfad-Regeln aus rc11, als reine Funktionen mit Tests:
- Eine Laufwerkswurzel behält ihren Trenner (`F:\`, nicht `F:`)
- UNC-Wurzeln bleiben absolut
- Verzeichnisprüfungen sind **windows-nativ** — kein `timeout ls -d`
- Pro Rip darf ein anderes Arbeitsverzeichnis gewählt werden, und **gesucht
wird später unter der Wahl des Jobs**, nicht unter der heutigen Einstellung
### 6.8 Selbst-Update *(neu)*
`electron-updater` gegen die Releases im öffentlichen Gitea (Entscheid 6).
Rippy prüft beim Start und danach täglich, lädt im Hintergrund und installiert
beim nächsten Start — **nie mitten in einem Rip**.
Die Quelle steht in der Konfiguration und ist vorbelegt. Ein nicht erreichbarer
Update-Server ist kein Fehler, sondern eine Zeile im Protokoll — das gilt
ausdrücklich auch, wenn das Heimnetz des Commanders gerade nicht antwortet.
**Vier Regeln, die zum Entscheid gehören:**
1. **Nur HTTPS.** Ohne Code-Signing (Entscheid 5) kann `electron-updater` ein
heruntergeladenes Update nicht auf Echtheit prüfen — die Absicherung liegt
damit ganz beim Transportweg. Eine `http://`-Update-Adresse wird abgelehnt,
nicht stillschweigend akzeptiert. (Die Prüfsumme aus `latest.yml` sichert
nur die Übertragung — sie kommt über denselben Kanal wie die Datei und ist
deshalb kein Ersatz für den sicheren Kanal selbst.)
2. **Nie während eines Rips.** Weder herunterladen noch installieren. Ein
Neustart mitten in einem 90-Minuten-Rip wäre der teuerste Fehler, den ein
Update-Mechanismus machen kann.
3. **Ein Fehlschlag ist sichtbar, aber nicht laut.** Die Prüfung darf still
scheitern (WLAN weg, Server aus) — aber der Zeitpunkt der letzten
erfolgreichen Prüfung steht im Über-Dialog. Wer nie prüft, weiß sonst nicht,
dass er nie prüft.
4. **Die Version, die läuft, ist ablesbar.** Im Fenster und im Protokollkopf.
Eine Fehlermeldung ohne Versionsnummer ist bei einem sich selbst
aktualisierenden Programm die halbe Miete verloren.
**Und eine Pflege-Regel, die das Selbst-Update erst wertvoll macht:**
Electron bringt etwa alle acht Wochen eine neue Hauptversion und pflegt nur
die letzten drei — wer stehen bleibt, sitzt nach rund einem halben Jahr auf
einem Chromium mit bekannten Sicherheitslücken. Deshalb: **Bei jedem
Rippy-Release wird geprüft, ob ein Electron-Sprung ansteht.** Das Verteilen
kostet dank Selbst-Update nichts; das Nicht-Verteilen kostet irgendwann
Vertrauen.
### 6.9 Erst-Einrichtung *(neu gedacht)*
Der Befund des Commanders zur heutigen Fassung: *„Dann hätte ich beim Setup
auch erwartet, dass ein echtes Setup passiert — wo will ich das hinspeichern,
ein Pre-Requirement-Check usw. Da kommt gar nichts, Rippy geht einfach auf."*
Der Assistent läuft **nach** der Installation, im Programm selbst, in fünf
Schritten:
| Schritt | Was passiert |
|---------|--------------|
| 1 Willkommen | Was Rippy tut, in vier Sätzen |
| 2 Prüfung | Laufwerke · Werkzeuge · Plattenplatz · Internet · WebView2 — **jeder Befund sagt, was zu tun ist** |
| 3 Werkzeuge | Was fehlt, wird geholt (§ 9) — mit Fortschritt, nicht mit einem eingefrorenen Fenster |
| 4 Ordner | Ablage und Arbeitsverzeichnis, mit echten Windows-Ordnerdialogen und Platzanzeige |
| 5 Automatik | Wie viel soll Rippy allein entscheiden? Je Disc-Typ einstellbar. |
Zwei Regeln, die aus dem alten Setup übernommen werden:
- **Nur ein Fehler blockiert.** Eine Warnung, die den Knopf sperrt, ist
Bevormundung; ein Fehler, der nur warnt, ist eine Falle. Kein optisches
Laufwerk ist eine **Warnung** — eine reine Komprimier-Maschine ist ein
vorgesehener Betriebsfall.
- **Ein Setup meldet nie Erfolg, während ein Pflichtwerkzeug fehlt.** Der
Bestand wird VOR und NACH dem Versuch gelesen; berichtet wird, was danach
wirklich da ist.
### 6.10 Was unverändert erhalten bleibt
Alles, was der Commander heute benutzt:
Kompression je Disc-Typ abwählbar (4K verlustfrei durchreichen) · Preset-Wahl
je Typ · Sprachwahl für Ton und Untertitel vor dem Rip · Original behalten ·
Vollautomatik · Nur-Hauptfeature · automatischer Auswurf · Medienserver-Refresh
(Jellyfin/Emby/Plex/Kodi) · NFO + Poster + Fanart · Serien-Staffelablage mit
Episoden-Zuordnung per Laufzeitabgleich (**nur bei eindeutiger Zuordnung wird
umbenannt**) · Webhook-Benachrichtigungen (Discord/Slack/ntfy/generisch) ·
Live-Log · Job-Detailansicht · Wiederholen ab Rip oder ab Kompression ·
4K-Schlüsselkette in drei Stufen · Restzeit-Schätzung.
---
## 7. Die Oberfläche
**Entscheid:** Neu bauen, Look behalten.
Wiedererkennbar bleibt die Design-Sprache — dunkel/hell, die Kachel-Anordnung,
das Kino-Banner, die Farbwelt. Neu ist, dass die Oberfläche eine
**Einzelplatz-Anwendung** bedient statt eines Docker-Clusters.
**Was verschwindet:** Worker-Verwaltung · Container-Platte · „Prüfen:
`docker compose ps`" · Speicherziele-Einhängen · Pfad-Map · Worker-Setup-Paket ·
Encoder-Auswahl je Knoten · alles, was einen zweiten Rechner voraussetzt.
**Was dazukommt:**
- Echte **Windows-Ordnerdialoge** statt eines nachgebauten Dateibrowsers im
Web. Das ist ein spürbarer Unterschied im Alltag: Der Nutzer sieht seine
Netzlaufwerke, seine Schnellzugriffe, seine gewohnte Oberfläche.
- **Tray-Symbol mit Zustand** — auf einen Blick: läuft ein Rip, wie weit,
wie lange noch. Rechtsklick: Öffnen, Pause, Beenden.
- **Windows-Benachrichtigungen** (echte Toasts, nicht Browser-Popups), mit
Klick direkt in den Job.
- **Fortschritt in der Taskleiste** — Windows kann einen Fortschrittsbalken
über dem Symbol zeigen. Bei einem 90-Minuten-Rip ist das genau die
Information, die man will, ohne das Fenster zu öffnen.
- **Die Bibliothek** als eigene Ansicht (§ 6.6).
**Was die Oberfläche NIE tut:** Eine Liste auf leer setzen, weil eine Nachricht
ausblieb (R2). Bei einem Aussetzer bleibt der letzte Stand stehen, und ein
Banner sagt, dass gerade nichts Neues kommt.
---
## 8. Setup, Autostart, Deinstallation
**Entscheid: pro Benutzer, ohne Administratorrechte, Windows 10 (ab 1809) und 11, x64.**
| | |
|---|---|
| Installer | `electron-builder` → NSIS, ein `RippySetup.exe` |
| Ziel | `%LOCALAPPDATA%\Programs\Rippy` — kein UAC-Dialog |
| Autostart | `HKCU\Software\Microsoft\Windows\CurrentVersion\Run` |
| Update | `electron-updater`, NSIS-Ziel (§ 6.8) |
| Deinstallation | Über die Windows-Programmliste, räumt **wirklich** auf |
Zu Windows 10, tagesaktuell eingeordnet: Die Verbraucher-Sicherheitsupdates
(ESU) enden im Oktober 2026, die Chromium-Familie läuft auf Windows 10 aber
erklärtermaßen weiter (Edge/WebView2 bis mindestens Oktober 2028). Electron 44
läuft dort. Für Rippy heißt das: bauen ja, und ein Satz in der Anleitung, dass
Windows 10 selbst ausläuft — mehr nicht.
**Warum kein Windows-Dienst:** Ein Dienst startet vor der Anmeldung — klingt
besser, kostet aber bei jeder Installation und jedem Update eine
Administrator-Abfrage, und er kann kein Tray-Symbol zeigen (keine
Benutzersitzung). Für einen Rechner, an dem jemand sitzt und Discs einlegt, ist
der Autostart-Eintrag der richtige Weg.
**Drei Dinge, die aus den alten Fehlern übernommen werden:**
1. **Drüber-Installieren funktioniert.** Rippy läuft fast immer (Autostart) —
der Installer beendet die laufende Fassung, bevor er kopiert. Sonst landet
die neue Version als `Rippy.exe.neu` daneben. *(Genau das ist passiert.)*
2. **Die Deinstallation räumt die WebView-Reste weg.** Beim letzten Mal blieben
**148 Dateien** liegen, weil die Renderer-Hilfsprozesse den Zwischenspeicher
offen hielten — und der Deinstallierer meldete trotzdem „Rippy wurde
entfernt."
3. **Das Aufräum-Skript darf keine Laufwerkswurzel löschen.** Wenn der
Installationsort aus der Registry eine Wurzel wäre, löschte die
Deinstallation das Laufwerk. Nicht beobachtet — aber nicht wiedergutzumachen.
Zum SmartScreen-Hinweis beim ersten Start und zur Update-Folge der fehlenden
Signatur: beides steht bei Entscheid 5 — die Anleitung erklärt den Weg, das
Release nennt Größe und Prüfsumme, und Updates laufen nur über HTTPS (§ 6.8).
---
## 9. Die Werkzeuge
Der Entscheid vom 28.08.2026 gilt unverändert: *„handbrake und makemkv MÜSSEN
mitgeliefert werden oder während des Setups separat installiert werden."* Die
Wege unterscheiden sich, und der Grund ist die **Lizenz**, nicht die
Bequemlichkeit.
| | Weg | Warum |
|---|---|---|
| **HandBrakeCLI** | **mitgeliefert** (~35 MB) | GPL-2 erlaubt die Weitergabe, solange Lizenztext und Quellverweis dabei sind. Damit komprimiert Rippy auch ohne Internet. |
| **FLAC** | **mitgeliefert** (~2 MB) | Xiph-Lizenz (BSD-artig), Weitergabe erlaubt. ⚠️ `flac.exe` braucht `libFLAC.dll` daneben — mit nur der EXE endet jeder Aufruf ohne eine Zeile Ausgabe. |
| **MakeMKV** | **beim Einrichten geholt** | Proprietär — die Lizenz erlaubt Dritten keine Weitergabe. Rippy lädt die offizielle Datei vom Hersteller und startet dessen Installer. |
Die Lizenztexte und Quellverweise für HandBrake und FLAC liegen dem Setup bei
— wegen Entscheid 9 wird die EXE weitergegeben, und damit ist das Pflicht,
nicht Kür.
**Die Bezugskette für MakeMKV** (aus dem 28.08.-Entscheid, unverändert):
eigene Quelle → Hersteller-Seite → Hersteller-Forum → Internet Archive. Mit
zwei Feinheiten, beide gemessen: **Die höchste Versionsnummer gewinnt, nicht
die erste** (das Archiv meldet sonst 1.18.2 statt 1.18.4), und **jeder Versuch
bekommt eine knappe Zeitgrenze** (8 s), weil das Forum in zwei von drei
Abrufen ausfiel.
MakeMKV signiert seinen Installer nicht (`NotSigned`, an der echten Datei
gemessen) — geprüft wird deshalb die Versions-Ressource
(`CompanyName: GuinpinSoft inc`). Das ersetzt keine Signatur, fängt aber ab,
was hier wirklich droht: eine Fehlerseite mit `.exe`-Namen, ein abgebrochener
Download, eine falsche Fassung.
### 9.1 Der Beta-Key — ein Termin, kein Detail
> **MakeMKVs freier Beta-Key ist bis Ende September 2026 gültig. Die Kaufseite
> für die Dauerlizenz ist defekt — der Hersteller verweist selbst auf den
> Beta-Key.** *(Am 30.08.2026 gegen Forum und Fachpresse geprüft.)*
Zur Einordnung, die in der ersten Fassung fehlte: Das ist **kein frischer
Unfall, sondern ein Dauerzustand seit Sommer 2025** — das Kaufsystem fällt
seither immer wieder aus, und der Beta-Key wurde bisher stets kurz vor Ablauf
verlängert (zuletzt Anfang August 2026, gültig bis Ende September). Rippy
plant also nicht für eine einmalige Panne, sondern für einen Rhythmus. Das
Restrisiko — der Hersteller verschwindet ganz — kann kein Programm der Welt
auffangen; was ein Programm auffangen kann, ist das Überraschtwerden:
1. **Der Key wird automatisch geholt** (wie heute) — aus dem Forum-Thread, in
dem der Hersteller ihn veröffentlicht.
2. **Rippy zeigt das Ablaufdatum im Dashboard** und warnt **sieben Tage
vorher** — statt dass ein Rip mitten in der Nacht scheitert und niemand
weiß, warum. Ohne gültigen Key geht keine Blu-ray und keine 4K-Disc mehr
auf; DVDs laufen weiter.
3. **Ein abgelaufener Key ist eine klare Meldung**, kein rätselhafter
MakeMKV-Fehlercode.
4. **Ein eingetragener Dauerlizenz-Schlüssel schlägt alles.** Wer eine Lizenz
hat, trägt sie ein und ist raus aus dem Thema.
---
## 10. Was aus dem alten Rippy mitkommt
**Kein Code. Aber jede teuer bezahlte Erkenntnis.**
Das ist der Kern des Grüne-Wiese-Ansatzes: Der Python-Code wird nicht portiert,
aber er ist die beste verfügbare Dokumentation darüber, wie sich MakeMKV,
HandBrake und Windows-Laufwerke wirklich verhalten. Diese Tabelle ist die
Arbeitsliste für den Neubau.
| Herkunft | Was übernommen wird | Wohin |
|----------|--------------------|-------|
| `ripping.py` | TINFO/SINFO/PRGV/MSG-Parser, Kommandobau, `laengster_titel`, `episoden_titel`, Sprach-Zusammenfassung | `rip/parser.ts`, `rip/makemkv.ts` |
| `win_ioctl.py` | Die hergeleiteten Steuercodes samt Herleitung — **in § 3.2 aus Node bestätigt** | `laufwerk/win32.ts` |
| `windows.py`, `winlauf.py` | Zugriffsgründe je Fehlernummer, `CREATE_NO_WINDOW`, Standby verhindern, die Prozess-Leine | `laufwerk/`, `werkzeuge/leine.ts` |
| `caps.py` | Encoder **messen** statt behaupten (`HandBrakeCLI --help` auswerten) | `komprimieren/encoder.ts` |
| `handbrake_aufruf.py` | `--aencoder` (nicht `--audio-codec`), Container aus dem Preset erzwingen, letzte 12 Zeilen puffern, `unknown option`-Erkennung | `komprimieren/handbrake.ts` |
| `medien.py` | Zielstruktur, NFO-Schema, Episoden-Zuordnung per Laufzeit, Medienserver-Refresh | `ablage/` |
| `prescan.py`, `clients/*` | TMDb/TVDb/OMDb/MusicBrainz/Jikan-Abfragen, Zwischenspeicher, Confidence | `metadaten/` |
| `pfade.py` | Laufwerkswurzeln, UNC, `naechster_vorhandener` **ohne Endlosschleife** | `speicher/pfade.ts` |
| `musicbrainz_cd.py`, `audio_cd.py` | DiscID-Rechnung (mit/ohne 150 Frames), 2048-vs-2352-Falle | `rip/audiocd.ts` |
| `makemkv_daten.py` | Die 4K-Schlüsselkette: Beta-Key-Abruf aus dem Forum-Thread, Schlüsselspeicher (`_private_data.tar`), KEYDB als Notnagel — samt der dort dokumentierten Fehldiagnosen | `werkzeuge/schluessel.ts` |
| `presets.py`, `eta.py`, `phasen.py` | Preset-Zuordnung je Disc-Typ, Restzeit, Phasenmodell | `komprimieren/`, `ablauf/` |
| `beschaffen.py`, `katalog.py` | Suchreihenfolge für Werkzeuge, Registry-Auswertung, Ausweichquellen | `werkzeuge/` |
| `AGENTS.md` | Die sechs Regeln aus § 4.3 | Wächter-Tests |
**Die Tests wandern sinngemäß mit.** Von den 977 heutigen Tests ist der größte
Teil reine Textverarbeitung (Parser, Pfade, Kommandobau) — die Testfälle
gelten unverändert, nur die Sprache wechselt. Das ist kein Nebeneffekt,
sondern die Absicherung: Ein neu geschriebener Parser, der dieselben
Testfälle besteht wie der alte, hat dieselben Fallen abgedeckt.
---
## 11. Was aus `main` entfernt wird
**Entscheid 7 — und zwar sofort.** Die Arbeit gehört in den `main`-Branch,
nicht hierher — dieser Abschnitt ist die Arbeitsliste dafür, damit sie nicht
aus dem Gedächtnis gemacht wird.
### 11.1 Der Windows-Anteil in `main` sind DREI Dinge, nicht eins
Diese Unterscheidung ist der ganze Abschnitt. Wer sie überspringt, löscht
entweder zu wenig oder reißt die Docker-Version mit.
**Zuerst in Klartext, ohne Dateinamen:**
> **A ist Rippy. B ist ein Handlanger für die VM. C sind Notizzettel, die
> wegen A eingeklebt wurden.**
**A — „Rippy" auf einem Windows-PC.** Das Programm, das man heute kennt:
`RippySetup.exe`, Doppelklick, Symbol in der Taskleiste, ein Fenster geht auf.
Disc rein, Rippy erkennt sie, rippt, komprimiert, legt ab. Ein vollständiges
Rippy, das alles allein macht. **Genau das wird durch v5 ersetzt.**
**B — „Rippy Worker" auf einem Hilfsrechner.** Ein anderes Programm,
`RippyWorkerSetup.exe`. Es **rippt nichts** und hat keine Bedienoberfläche.
Beim Installieren fragt es drei Dinge: *IP der Rippy-VM? Name dieses Helfers?
Wie viele Kerne?* Danach wartet es. Will die **Docker-Rippy auf der VM** einen
Film komprimieren und ist selbst zu langsam, schickt sie die Arbeit dorthin.
```
VM (Docker-Rippy) Ein Windows-PC
├─ Disc einlesen
├─ rippen
└─ komprimieren? zu langsam ──────► B: „Rippy Worker"
komprimiert und
Ergebnis ◄───────────────────── schickt zurück
```
B läuft nur *zufällig* auf Windows — es gehört zur Docker-Welt. **Entscheid 8:
B bleibt bei Docker** — es wird weder abgeräumt (es ist Docker, und Docker
bleibt unangetastet) noch nach v5 übernommen (v5 ist Einzelplatz, Entscheid 3
— es gibt dort keinen zweiten Knoten, dem man etwas schicken könnte).
**C — Notizzettel im Docker-Code.** Keine eigenen Dateien, sondern verstreute
Stellen *mitten in* Dateien, die Docker jeden Tag benutzt, wo jemand schrieb:
„falls wir gerade auf Windows laufen, mach es anders". Eingebaut wurden sie,
damit **A** funktioniert.
**Der Unterschied zwischen A und C ist nicht der Inhalt, sondern der Ort:**
| | Bild | Aufräumen heißt |
|---|------|-----------------|
| **A** | Ganze Ordner, auf denen „Windows" steht. Docker schlägt sie nie auf. | **Ordner wegwerfen.** Ganze Dateien löschen, fertig. |
| **C** | Einzelne Seiten mit Windows-Notizen, die in Ordnern liegen, die Docker täglich benutzt. | **Seiten einzeln durchgehen.** Die Datei bleibt, ein paar Zeilen gehen — und bei jeder Zeile ist zu prüfen, ob wirklich nur A sie brauchte. |
Deshalb ist C der heikle Teil: Erwischt man eine Zeile, die Docker doch
braucht, geht auf der VM etwas kaputt. Und manche Windows-Zeile braucht sogar
**B** weiter — etwa die Regel, dass eine Laufwerkswurzel `F:\` mit Trenner
geschrieben werden muss (§ 1). Der Handlanger läuft ja auf Windows.
**In Dateien ausgedrückt:**
| | Was es ist | Schicksal |
|---|---|---|
| **A** | **Die Standalone-App** — PyInstaller-Setup, Tray, WebView2-Fenster, Einrichtungs-Assistent, lokale Queue, Werkzeug-Beschaffung, Win32-Treiber | **weg** — v5 ersetzt sie |
| **B** | **Der Remote-Encode-Worker**`deploy/worker-windows/`, `/worker-setup/*`, Pfad-Map | **bleibt** |
| **C** | **Windows-Zweige im Docker-Code** — die `os.name == "nt"`-Stellen in `main.py`, `betrieb.py`, `ablauf.py`, `rohdaten.py`, dazu die Fähigkeiten-Auskunft im UI | **einzeln prüfen** |
### 11.2 Teil A — die Standalone-App (weg)
```
src/rippy/windows_app.py Installer, Dienst, Deinstallation
src/rippy/fenster.py WebView2-Fenster
src/rippy/setup_fenster.py Einrichtungs-Assistent
src/rippy/einrichtung.py Voraussetzungs-Prüfungen
src/rippy/standalone.py lokale Zustellung statt Celery
src/rippy/drives/windows.py Win32-Laufwerkstreiber
src/rippy/drives/win_ioctl.py die Steuercodes
src/rippy/platform/winlauf.py CREATE_NO_WINDOW, Waisen, Job Object
src/rippy/platform/win_registry.py
src/rippy/platform/verknuepfungen.py
src/rippy/platform/dateiangaben.py
src/rippy/tools/ Werkzeug-Katalog + Beschaffung (Windows-only)
src/rippy/rip/audio_cd.py Audio-CD über Win32
src/rippy/rip/musicbrainz_cd.py DiscID-Rechnung
packaging/windows/ der PyInstaller-Bau
dist/windows/ die gebaute EXE + vendor/
+ die zugehörigen test_*.py
```
Dazu: `src/rippy/queue/lokal.py` und `queue/laeufer.py` (die lokale Queue hat
außerhalb der Standalone-App keinen Aufrufer) und `bus/waechter.py`, soweit er
nur die Windows-Disc-Wache bedient.
**Die Steuercodes und Parser sterben nicht — sie sind bereits nach v5
übernommen** (§ 3.2 hat sie aus Node bestätigt, § 10 listet den Rest). Was
gelöscht wird, ist die Python-Fassung, nicht das Wissen. Wichtig fürs
Löschen von `makemkv_daten.py`-Verwandten: Die Schlüsselketten-Erkenntnisse
(§ 10) VOR dem Löschen in v5-Notizen sichern, falls W-7 noch nicht begonnen
hat — das Wissen darf nicht nur im Git-Verlauf liegen.
### 11.3 Teil C — die verstreuten Zweige (einzeln prüfen)
Hier ist blindes Löschen gefährlich, weil manches auch **ohne** die
Standalone-App gebraucht wird — vom Remote-Worker (Teil B) oder von der
Pfad-Behandlung allgemein.
| Fundort | Prüfen |
|---------|--------|
| `docker/api/main.py``betrieb.windows_laufwerke()`, die Browse-Sonderfälle, `/betrieb`-Fähigkeiten | Die Fähigkeiten-Auskunft bleibt sinnvoll (Docker-AiO vs. verteilt), nur die Windows-Werte fallen weg |
| `src/rippy/betrieb.py` | Ganz raus oder auf Docker-Fälle eindampfen |
| `docker/worker/ablauf.py`, `rohdaten.py` | Die Windows-Pfadzweige raus — aber `nativ_nachsehen()` prüfen: gilt es auch für den Remote-Worker? |
| `docker/api/rohdaten.py`, `mounts.py` | dito |
| `src/rippy/pfade.py` | **Bleibt.** Laufwerkswurzeln und UNC braucht auch der Remote-Worker |
| UI: `useBetrieb`, `WorkerVerwaltung`, `Anleitung`, `Settings` | Nur die Standalone-Texte raus; die Worker-Verwaltung bedient Teil B |
| `src/rippy/test_keine_container_reste.py` | Der Wächter-Test wird gegenstandslos — mit weg |
### 11.4 Wie das gemacht wird
1. **Jetzt, nicht erst wenn v5 steht.** Commander-Entscheid vom 30.08.2026
(zweite Fassung): Das Aufräumen wartet **nicht** auf Etappe W-6. Die
Konsequenz steht bei Entscheid 7 — zwischen Aufräumen und W-6 liefert das
Repo kein installierbares Windows-Rippy; die installierte rc11-Fassung
läuft lokal weiter, bekommt aber keine Reparaturen mehr.
2. **Ein eigener Branch**, nicht direkt auf `main`. Der Umfang ist zu groß für
einen Freihand-Commit.
3. **In der Reihenfolge A → C → (B bleibt).** Erst die eindeutigen Dateien,
dann die verstreuten Zweige — sonst sucht man Aufrufer, die es noch gibt.
4. **Die Ampel nach jedem Schritt.** `AGENTS.md` Regel A gilt unverändert; ein
Aufräumen, das die Tests rot macht, ist nicht fertig, sondern angefangen.
5. **Ein SAVEPOINT-Eintrag** dazu, mit der Zahl der entfernten Zeilen und dem
Hinweis, wo das Wissen jetzt liegt.
---
## 12. Etappen
**Grundregel:** Jede Etappe endet mit grüner Ampel und einem Programm, das man
starten kann. Kein großer Umbau, nach dem monatelang nichts geht.
**Parallel dazu, unabhängig von den Etappen:** das Aufräumen von `main`
(§ 11.4) — es startet sofort und braucht auf nichts hiervon zu warten.
| # | Titel | Inhalt | Fertig, wenn |
|---|-------|--------|--------------|
| **W-0** | **Gerüst** | Electron 44 + TypeScript + Vite + Vitest, drei Prozesse, IPC, `node:sqlite` anbinden (§ 3.4 — bereits bewiesen), Prozess-Leine, CI-Ampel, npm-Skript-Sperre in der Bau-Anleitung (§ 3.5) | Ein Fenster geht auf, der Kern läuft, ein Testlauf ist grün |
| **W-1** | **Laufwerk** | `laufwerk/win32.ts` aus dem § 3.2-Beweis, Wache im 3-Sekunden-Takt (§ 5), Disc-Typ, Verriegeln, Auswerfen mit Nachsehen | Disc rein → Rippy sagt korrekt, was es ist. Auswerfen wirkt (nachgemessen, nicht geglaubt) |
| **W-2** | **Rippen** | makemkvcon-Aufruf, Parser, Fortschritt, Abbruch, Lesefehler-Warnung, Werkzeug-Beschaffung | Eine DVD und eine Blu-ray laufen durch, verlustfrei |
| **W-3** | **Komprimieren + Ablegen** | HandBrake, Encoder gemessen, Presets, Zielstruktur, NFO, Poster, Medienserver | Ein Film liegt fertig in Jellyfin und ist dort sichtbar |
| **W-4** | **Metadaten-Automatik** | Die vier Quellen aus § 5, Sicherheitsgrad, Nachkorrektur | Fünf verschiedene Discs richtig erkannt, ohne Nachfrage |
| **W-5** | **Oberfläche** | Dashboard, Einstellungen, Logs, Bibliothek, Tray, Windows-Toasts, Taskleisten-Fortschritt | Der Commander bedient Rippy einen Abend lang ohne Rückfrage |
| **W-6** | **Setup + Update** | NSIS, Autostart, Erst-Einrichtung, Selbst-Update gegen das öffentliche Gitea (inkl. Messung der Fest-Adresse für `latest.yml`, Entscheid 6), Lizenztexte beilegen (Entscheid 9), Deinstallation | Frischer Rechner: Setup → Disc rein → MKV raus. Und: drüber-installieren geht |
| **W-7** | **Der Rest** | Audio-CD, ISO-Backup, mehrere Laufwerke, 4K-Schlüsselkette (`werkzeuge/schluessel.ts`), WM_DEVICECHANGE als Wache-Verfeinerung (§ 5) | Je Feature ein Nachweis |
Reihenfolge-Begründung: W-1 bis W-3 sind die Kette, ohne die Rippy nichts tut.
W-4 und W-5 machen sie bequem. W-6 macht sie weitergebbar. W-7 sind die
Ergänzungen, die ohne die Kette keinen Sinn hätten.
---
## 13. Risiken und offene Punkte
| Risiko | Bewertung | Behandlung |
|--------|-----------|------------|
| **MakeMKV-Beta-Key läuft Ende September 2026 ab** | hoch × sicher | § 9.1 — Ablaufdatum sichtbar, Warnung 7 Tage vorher, klare Meldung, Dauerlizenz eintragbar. Einordnung: Dauerzustand seit Sommer 2025, der Key wurde bisher stets verlängert. **Bleibt der akuteste Termin im Dokument.** |
| **Kein installierbares Windows-Rippy zwischen Aufräumen und W-6** | sicher × akzeptiert | Folge von Entscheid 7 („sofort"). Die installierte rc11-EXE läuft lokal weiter; die Docker-Version deckt die VM ab. Bewusst bezahlter Preis |
| Neubau dauert länger als eine Reparatur | sicher | Deshalb Entscheid 2: Die Docker-Version läuft weiter. Es entsteht keine Lücke auf der VM |
| Electron-Paket ~150200 MB | sicher, gering | Ein einmaliger Download. Der Gegenwert ist eine Werkzeugkette statt zweier |
| **Electron altert schnell** (alle ~8 Wochen neue Hauptversion, nur 3 gepflegt) | sicher × planbar | Pflege-Regel in § 6.8: bei jedem Rippy-Release den Electron-Sprung prüfen. Das Selbst-Update macht das Verteilen billig |
| SmartScreen bei unsignierter EXE | mittel | Entscheid 5: kein Zertifikat. Anleitung + Prüfsumme beim Release — reicht für die Zielgruppe aus Entscheid 9 |
| **Update ohne Signaturprüfung** | mittel × Folge von Entscheid 5 | § 6.8 — nur HTTPS, `http://` wird abgelehnt. Der Transportweg ist die einzige Absicherung, deshalb darf er nicht optional sein |
| **Feste `latest.yml`-Adresse im Gitea** | niedrig × messbar | Entscheid 6, ⚠️-Punkt — in W-6 am ersten Release messen; Rückfallebene festes Update-Tag. Eine Bau-Einstellung, keine Programmänderung |
| Updates hängen am Heimnetz des Commanders | niedrig | § 6.8 Regel 3 — ein stummer Server ist eine Protokollzeile, kein Fehler. Wer eine eigene Quelle will, trägt sie ein |
| **Aufräumen von `main` reißt Docker mit** | mittel × vermeidbar | § 11 — drei Teile sauber getrennt, Remote-Worker bleibt, eigener Branch, Ampel nach jedem Schritt |
| koffi + Electron + npm-Install-Sperre | niedrig | § 3.5 gemessen: koffi lädt mit vorgebauten Binärdateien, Electron braucht `install.js` von Hand. Beides steht in der Bau-Anleitung |
| Parallel-Rip vs. MakeMKV-Datenverzeichnis | mittel | § 6.5 — **messen, bevor die Grenze festgeschrieben wird** |
| Ein neu geschriebener Parser bringt neue Fehler | mittel | Die alten Testfälle wandern mit (§ 10). Ein Parser, der sie besteht, kennt dieselben Fallen |
| 4K-UHD-Schlüssel | gelöst | Unter Windows holt makemkvcon sie selbst — der strukturelle Vorteil dieser Plattform, gemessen 25.07.2026 |
| ~~`node:sqlite` in Electron ungeprüft~~ | **gelöst** | § 3.4 — am 30.08.2026 im `utilityProcess` von Electron 44 gemessen. Rückfallebene `better-sqlite3` nicht gebraucht |
| ~~Öffentliche Update-Quelle für Externe~~ | **gelöst** | § 3.6 — Gitea ist öffentlich per HTTPS erreichbar, Entscheid 6 nutzt es direkt |
### Stand der Entscheidungen (30.08.2026, zweite Fassung)
Alle offenen Punkte der ersten Fassung sind entschieden **und** die Prüfung
hat zwei davon zusätzlich vereinfacht: Code-Signing **nein** (Entscheid 5) ·
Update-Quelle **öffentliches Gitea, direkt** (Entscheid 6, durch Messung § 3.6
von einer Infrastruktur-Frage zu einer Bau-Einstellung geschrumpft) · alter
Windows-Weg **wird sofort entfernt** (Entscheid 7) · Remote-Worker **bleibt
bei Docker** (Entscheid 8) · Zielgruppe **Bekannte mit Link** (Entscheid 9).
Was daraus an Arbeit folgt, steht in § 11 und § 12.
---
## 14. Was NICHT gebaut wird
- **Kein Python.** Auch nicht „nur für den einen Parser".
- **Kein HTTP-Server, kein Port, kein localhost.** Fenster und Kern reden über
IPC.
- **Keine Docker-Kompatibilität.** Kein geteiltes Modul, kein gemeinsames
Schema, keine Container-Pfade.
- **Kein Linux, kein macOS.** „100 % für Windows gebaut" heißt: Wo eine
Windows-Lösung besser ist als eine portable, gewinnt die Windows-Lösung.
- **Kein ARM-Fork.** Rippy bleibt ein Eigenbau.
- **Keine Auth.** Einzelplatz, kein Netzdienst — es gibt nichts zu schützen.
- **Kein öffentlicher Auftritt.** Entscheid 9: Weitergabe per Link an
Bekannte, kein Marketing, kein Support-Versprechen an Fremde.
---
## Anhang — Verhältnis zu den bestehenden Konzepten
`KONZEPT.md` (v1) und `KONZEPT-V2.md` (v2) **bleiben gültig für die
Docker-Version**. Dieses Dokument ersetzt sie nicht, es steht daneben.
Drei Punkte aus v2 werden für Windows bewusst anders entschieden — jeweils mit
Begründung:
| v2 sagt | v5 macht | Warum |
|---------|----------|-------|
| Entscheid 4: *kein Electron, pywebview + WebView2* (8 MB statt 150 MB) | **Electron** | Die Rechnung war richtig und die Schlussfolgerung trotzdem falsch: Sie hat nur die Fenster-Frage betrachtet. Der Preis für pywebview war nicht 0 MB, sondern **eine zweite Werkzeugkette** — PyInstaller, hidden imports, `pythonnet`, und ein Kern, der aus Container-Code bestand. Electron kostet 150 MB und schenkt uns *eine* Sprache von der Oberfläche bis zum Laufwerk. |
| Eine Anwendung, drei Verdrahtungen (Ports/Treiber) | **Ein Windows-Programm** | Die Ports-Architektur ist richtig für ein Produkt, das drei Betriebsarten tragen muss. Bei Entscheid 2 (Docker bleibt getrennt) trägt sie nichts mehr — sie wäre Abstraktion auf Vorrat. |
| REST + SSE als Schnittstelle | **IPC** | Ein lokaler Webserver in einer Einzelplatz-Anwendung ist eine offene Tür ohne Gegenwert. |
---
*Rippy v5 · Konzept, zweite Fassung · 30.08.2026 · Branch `worktree-windows-electron`*