0935766f61
Ampel / ampel (push) Successful in 28s
4K-UHD scheiterte an "The volume key is unknown for this disc". Am 25.07.2026
im Worker-Container nachgemessen: Laufwerk und MakeMKV sind in Ordnung
(LibreDrive v06.3 / Meldung 1011, Disc wird gelesen, AACS-Dump geschrieben) -
MakeMKV versucht gar nicht erst, online einen Schluessel zu holen. Belege:
_private_data.tar enthaelt nur die Index-Datei und KEINE hkd_*.bin; auch mit
geloeschter update.conf (Meldung 5074 belegt den Web-Kontakt) und mit
app_UpdateEnable="1" kam keiner; hkdata.fairuse.org und hkdata.crabdance.com
loesen weltweit nicht mehr auf (NXDOMAIN gegen Fritz!Box, 8.8.8.8, 1.1.1.1).
Betroffen war Akira UHD (MKB v76, Pressung Dez. 2020) - also gerade KEINE
Neuerscheinung. Der bisherige Fehlertext ("Disc neuer als die
Schluessel-Datenbank, mit einem der naechsten Updates rippbar") war falsch.
Einziger heute funktionierender Weg ist eine vom Nutzer selbst mitgebrachte
KEYDB.cfg. Rippy liefert KEINE Schluessel mit, laedt keine herunter und
verteilt keine - es stellt nur den Platz bereit und zeigt an, was dort liegt.
- Datenverzeichnis persistent gemountet (MAKEMKV_DATA_HOST, Default
/srv/rippy/makemkv): KEYDB.cfg und AACS-Dumps ueberleben jeden Rebuild.
Vorher loeschte jeder "up -d --build" beides - inklusive des Dumps, auf den
die Fehlermeldung selbst verwies.
- entrypoint.sh und tasks.py schreiben settings.conf ergaenzend statt
zerstoerend. Der entrypoint bricht bei nicht beschreibbarem Verzeichnis
nicht mehr ab - mit "restart: unless-stopped" waere das ein Crashloop
gewesen, in dem auch reines DVD-Rippen tot ist.
- Neues Zwillings-Modul makemkv_daten.py (docker/api + docker/worker,
byteweise identisch; test_zwillinge_sind_byteweise_identisch wacht darueber
und wurde durch absichtliches Verstellen als wirksam nachgewiesen).
- API: GET/POST/DELETE /system/keydb, GET /system/aacs-dumps(/{dateiname}).
JSON-Body statt Multipart - python-multipart ist bewusst nicht installiert
und wuerde die API beim Import toeten. nginx client_max_body_size 64m,
sonst scheitert der Upload mit 413, bevor die API ihn sieht.
- UI (Einstellungen -> System): Status, Hochladen per Datei-Dialog, Entfernen,
Dump-Download, KEYDB-Plakette je Worker (nur wo das Verzeichnis wirklich
gemountet ist - ein Remote-Transcode-Worker truege sonst eine Warnung,
die ihn nichts angeht).
- parse_msg() + log_cb: MakeMKV-Meldungen landen im Rippy-Log (gedrosselt:
Code 1003 raus, keine Wiederholungen, max. 40 je Rip). Nebenbei behoben:
der alte Parser (split(",", 4)[3]) schnitt jede Meldung am ersten Komma ab.
- Fuenf Stellen richtiggestellt, die behaupteten, MakeMKV-Updates braechten
die neueste Disc-Schluessel-Datenbank mit (UI, Anleitung, README,
Worker-Dockerfile, makemkv_key.py).
NICHT bewiesen: ein erfolgreicher UHD-Rip - es lag keine KEYDB.cfg mit dem
Akira-Schluessel vor. Belegt sind der Befund und die neue Mechanik. So steht
es auch im SAVEPOINT und in der ROADMAP.
Quellen (AGENTS Regel D):
- Datenverzeichnis + Dateiname GROSS/case-sensitiv:
https://forum.makemkv.com/forum/viewtopic.php?t=30636
- hkd_*.bin in _private_data.tar:
https://forum.makemkv.com/forum/viewtopic.php?t=32675
- headless settings.conf / app_UpdateEnable:
https://forum.makemkv.com/forum/viewtopic.php?t=20364
- KEYDB.cfg-Zeilenformat (libaacs):
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
- MSG-/PRGV-Format: https://www.makemkv.com/developers/usage.txt
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
268 lines
14 KiB
Markdown
268 lines
14 KiB
Markdown
# Rippy — die All-in-one Disc-Ripping-Maschine
|
|
|
|
Disc rein → automatisch erkannt (Titel, Poster, Metadaten) → verlustfrei
|
|
gerippt (MakeMKV) → auf Arbeitsgröße komprimiert (HandBrake) → fertig
|
|
abgelegt, wo DU willst (lokal, NAS, jede Freigabe). Modernes Web-UI,
|
|
Echtzeit-Fortschritt, komplett in Docker, komplett lokal.
|
|
|
|
## Schnellstart
|
|
|
|
Voraussetzungen: Docker + Docker Compose, ein optisches Laufwerk am Host.
|
|
|
|
```bash
|
|
git clone <repo-url> rippy && cd rippy
|
|
cp .env.example .env # optional: API-Keys — geht auch später im UI
|
|
mkdir -p /srv/rippy/media # Ablage-Basis (anpassbar in docker-compose.yml)
|
|
docker compose up -d --build
|
|
```
|
|
|
|
Dann `http://<host>` öffnen — der **Einrichtungs-Assistent** startet beim
|
|
ersten Mal automatisch (API-Keys, Verarbeitung, erkannte Hardware).
|
|
|
|
### Laufwerk anpassen
|
|
|
|
Der Worker braucht **zwei** Host-Geräteknoten: den CD-ROM-Knoten (meist
|
|
`/dev/sr0`) und den passenden **SCSI-Generic-Knoten** (`/dev/sgN`) — MakeMKV
|
|
spricht Laufwerke über die sg-Schicht an. Die sg-Nummer ist **je Host anders**
|
|
(auf frischen Systemen oft `/dev/sg0`).
|
|
|
|
Ermitteln, welche Knoten dein Blu-ray/DVD-Laufwerk hat:
|
|
```bash
|
|
lsscsi -g # zeigt Modell + zugehörigen /dev/sgN
|
|
# oder: ls -la /sys/class/scsi_generic/
|
|
```
|
|
Dann in der `.env` eintragen (Defaults passen für den Ursprungs-Host):
|
|
```bash
|
|
OPTICAL_SR=/dev/sr0 # CD-ROM-Knoten deines Laufwerks
|
|
OPTICAL_SG=/dev/sg0 # sg-Knoten deines Laufwerks
|
|
```
|
|
`docker compose up -d` nutzt diese Werte. (Für Feineinstellungen geht weiterhin
|
|
eine `docker-compose.override.yml` mit eigenen `devices:`-Einträgen.)
|
|
|
|
**Laufwerk in einer VM?** Per USB-Passthrough anhand der Vendor-ID
|
|
durchreichen (Proxmox: `qm set <vmid> -usb0 host=xxxx:yyyy,usb3=1`) —
|
|
NICHT als emuliertes CD-ROM (`media=cdrom`), das kann keine SCSI-Kommandos.
|
|
|
|
## Wie es funktioniert
|
|
|
|
1. **Disc-Wache** (ioctl-Polling, kein udev-Gefrickel) erkennt Einlegen,
|
|
identifiziert die Disc (Volume-Label → TMDB → OMDb-Fallback) und zeigt
|
|
sie mit Poster auf dem Dashboard. Klick auf einen Job-Titel öffnet die
|
|
Detail-Ansicht (Poster, Jahr, Beschreibung, Ablagepfad).
|
|
2. **Rip** (MakeMKV, verlustfrei — der einzige Weg durch AACS): Ziel wählst
|
|
du beim Start (Filme/Serien/Musik/eigener Pfad, inkl. Netzwerk-Ziele) —
|
|
oder du stellst die **Vollautomatik** an (Einstellungen → Ripping) und
|
|
der Rip startet ohne Nachfrage in den passenden Schnellwahl-Ordner.
|
|
Optional **nur Hauptfilm** (längster Titel, Extras bleiben weg). Bereits
|
|
gerippte Discs erkennt Rippy am Fingerabdruck und warnt. Vor dem Start
|
|
prüft Rippy den freien Platz gegen die Disc-Größe. Alle Tonspuren und
|
|
Untertitel bleiben erhalten (wichtig für Anime/O-Ton). Audio-CDs laufen
|
|
über abcde → FLAC + MusicBrainz.
|
|
**Serien**: Im Dialog „Serien" wählen → Serienname + Staffel angeben —
|
|
Ablage wird `<Serie>/Season NN`, und die Episoden werden nach dem Rip
|
|
per Laufzeitabgleich (TMDB) automatisch zu „Serie S01E02.mkv" benannt
|
|
(nur bei eindeutiger Zuordnung — sonst bleiben die Namen, mit Log).
|
|
3. **Kompression** (HandBrake, eigener Job auf eigener Queue): x265/x264,
|
|
Preset im UI wählbar; Rohdatei wird erst nach Erfolg gelöscht
|
|
(„Original behalten" als Option). Fehlgeschlagene Kompressionen lassen
|
|
sich ohne Neu-Rip neu anstoßen.
|
|
4. **Media-Server-Ablage**: Unter Einstellungen → Ripping (oder im
|
|
Einrichtungs-Assistenten) wählst du dein System — Jellyfin, Emby, Kodi,
|
|
Plex oder keins. Fertige Rips heißen dann „Titel (Jahr)" statt
|
|
Job-UUID; für Jellyfin/Emby/Kodi legt Rippy zusätzlich movie.nfo +
|
|
poster.jpg dazu (Kodi-NFO-Schema, lesen alle drei nativ). Plex nutzt
|
|
nur die Benennung. **Jellyfin/Emby**: Server-URL + API-Key eintragen,
|
|
dann stößt Rippy nach jedem fertigen Rip sofort einen Bibliotheks-Scan
|
|
an — Disc rein, Film erscheint im Server.
|
|
5. **4K-UHD**: braucht **zwei** Dinge — ein LibreDrive-fähiges Laufwerk
|
|
(MakeMKV-Forum: „Ultimate UHD Drives Flashing Guide") **und** einen
|
|
passenden Disc-Schlüssel. Normale BD/DVD gehen mit jedem Laufwerk und
|
|
ohne Schlüssel-Datei.
|
|
**Der Schlüssel ist heute die eigentliche Hürde.** Am 25.07.2026 auf
|
|
der Rippy-VM gemessen (Akira UHD, MKB v76, Pressung Dezember 2020):
|
|
Laufwerk und MakeMKV waren in Ordnung — „Using LibreDrive mode
|
|
(v06.3)", die Disc wurde gelesen — und trotzdem kam „The volume key is
|
|
unknown for this disc". MakeMKV versucht dabei **gar nicht mehr**,
|
|
online einen Schlüssel zu holen, und die früher genutzten
|
|
Schlüssel-Server (`hkdata.fairuse.org`, `hkdata.crabdance.com`) lösen
|
|
weltweit nicht mehr auf. Ein MakeMKV-Update ändert daran nichts.
|
|
Der einzige Weg, der heute funktioniert, ist eine Datei **`KEYDB.cfg`**
|
|
(GROSS geschrieben — Linux unterscheidet Groß- und Kleinschreibung) im
|
|
MakeMKV-Datenverzeichnis. Die legst du unter **Einstellungen → System**
|
|
ab (siehe unten).
|
|
⚠️ **Rippy liefert keine Schlüssel mit, lädt keine herunter und
|
|
verteilt keine.** Rippy stellt nur den Platz für eine Datei bereit, die
|
|
du selbst mitbringst, und zeigt dir ehrlich an, was dort liegt.
|
|
⚠️ UHD-Rohdaten sind bis 100 GB groß — wenn die Platte der
|
|
Rippy-Maschine dafür zu klein ist, lege das **Arbeitsverzeichnis**
|
|
(Einstellungen → Verarbeitung) auf eine eingehängte Freigabe, z. B.
|
|
`/app/media/nas-arbeit`. Rippy bricht sonst VOR dem Rip mit einer
|
|
Klartext-Meldung ab statt nach 40 GB mit voller Platte.
|
|
|
|
## Speicherziele (NAS, Freigaben)
|
|
|
|
Unter **Einstellungen → Speicherziele** hängst du NFS- oder SMB-Freigaben
|
|
direkt aus dem UI ein — sie erscheinen sofort in der Ziel-Auswahl beim
|
|
Rippen und werden beim Start automatisch wieder verbunden.
|
|
Technik: der api-Container läuft mit `CAP_SYS_ADMIN` und einem
|
|
rshared-Bind auf `/srv/rippy/media`, Mounts propagieren zu allen
|
|
Containern. ⚠️ Zugangsdaten liegen unverschlüsselt in der lokalen
|
|
Postgres-DB — bewusster Heimnetz-Kompromiss; lege fürs NAS einen eigenen,
|
|
eingeschränkten Benutzer an.
|
|
|
|
## Benachrichtigungen
|
|
|
|
**Einstellungen → Benachrichtigungen**: eine Webhook-URL eintragen, „Test
|
|
senden" drücken, speichern — fertig. Rippy meldet Job-Ende (fertig,
|
|
fehlgeschlagen, abgebrochen) und erkennt den Dienst an der URL selbst:
|
|
|
|
| Dienst | URL-Beispiel | Format |
|
|
|---|---|---|
|
|
| Discord | `https://discord.com/api/webhooks/…` | `{"content": …}` |
|
|
| Slack | `https://hooks.slack.com/services/…` | `{"text": …}` |
|
|
| ntfy (Handy-Push) | `https://ntfy.sh/mein-geheimes-thema` | Roh-Text + Titel |
|
|
| Eigenes (HA, n8n, …) | beliebige HTTPS-URL | `{"title","message","level"}` |
|
|
|
|
## System, MakeMKV-Beta-Key, Disc-Schlüssel & Updates
|
|
|
|
**Einstellungen → System** zeigt die Werkzeug-Versionen jedes Workers
|
|
(MakeMKV, HandBrake), den Key-Status und den freien Speicherplatz. Der
|
|
MakeMKV-Beta-Key (wechselt ~monatlich, Forum-Thread t=1053) wird hier im
|
|
UI eingetragen und gilt ab dem **nächsten Rip** — ohne Rebuild, ohne
|
|
Neustart; er schlägt den Key aus der `.env`.
|
|
|
|
**Disc-Schlüssel (`KEYDB.cfg`)** — im selben Tab, nur für 4K-UHD nötig:
|
|
|
|
- **Status**: Rippy zeigt, ob eine `KEYDB.cfg` im MakeMKV-Datenverzeichnis
|
|
liegt, mit Pfad, Größe, Anzahl der Disc-Einträge und Änderungsdatum.
|
|
- **Hochladen**: Knopf „KEYDB.cfg hochladen", dann deine eigene Datei im
|
|
Datei-Dialog auswählen — mehr ist nicht zu tun. Rippy prüft den Inhalt
|
|
auf Plausibilität und lehnt Unsinn (leere Datei, versehentlich geladene
|
|
HTML-Fehlerseite) mit einer deutschen Klartext-Meldung ab, statt ihn
|
|
stillschweigend zu speichern.
|
|
- **Entfernen**: ein Knopf, die Datei ist wieder weg.
|
|
- **AACS-Dumps herunterladen**: MakeMKV legt bei jeder unbekannten Disc
|
|
selbst einen Dump ab. Genau den braucht man, wenn man im MakeMKV-Forum
|
|
um den Schlüssel für eine neue Pressung bittet — hier holst du ihn dir
|
|
aus dem Container, ohne SSH.
|
|
|
|
Der Beta-Key ist die **Lizenz für die Software**; die `KEYDB.cfg` sind
|
|
**Disc-Schlüssel** — zwei völlig verschiedene Dinge. **Rippy liefert und
|
|
lädt keine Disc-Schlüssel**, es hält nur ein persistentes Verzeichnis
|
|
dafür bereit (`MAKEMKV_DATA_HOST`, siehe Tabelle unten) — rebuild-fest,
|
|
damit deine Datei einen `docker compose build` überlebt.
|
|
|
|
**Updates:** „Auf Updates prüfen" vergleicht mit makemkv.com und den
|
|
offiziellen HandBrake-Releases (12 h gecacht). MakeMKV-Update ohne
|
|
Code-Änderung: `MAKEMKV_VERSION=x.y.z` in die `.env`, dann
|
|
`docker compose build worker && docker compose up -d worker`.
|
|
⚠️ **Richtigstellung (25.07.2026):** Hier stand früher, neue MakeMKV-
|
|
Versionen brächten „auch die neueste Disc-Schlüssel-Datenbank mit". Das
|
|
ist widerlegt — auf der VM nachgemessen: MakeMKV holt für eine unbekannte
|
|
UHD-Disc keinen Schlüssel mehr, weder mitgeliefert noch aus dem Netz. Ein
|
|
Update lohnt für Programmfehler und neue Laufwerke, hilft aber NICHT
|
|
gegen „The volume key is unknown". Dafür brauchst du die `KEYDB.cfg`.
|
|
HandBrake kommt im Docker-Worker bewusst aus Debian (stabil, hinkt der
|
|
offiziellen Version hinterher); der native Windows-Worker nutzt die
|
|
aktuelle Version direkt.
|
|
|
|
## Verarbeitung & Hardware
|
|
|
|
**Einstellungen → Verarbeitung** zeigt ehrlich an, welche Encoder deine
|
|
Worker WIRKLICH haben (CPU x264/x265, VAAPI bei AMD/Intel-GPU, NVENC bei
|
|
NVIDIA) — jeder Worker meldet seine Fähigkeiten selbst beim Start.
|
|
|
|
### Optional: weitere Maschinen als Transcode-Worker
|
|
|
|
Die Kompression läuft als eigener Celery-Task auf der Queue `transcode` —
|
|
JEDE Maschine im Netz kann sie übernehmen. **Einstellungen → Worker** zeigt
|
|
für beide Varianten einen Copy-Paste-Befehl:
|
|
- **Linux (Docker)**: `deploy/remote-transcode-worker.yml`
|
|
- **Windows (nativ, ohne Docker)**: braucht nur Python 3.10+ — der
|
|
Installer (`install.ps1`) kommt von der Rippy-Instanz selbst
|
|
(`/api/worker-setup/windows`), lädt Worker-Code und HandBrakeCLI
|
|
automatisch und erzeugt eine `start-worker.bat` (optional `-Autostart`).
|
|
|
|
Ohne Zusatz-Worker macht der eingebaute CPU-Worker alles selbst — Rippy
|
|
bleibt All-in-one.
|
|
|
|
## Umgebungsvariablen (.env)
|
|
|
|
| Variable | Pflicht | Zweck |
|
|
|---|---|---|
|
|
| `TMDB_API_KEY` | empfohlen | Metadaten (deutsche Texte) — alternativ im UI/Wizard; beide Key-Arten gehen (v3-Schlüssel und v4-Token) |
|
|
| `WORKER_NAME` | optional | Anzeigename des eingebauten Workers (Standard: rippy-hauptworker) |
|
|
| `OMDB_API_KEY` | optional | zweite Metadaten-Quelle (Fallback) |
|
|
| `THETVDB_API_KEY` | optional | Serien-Fallback |
|
|
| `MAKEMKV_APP_KEY` | optional | MakeMKV-Beta-Key (Forum); DVDs gehen ohne — bequemer: im UI unter Einstellungen → System pflegen |
|
|
| `MAKEMKV_URL_BASE` | optional | alternative Download-Quelle für den Image-Build |
|
|
| `MAKEMKV_DATA_HOST` | optional | Host-Verzeichnis für MakeMKVs Daten (Default `/srv/rippy/makemkv`) — hier liegen deine `KEYDB.cfg` und die AACS-Dumps; persistent, überlebt jeden Rebuild |
|
|
| `OPTICAL_SR` | je Host | Host-Pfad des CD-ROM-Knotens (Default `/dev/sr0`) — siehe „Laufwerk anpassen" |
|
|
| `OPTICAL_SG` | je Host | Host-Pfad des sg-Knotens des Laufwerks (Default `/dev/sg1`; oft `/dev/sg0`) |
|
|
| `POSTGRES_PASSWORD` | optional | DB-Passwort (Default `rippy`) — für exponierte Umgebungen ein starkes Passwort setzen |
|
|
|
|
UI-Einstellungen (Wizard/Settings) überstimmen die Env-Variablen.
|
|
|
|
## Rippy woanders bereitstellen
|
|
|
|
Rippy ist reines Docker Compose — es läuft auf **jedem Linux-Host mit
|
|
Docker**, nicht nur auf der Original-VM. Es gibt (noch) keine fertigen
|
|
Registry-Images; gebaut wird beim ersten `up` direkt aus dem Repo.
|
|
|
|
Voraussetzungen auf dem Ziel-Host:
|
|
1. Linux (x86_64) mit Docker + Compose-Plugin.
|
|
2. Ein optisches Laufwerk, das der Host sieht (`ls /dev/sr* /dev/sg*`).
|
|
In einer VM: per **USB-Passthrough** (Vendor-ID) durchreichen, NICHT
|
|
als emuliertes CD-ROM — siehe „Laufwerk anpassen" oben.
|
|
3. Ablage-Basis anlegen: `mkdir -p /srv/rippy/media` (oder Pfad in
|
|
`docker-compose.yml` anpassen). Der Bind nutzt **shared/slave-Propagation**
|
|
(fürs In-UI-Einhängen von NAS-Freigaben). Auf den meisten systemd-Linux-Hosts
|
|
ist `/` schon `rshared`, dann ist nichts zu tun. Bricht `docker compose up`
|
|
mit *„path … is mounted on / but it is not a shared mount"* ab, einmalig:
|
|
```bash
|
|
sudo mount --bind /srv/rippy/media /srv/rippy/media
|
|
sudo mount --make-rshared /srv/rippy/media
|
|
```
|
|
(reboot-fest als systemd-`.mount`-Unit persistieren.) Braucht einen klassischen
|
|
Linux-Docker-Host mit `SYS_ADMIN` — nicht Docker-Desktop/rootless/Podman.
|
|
4. MakeMKV-Datenverzeichnis anlegen: `mkdir -p /srv/rippy/makemkv` (oder
|
|
eigenen Pfad über `MAKEMKV_DATA_HOST` in der `.env`). Dort liegen
|
|
MakeMKVs Programmzustand, die AACS-Dumps und — falls du 4K-UHD rippst —
|
|
deine eigene `KEYDB.cfg`. Das Verzeichnis gehört dem Host, damit ein
|
|
`docker compose build` deine Datei nicht wegwirft.
|
|
|
|
Dann wie im Schnellstart: klonen, `.env` füllen, `docker compose up -d
|
|
--build`, `http://<host>` öffnen — der Einrichtungs-Assistent führt durch
|
|
den Rest (API-Keys, Media-Server, Verarbeitung). Updates: `git pull &&
|
|
docker compose up -d --build`.
|
|
|
|
Nicht mitnehmen musst du: Gitea, Arcane, den CI-Runner — das ist die
|
|
Entwicklungs-Infrastruktur DIESER Installation, nicht Teil von Rippy.
|
|
|
|
### Härtung für fremde/exponierte Netze
|
|
|
|
Rippy ist bewusst **Heimnetz-only**: Es hat **keine Authentifizierung**, und
|
|
Redis + PostgreSQL veröffentlichen Ports auf allen Interfaces (für Remote-Worker).
|
|
Im vertrauten LAN ist das ein akzeptierter Kompromiss — **exponiere Rippy niemals
|
|
ungeschützt an ein unsicheres oder öffentliches Netz.** Wer es weiter absichern will:
|
|
|
|
- **Zugriff kapseln:** API (`:8000`) und UI hinter einen Reverse-Proxy mit Auth
|
|
(Basic-/Forward-Auth) legen oder nur über VPN erreichbar machen; Port `8000`
|
|
nicht auf `0.0.0.0` binden.
|
|
- **DB/Broker abschotten:** `POSTGRES_PASSWORD` in der `.env` auf ein starkes
|
|
Passwort setzen; die Ports `5432`/`6379` nur an ein internes/VPN-Interface binden
|
|
(z. B. `"10.0.0.5:5432:5432"`) statt an `0.0.0.0`; Redis mit `--requirepass
|
|
<geheim>` starten und das Passwort in `REDIS_URL` ergänzen.
|
|
- **CORS:** `main.py` erlaubt aktuell `*` (nötig, weil UI und API getrennte Ports
|
|
sind) — hinter einem Proxy auf die echte UI-Herkunft einschränken.
|
|
- **NAS-Zugangsdaten** liegen bewusst im Klartext in der DB — ein weiterer Grund,
|
|
den DB-Port nie offen ins unsichere Netz zu hängen.
|
|
|
|
## Entwicklung
|
|
|
|
CI („Ampel") läuft bei jedem Push: Ruff, pytest, Vite-Build. Es gibt genau
|
|
einen Branch, `main` (Single Source of Truth) — die Ampel prüft nur,
|
|
deployt wird direkt aus `main`. Rot heißt: nicht deployen.
|
|
Regeln für Beiträge: [AGENTS.md](AGENTS.md) · Konzept: [KONZEPT.md](KONZEPT.md) ·
|
|
Fahrplan: [ROADMAP.md](ROADMAP.md)
|