diff --git a/README.md b/README.md
index 9d9d3f0..565717a 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,14 @@ der Flimmerkiste und dem warmen Flimmern, das ein Bildschirm im dunklen Zimmer
verbreitet. Metadaten kommen von [TMDB](https://www.themoviedb.org/), die Daten
bleiben auf deinem Server.
+
+
+
+
+> **Bebilderte Anleitung:** [docs/ANLEITUNG.md](docs/ANLEITUNG.md) —
+> führt einmal durch die ganze Anwendung.
+> **Schnittstelle:** [docs/API.md](docs/API.md).
+
## Was Flimmer kann
- **Tracking** — Serien und Filme mit Status (Schaue gerade, Pausiert,
@@ -29,8 +37,13 @@ bleiben auf deinem Server.
Gotify oder Webhook (Discord/Slack-kompatibel), pro Nutzer einstellbar.
- **Mehrere Nutzer** — jede Person hat ihre eigene Bibliothek; Universen lassen
sich teilen. Der erste registrierte Zugang wird Administrator.
+- **Übersicht, Kalender und Statistik** — was als Nächstes dran ist, wann welche
+ Episode läuft, und wie viel Zeit wohin geflossen ist.
+- **Import und Export** — aus Trakt.tv oder CSV hinein, als CSV oder JSON hinaus.
- **REST-API und PWA** — dokumentierte Schnittstelle mit eigenen API-Schlüsseln,
- installierbar auf dem Startbildschirm.
+ installierbar auf dem Startbildschirm, mit Offline-Rückfall.
+- **Papierkorb statt Löschen** — 30 Tage Bedenkzeit, und jede Sichtung lässt sich
+ mit einem Klick zurücknehmen.
## Schnellstart
@@ -165,22 +178,49 @@ npm run dev
| `npm run db:migrate` | Migration erzeugen und anwenden |
| `npm run db:studio` | Prisma Studio |
| `npm run db:seed` | Beispieldaten |
+| `npm test` | Testlauf gegen die Entwicklungsdatenbank (braucht keinen TMDB-Schlüssel) |
| `npm run icons` | Favicon und PWA-Icons aus dem SVG-Logo neu erzeugen |
+| `npm run screenshots` | Screenshots der Anleitung neu aufnehmen |
+
+Ohne TMDB-Schlüssel lässt sich die Suche mit einem lokalen Ersatz ausprobieren,
+der aus den Beispieldaten antwortet:
+
+```bash
+node --import tsx scripts/tmdb-stub.mjs 3099
+TMDB_API_KEY=stub TMDB_BASE_URL=http://127.0.0.1:3099/3 npm run dev
+```
Aufbau des Projekts:
```
src/
- app/ Routen (App Router), API-Endpunkte
+ app/ Routen (App Router), API-Endpunkte, Server-Aktionen
components/ Wiederverwendbare Bausteine
- lib/ Serverseitige Logik: DB, Auth, TMDB, Benachrichtigungen
+ lib/ Serverseitige Logik: DB, Auth, TMDB, Tracking, Statistik,
+ Universen, Benachrichtigungen, Abgleich, Import/Export
i18n/ Oberflächentexte
-prisma/ Schema, Migrationen, Seed
+prisma/ Schema, Migrationen, Seed samt Beispielkatalog
+tests/ Testlauf mit nachgebildeten TMDB-Antworten
public/brand/ Logo in allen Varianten
docker/ Entrypoint des Containers
-docs/ Anleitung, Design-Dokumentation
+docs/ Anleitung mit Screenshots, API, Design-Dokumentation
```
+## Hintergrundlauf
+
+Einmal täglich frischt Flimmer laufende Serien bei TMDB auf, meldet neue
+Episoden, prüft die Verfügbarkeit von Watchlist-Titeln und leert den
+Papierkorb. Gesteuert wird das über `ENABLE_SCHEDULER` und `SYNC_HOUR`.
+
+Wer lieber einen eigenen Cron benutzt, setzt `ENABLE_SCHEDULER=false`, vergibt
+ein `CRON_SECRET` und ruft auf:
+
+```bash
+curl -X POST -H "Authorization: Bearer $CRON_SECRET" https://flimmer.example.com/api/cron/sync
+```
+
+In der Verwaltung lässt sich der Lauf jederzeit von Hand anstoßen.
+
## Technik
Next.js (App Router) mit TypeScript, PostgreSQL über Prisma, Tailwind CSS.
diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md
new file mode 100644
index 0000000..3619421
--- /dev/null
+++ b/docs/ANLEITUNG.md
@@ -0,0 +1,360 @@
+
+
+
+
+# Flimmer — Anleitung
+
+Diese Anleitung führt einmal durch die ganze Anwendung: vom ersten Konto über
+das tägliche Abhaken bis zu Universen, Statistik und Schnittstelle.
+
+Die Bilder stammen aus einer Instanz mit Beispieldaten (`npm run db:seed`).
+Die Poster darin sind erfunden — mit hinterlegtem TMDB-Schlüssel stehen dort
+die echten.
+
+## Inhalt
+
+1. [Erster Start](#erster-start)
+2. [Die Übersicht](#die-übersicht)
+3. [Titel finden und aufnehmen](#titel-finden-und-aufnehmen)
+4. [Eine Serie verfolgen](#eine-serie-verfolgen)
+5. [Die Bibliothek](#die-bibliothek)
+6. [Universen](#universen)
+7. [Kalender](#kalender)
+8. [Statistik](#statistik)
+9. [Einstellungen](#einstellungen)
+10. [Benachrichtigungen](#benachrichtigungen)
+11. [Import und Export](#import-und-export)
+12. [API-Schlüssel](#api-schlüssel)
+13. [Papierkorb](#papierkorb)
+14. [Verwaltung](#verwaltung)
+15. [Auf dem Handy](#auf-dem-handy)
+16. [Helles Farbschema](#helles-farbschema)
+
+---
+
+## Erster Start
+
+
+
+Nach der Installation ruf die Adresse deiner Instanz auf. Solange es noch kein
+einziges Konto gibt, ist die Registrierung offen — **der erste Zugang wird
+automatisch Administrator**. Lege ihn also gleich für dich an.
+
+Danach kannst du die Registrierung in der [Verwaltung](#verwaltung) schließen,
+damit sich niemand sonst anmeldet.
+
+> Ist SSO eingerichtet (`OIDC_*` in der `.env`), erscheint hier zusätzlich ein
+> Knopf für die Anmeldung über den eigenen Anbieter.
+
+---
+
+## Die Übersicht
+
+
+
+Die Startseite beantwortet drei Fragen:
+
+- **Weiterschauen** — welche Serien laufen gerade, und welche Episode ist als
+ Nächstes dran. Der Knopf **+1 Episode** hakt genau diese Folge ab; du musst
+ die Serie dafür nicht öffnen.
+- **Demnächst** — welche Episoden deiner Serien in den nächsten vier Wochen
+ ausgestrahlt werden.
+- **Neu verfügbar** — welche Titel von deiner Watchlist zuletzt auf einem
+ Streamingdienst in deiner Region aufgetaucht sind.
+
+Ist die Bibliothek noch leer, steht hier stattdessen der Weg zur Suche.
+
+---
+
+## Titel finden und aufnehmen
+
+
+
+Über **Suchen** durchsuchst du TMDB. Die Treffer lassen sich auf *Alles*,
+*Serien* oder *Filme* eingrenzen; die Suchanfrage steht in der Adresszeile und
+ist damit teilbar.
+
+Ein Klick auf einen Treffer holt die vollständigen Daten — bei Serien alle
+Staffeln und Episoden — und öffnet die Detailseite. Von dort nimmst du den
+Titel auf, wahlweise als **Ich schaue das gerade** oder **Auf die Watchlist**.
+
+> **Nichts dabei?** Unter *Titel selbst anlegen* trägst du etwas ein, das TMDB
+> nicht kennt — Heimvideos, Vorträge, regionale Produktionen. Solche Titel
+> werden vom automatischen Abgleich nie angefasst.
+
+---
+
+## Eine Serie verfolgen
+
+
+
+Die Detailseite ist der Ort, an dem du eine Serie führst:
+
+- **Fortschrittsbalken** mit gesehenen Episoden und verbleibender Laufzeit,
+ berechnet aus der echten Episodenanzahl.
+- **Als Nächstes dran** nennt Staffel, Episode, Titel und Ausstrahlungsdatum.
+ Der Knopf **+1 Episode** hakt sie ab.
+- Der **Status** (Schaue gerade, Watchlist, Pausiert, Abgeschlossen,
+ Abgebrochen) ergibt sich beim Abhaken von selbst: die erste Episode startet
+ die Serie, die letzte schließt sie ab. Pausiert und Abgebrochen bleiben
+ stehen — wer bewusst pausiert hat, will das nicht durch ein Häkchen
+ aufgehoben bekommen.
+- In der **Staffelliste** hakst du einzelne Episoden ab, ganze Staffeln auf
+ einmal, oder über *Bis hierhin gesehen* alles Vorherige. Beim Abhaken glimmt
+ die Zeile kurz warm auf.
+- Rechts stehen **Bewertung** (1 bis 10), **wo du das schaust** (mit Vorschlag
+ aus der Streaming-Verfügbarkeit), **Tags** und ein **Notizfeld**.
+
+Jede Sichtung meldet sich unten kurz zurück und lässt sich mit **Rückgängig**
+zurücknehmen.
+
+Ist eine Serie durch, erscheint **Noch einmal schauen** — der Fortschritt
+beginnt von vorn, die alten Sichtungen bleiben für die Statistik erhalten. Die
+Karte zeigt dann „2. Durchlauf".
+
+---
+
+## Die Bibliothek
+
+
+
+Alles, was du verfolgst, als Posterraster. Jede Kachel zeigt Status,
+Bewertung und — bei Serien — den Fortschritt.
+
+Filtern kannst du nach:
+
+| Filter | Wirkung |
+|---|---|
+| Volltextsuche | Titel und Originaltitel |
+| Status | Schaue gerade, Watchlist, Pausiert, Abgeschlossen, Abgebrochen |
+| Typ | nur Serien oder nur Filme |
+| Genre | alle Genres, die in deiner Bibliothek vorkommen |
+| Dienst | Streamingdienste deiner Titel |
+| Tag | deine eigenen Schlagworte |
+
+Sortieren geht nach zuletzt aktualisiert, zuletzt hinzugefügt, Titel, Bewertung
+oder Erscheinungsjahr. Die gesamte Auswahl steht in der Adresszeile — ein
+Lesezeichen auf „alle pausierten Krimis" ist damit möglich.
+
+---
+
+## Universen
+
+
+
+Ein Universum bündelt Titel, die zusammengehören — MCU, Star Wars, Wizarding
+World — und zwar **Filme und Serien gemeinsam**.
+
+
+
+Auf der Detailseite steht oben der Gesamtfortschritt: wie viel abgeschlossen
+ist, wie viel Zeit schon drinsteckt und wie lange es bis zum Ende noch dauert.
+Der erste noch offene Eintrag ist als **Als Nächstes dran** hervorgehoben.
+
+**Mehrere Reihenfolgen** sind der Kern der Sache:
+
+- Die **Erscheinungsreihenfolge** ist immer da und wird aus den
+ Erscheinungsdaten berechnet — sie stimmt damit auch, wenn später etwas
+ dazukommt.
+- Über **+ Reihenfolge** legst du weitere an, etwa *Chronologisch* oder
+ *Für Einsteiger*. Sortiert wird per Ziehen oder über die Pfeile neben jedem
+ Eintrag; gespeichert wird sofort.
+
+Titel nimmst du rechts aus deiner Bibliothek auf. Gehört ein Film zu einer
+TMDB-Reihe (etwa „Spider-Man Collection"), lässt sich die ganze Reihe mit einem
+Klick übernehmen.
+
+**Teilen** geht mit anderen Konten derselben Instanz, wahlweise nur zum Ansehen
+oder zum gemeinsamen Pflegen. Der Fortschritt bleibt dabei persönlich: jede
+Person sieht ihren eigenen Stand.
+
+---
+
+## Kalender
+
+
+
+Das Monatsraster zeigt, wann Episoden deiner Serien laufen. Tage mit Terminen
+sind hinterlegt, der heutige Tag trägt den Signaturverlauf. Unter dem Raster
+stehen dieselben Termine noch einmal als Liste mit vollem Datum.
+
+Mit den Pfeilen blätterst du durch die Monate, **Heute** springt zurück.
+
+Abgebrochene Serien tauchen hier nicht auf. Abgeschlossene schon — wer bei
+einer laufenden Serie auf dem Stand ist, will die nächste Folge erst recht
+sehen.
+
+---
+
+## Statistik
+
+
+
+Oben die **Gesamtschauzeit** in Stunden und umgerechnet in Tage am Stück, daneben
+Episoden, Filme und abgeschlossene Titel.
+
+- **Die letzten zwölf Monate** — abgehakte Episoden und Filme je Monat.
+ Beim Überfahren einer Säule erscheinen die genauen Zahlen samt Schauzeit.
+- **Nach Streamingdienst** — wo du tatsächlich geschaut hast. Grundlage ist das
+ Feld *wo schaue ich das*, nicht die bloße Verfügbarkeit.
+- **Nach Genre** — Titel mit mehreren Genres zählen in jedem mit.
+- **Jahr in Zahlen** — Schauzeit, Episoden, Filme, neu Angefangenes,
+ Abgeschlossenes, der stärkste Monat und der Titel mit der meisten Zeit.
+ Über die Jahreszahlen wechselst du das Jahr.
+- Darunter **wo die meiste Zeit hinging** und **deine besten Bewertungen**.
+
+Alles ist aus den Sichtungen gerechnet; Rewatches zählen mit.
+
+---
+
+## Einstellungen
+
+
+
+Hier stellst du Anzeigename und **Region** ein — Letztere bestimmt, welche
+Streamingdienste angezeigt werden (Standard: DE).
+
+Darunter legst du fest, **wovon Flimmer dir berichten soll**: neue Episoden,
+neue Releases, neu verfügbare Watchlist-Titel.
+
+Wenn der Betreiber öffentliche Profile erlaubt, kannst du deins freischalten
+und eine Adresse wählen. Die Profilseite zeigt Zahlen und zuletzt Gesehenes —
+**keine** Notizen, keine Watchlist, keine E-Mail-Adresse.
+
+Das Passwort änderst du im Abschnitt darunter; danach wirst du auf allen
+Geräten abgemeldet.
+
+---
+
+## Benachrichtigungen
+
+
+
+*Wovon* berichtet wird, steht im Profil — *wohin*, hier. Vier Wege stehen zur
+Wahl:
+
+| Kanal | Nötige Angaben |
+|---|---|
+| **E-Mail** | nichts (oder eine abweichende Adresse); der SMTP-Zugang kommt vom Betreiber |
+| **ntfy** | Server (Standard `https://ntfy.sh`) und Topic, optional ein Token |
+| **Gotify** | Serveradresse und Anwendungs-Token |
+| **Webhook** | die Zieladresse — funktioniert mit Discord, Slack und Mattermost |
+
+Jeder Kanal lässt sich einzeln stummschalten und mit **Testen** ausprobieren.
+Unten steht, was zuletzt verschickt wurde — auch fehlgeschlagene Versuche mit
+Grund.
+
+Dieselbe Meldung kommt nie zweimal: Flimmer merkt sich, was schon draußen ist.
+
+---
+
+## Import und Export
+
+
+
+**Import** aus zwei Quellen:
+
+- **CSV**, wie es Tabellenprogramme und viele Tracker ausgeben. Die Kopfzeile
+ braucht mindestens eine Titelspalte (`Titel`, `title`, `name` oder `show`);
+ erkannt werden außerdem Typ, Status, Bewertung, TMDB-ID, Jahr und Notiz.
+ Komma und Semikolon als Trennzeichen funktionieren beide.
+- **Trakt.tv** — ein JSON-Export aus Watchlist, History, Ratings oder
+ Collection. Flimmer sucht sich die Einträge selbst heraus.
+
+Der Import **überschreibt nichts**. Titel, die schon in deiner Bibliothek
+liegen, bleiben unangetastet; es kommt nur dazu, was fehlt.
+
+**Export** als JSON (mit allen Sichtungen und Universen) oder als CSV für
+Tabellenkalkulationen.
+
+---
+
+## API-Schlüssel
+
+
+
+Für Skripte und eigene Anwendungen. Ein Schlüssel ist **einmalig** sichtbar —
+danach liegt in der Datenbank nur ein Hash. Er hat dieselben Rechte wie dein
+Konto; zieh ihn zurück, sobald du ihn nicht mehr brauchst.
+
+```bash
+curl -H "Authorization: Bearer flm_…" \
+ https://flimmer.example.com/api/v1/bibliothek?status=WATCHING
+```
+
+Alle Endpunkte stehen in [`API.md`](API.md); die maschinenlesbare Fassung
+liegt unter `/api/v1/openapi.json`.
+
+---
+
+## Papierkorb
+
+
+
+Entfernte Titel landen für **30 Tage** im Papierkorb und behalten dabei ihren
+Fortschritt. Von dort lassen sie sich wiederherstellen oder sofort endgültig
+löschen. Nimmst du denselben Titel erneut auf, lebt der alte Eintrag samt
+Sichtungen wieder auf.
+
+Nach Ablauf der Frist räumt der tägliche Hintergrundlauf auf.
+
+---
+
+## Verwaltung
+
+
+
+Nur für Administratoren. Hier lässt sich
+
+- der **Name der Instanz** ändern,
+- die **Registrierung** öffnen oder schließen,
+- **öffentliche Profile** instanzweit erlauben oder abschalten,
+- der **Abgleich** von Hand anstoßen,
+- und der Zustand der Instanz ablesen: Anzahl Konten, Umfang des Katalogs und
+ ob TMDB, SMTP, Zeitplan und OIDC eingerichtet sind.
+
+In der Kontenliste vergibst du Verwaltungsrechte und sperrst Konten. Ein
+gesperrtes Konto wird sofort von allen Geräten abgemeldet. Die letzte
+Administration lässt sich nicht entmachten, und sich selbst kann niemand
+sperren.
+
+---
+
+## Auf dem Handy
+
+
+
+Die Oberfläche ist für schmale Geräte ausgelegt: die Navigation rutscht in eine
+eigene, wischbare Zeile, Karten stapeln sich untereinander, und der Knopf
+**+1 Episode** bleibt groß genug für den Daumen.
+
+Flimmer ist als **PWA** installierbar — im Browsermenü *Zum Startbildschirm
+hinzufügen*. Danach startet die App im eigenen Fenster ohne Adresszeile.
+Bereits geladene Seiten und Poster bleiben auch ohne Verbindung sichtbar;
+statt einer Fehlerseite erscheint dann ein Hinweis.
+
+---
+
+## Helles Farbschema
+
+
+
+Über den Knopf im Kopfbereich wechselst du zwischen **Dunkel**, **Hell** und
+**System**. Die Wahl bleibt auf dem Gerät gespeichert und greift, bevor die
+Seite gezeichnet wird — es blitzt nichts auf.
+
+---
+
+## Und wenn etwas fehlt?
+
+- **Die Suche findet nichts** → Es fehlt der TMDB-Schlüssel. Trag
+ `TMDB_API_KEY` in die `.env` ein und starte die Instanz neu.
+- **Keine Streamingdienste** → Prüfe die Region in den Einstellungen. TMDB hat
+ nicht für jedes Land Daten zu jedem Titel.
+- **Keine Benachrichtigungen** → Erst im Profil auswählen, *wovon* berichtet
+ werden soll, dann einen Kanal anlegen und mit *Testen* prüfen.
+- **Neue Episoden fehlen** → Der Abgleich läuft einmal täglich. In der
+ Verwaltung lässt er sich sofort anstoßen.
+
+Alles Weitere zur Installation, zum Aktualisieren und zum Backup steht in der
+[README](../README.md).
diff --git a/docs/API.md b/docs/API.md
new file mode 100644
index 0000000..86b41df
--- /dev/null
+++ b/docs/API.md
@@ -0,0 +1,190 @@
+# Flimmer — REST-API
+
+Alles, was die Oberfläche kann, geht auch über die Schnittstelle: Bibliothek
+lesen und pflegen, Episoden abhaken, suchen, Kalender und Statistik abrufen,
+Universen verwalten, Daten exportieren.
+
+Die maschinenlesbare Beschreibung liegt unter
+`GET /api/v1/openapi.json` (OpenAPI 3.1) — damit lassen sich Clients erzeugen
+oder die Endpunkte in Insomnia, Bruno oder Postman importieren.
+
+## Anmeldung
+
+Jede Anfrage trägt einen persönlichen API-Schlüssel im `Authorization`-Kopf:
+
+```bash
+curl -H "Authorization: Bearer flm_a1b2c3d4.xxxxxxxxxxxx" \
+ https://flimmer.example.com/api/v1/me
+```
+
+Schlüssel legst du unter **Einstellungen → API-Schlüssel** an. Der Klartext ist
+**einmalig** sichtbar; danach liegt in der Datenbank nur ein Hash. Ein Schlüssel
+hat dieselben Rechte wie dein Konto — behandle ihn wie ein Passwort und zieh ihn
+zurück, sobald du ihn nicht mehr brauchst.
+
+Aus dem Browser heraus funktioniert stattdessen das Sitzungscookie; dieselben
+Endpunkte bedienen also Skripte und das eigene Frontend.
+
+## Antwortformat
+
+Alle Antworten sind JSON. Zeitangaben in ISO 8601, Laufzeiten in Minuten.
+Fehler tragen immer ein Feld `error`:
+
+```json
+{ "error": "Diesen Eintrag gibt es nicht." }
+```
+
+| Status | Bedeutung |
+|---|---|
+| 200 | In Ordnung |
+| 401 | Kein oder ungültiger Schlüssel |
+| 404 | Nicht vorhanden (oder nicht deins) |
+| 422 | Anfrage unvollständig — `details` nennt die Felder |
+| 502 / 503 | TMDB nicht erreichbar bzw. kein Schlüssel hinterlegt |
+
+## Endpunkte
+
+### Konto
+
+```http
+GET /api/v1/me
+```
+
+### Bibliothek
+
+```http
+GET /api/v1/bibliothek?status=WATCHING&typ=SERIES&genre=Drama&q=nord&limit=50&offset=0
+POST /api/v1/bibliothek
+```
+
+Filter: `q`, `status`, `typ`, `genre`, `tag`, `dienst`, `limit`, `offset`.
+Status: `WATCHING`, `PAUSED`, `COMPLETED`, `DROPPED`, `WATCHLIST`.
+
+Aufnehmen — entweder mit interner Kennung oder direkt mit der TMDB-Kennung:
+
+```bash
+curl -X POST -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
+ -d '{"tmdbId": 1396, "type": "SERIES", "status": "WATCHING"}' \
+ https://flimmer.example.com/api/v1/bibliothek
+```
+
+```http
+GET /api/v1/bibliothek/{entryId}
+PATCH /api/v1/bibliothek/{entryId}
+DELETE /api/v1/bibliothek/{entryId}
+```
+
+`GET` liefert den Eintrag samt `progress` und `nextEpisode`.
+`PATCH` ändert `status`, `rating` (1–10), `notes`, `whereToWatch`, `favorite`.
+`DELETE` legt den Eintrag in den Papierkorb — dort bleibt er 30 Tage.
+
+### Sichtungen
+
+```http
+GET /api/v1/bibliothek/{entryId}/sichtungen
+POST /api/v1/bibliothek/{entryId}/sichtungen
+DELETE /api/v1/bibliothek/{entryId}/sichtungen
+```
+
+`POST` kennt fünf Formen:
+
+| Rumpf | Wirkung |
+|---|---|
+| `{"action":"next"}` | Die nächste offene Episode abhaken |
+| `{"action":"movie"}` | Einen Film als gesehen markieren |
+| `{"action":"episodes","episodeIds":["…"]}` | Eine Auswahl abhaken |
+| `{"action":"season","seasonId":"…"}` | Eine ganze Staffel |
+| `{"action":"upTo","episodeId":"…"}` | Alles bis einschließlich dieser Episode |
+
+Die Antwort trägt `watchIds` — damit lässt sich genau diese Aktion zurücknehmen:
+
+```bash
+# Nächste Episode abhaken
+IDS=$(curl -s -X POST -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
+ -d '{"action":"next"}' \
+ https://flimmer.example.com/api/v1/bibliothek/$ENTRY/sichtungen | jq -c .watchIds)
+
+# … und wieder zurücknehmen
+curl -X DELETE -H "Authorization: Bearer $KEY" -H "content-type: application/json" \
+ -d "{\"watchIds\": $IDS}" \
+ https://flimmer.example.com/api/v1/bibliothek/$ENTRY/sichtungen
+```
+
+Alternativ nimmt `DELETE` auch `{"episodeIds":["…"]}` — dann verschwindet die
+Markierung dieser Episoden im aktuellen Durchlauf.
+
+### Suche
+
+```http
+GET /api/v1/suche?q=nordlicht&typ=serie
+```
+
+Durchsucht TMDB. Die Treffer tragen `tmdbId` und `type` — direkt verwendbar für
+`POST /api/v1/bibliothek`. Ohne hinterlegten TMDB-Schlüssel antwortet der
+Endpunkt mit 503.
+
+### Kalender
+
+```http
+GET /api/v1/kalender?von=2026-09-01&bis=2026-09-30
+```
+
+Kommende Episoden der getrackten Serien. Ohne Zeitraum: die nächsten 30 Tage.
+
+### Statistik
+
+```http
+GET /api/v1/statistik?jahr=2026
+```
+
+Liefert `totals`, `monthly`, `providers`, `genres` und `yearReview` auf einmal.
+
+### Universen
+
+```http
+GET /api/v1/universen
+POST /api/v1/universen
+GET /api/v1/universen/{id}?reihenfolge={orderId}
+POST /api/v1/universen/{id}
+```
+
+`GET /api/v1/universen/{id}` gibt die Einträge in der gewählten Reihenfolge
+zurück, dazu `progress` und `nextUp`. Ohne `reihenfolge` gilt die
+Erscheinungsreihenfolge.
+
+### Export
+
+```http
+GET /api/v1/export?format=json
+GET /api/v1/export?format=csv
+```
+
+JSON enthält zusätzlich alle Sichtungen und Universen.
+
+## Beispiel: Serien mit offener Episode auflisten
+
+```bash
+#!/usr/bin/env bash
+set -euo pipefail
+
+KEY="flm_…"
+HOST="https://flimmer.example.com"
+
+curl -s -H "Authorization: Bearer $KEY" \
+ "$HOST/api/v1/bibliothek?status=WATCHING&typ=SERIES" \
+| jq -r '.items[] | "\(.title): \(.watched)/\(.total)"'
+```
+
+## Grenzen
+
+Es gibt bewusst keine Ratenbegrenzung: Flimmer läuft auf deinem eigenen Server,
+und ein Limit gegen dich selbst hilft niemandem. Wer die Instanz öffentlich
+erreichbar macht, setzt so etwas besser im Reverse Proxy.
+
+Der Abgleich mit TMDB lässt sich über einen eigenen Endpunkt anstoßen — der
+nimmt allerdings das Cron-Geheimnis, nicht einen API-Schlüssel:
+
+```bash
+curl -X POST -H "Authorization: Bearer $CRON_SECRET" \
+ https://flimmer.example.com/api/cron/sync
+```
diff --git a/docs/screenshots/01-anmeldung.webp b/docs/screenshots/01-anmeldung.webp
new file mode 100644
index 0000000..743ff5d
Binary files /dev/null and b/docs/screenshots/01-anmeldung.webp differ
diff --git a/docs/screenshots/02-uebersicht.webp b/docs/screenshots/02-uebersicht.webp
new file mode 100644
index 0000000..866f6e3
Binary files /dev/null and b/docs/screenshots/02-uebersicht.webp differ
diff --git a/docs/screenshots/03-suche.webp b/docs/screenshots/03-suche.webp
new file mode 100644
index 0000000..29c0aff
Binary files /dev/null and b/docs/screenshots/03-suche.webp differ
diff --git a/docs/screenshots/04-serie.webp b/docs/screenshots/04-serie.webp
new file mode 100644
index 0000000..7e4a5dc
Binary files /dev/null and b/docs/screenshots/04-serie.webp differ
diff --git a/docs/screenshots/05-bibliothek.webp b/docs/screenshots/05-bibliothek.webp
new file mode 100644
index 0000000..f33c7b7
Binary files /dev/null and b/docs/screenshots/05-bibliothek.webp differ
diff --git a/docs/screenshots/06-universen.webp b/docs/screenshots/06-universen.webp
new file mode 100644
index 0000000..c818e39
Binary files /dev/null and b/docs/screenshots/06-universen.webp differ
diff --git a/docs/screenshots/07-universum.webp b/docs/screenshots/07-universum.webp
new file mode 100644
index 0000000..9627667
Binary files /dev/null and b/docs/screenshots/07-universum.webp differ
diff --git a/docs/screenshots/08-hell.webp b/docs/screenshots/08-hell.webp
new file mode 100644
index 0000000..9a5c88d
Binary files /dev/null and b/docs/screenshots/08-hell.webp differ
diff --git a/docs/screenshots/09-kalender.webp b/docs/screenshots/09-kalender.webp
new file mode 100644
index 0000000..60edd3b
Binary files /dev/null and b/docs/screenshots/09-kalender.webp differ
diff --git a/docs/screenshots/10-statistik.webp b/docs/screenshots/10-statistik.webp
new file mode 100644
index 0000000..5a914a4
Binary files /dev/null and b/docs/screenshots/10-statistik.webp differ
diff --git a/docs/screenshots/11-einstellungen.webp b/docs/screenshots/11-einstellungen.webp
new file mode 100644
index 0000000..c3f3a39
Binary files /dev/null and b/docs/screenshots/11-einstellungen.webp differ
diff --git a/docs/screenshots/12-benachrichtigungen.webp b/docs/screenshots/12-benachrichtigungen.webp
new file mode 100644
index 0000000..0279981
Binary files /dev/null and b/docs/screenshots/12-benachrichtigungen.webp differ
diff --git a/docs/screenshots/13-api.webp b/docs/screenshots/13-api.webp
new file mode 100644
index 0000000..e05d346
Binary files /dev/null and b/docs/screenshots/13-api.webp differ
diff --git a/docs/screenshots/14-daten.webp b/docs/screenshots/14-daten.webp
new file mode 100644
index 0000000..52d09fe
Binary files /dev/null and b/docs/screenshots/14-daten.webp differ
diff --git a/docs/screenshots/15-verwaltung.webp b/docs/screenshots/15-verwaltung.webp
new file mode 100644
index 0000000..89d9cad
Binary files /dev/null and b/docs/screenshots/15-verwaltung.webp differ
diff --git a/docs/screenshots/16-papierkorb.webp b/docs/screenshots/16-papierkorb.webp
new file mode 100644
index 0000000..17190d9
Binary files /dev/null and b/docs/screenshots/16-papierkorb.webp differ
diff --git a/docs/screenshots/17-mobil-uebersicht.webp b/docs/screenshots/17-mobil-uebersicht.webp
new file mode 100644
index 0000000..7226359
Binary files /dev/null and b/docs/screenshots/17-mobil-uebersicht.webp differ
diff --git a/docs/screenshots/18-mobil-serie.webp b/docs/screenshots/18-mobil-serie.webp
new file mode 100644
index 0000000..e437316
Binary files /dev/null and b/docs/screenshots/18-mobil-serie.webp differ
diff --git a/package.json b/package.json
index 4dcd7eb..181ca1a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "flimmer",
- "version": "0.5.0",
+ "version": "0.6.0",
"private": true,
"description": "Flimmer — selbst hostbarer Serien- und Film-Tracker",
"license": "MIT",
@@ -15,8 +15,9 @@
"db:studio": "prisma studio",
"db:seed": "tsx prisma/seed.ts",
"icons": "tsx scripts/generate-icons.ts",
- "screenshots": "tsx scripts/screenshots.ts",
- "test": "node --env-file-if-exists=.env --conditions=react-server --test-concurrency=1 --import tsx --test tests/*.test.ts"
+ "screenshots": "node scripts/screenshots.mjs && node scripts/optimize-screenshots.mjs",
+ "test": "node --env-file-if-exists=.env --conditions=react-server --test-concurrency=1 --import tsx --test tests/*.test.ts",
+ "tmdb:stub": "node --import tsx scripts/tmdb-stub.mjs"
},
"dependencies": {
"@prisma/adapter-pg": "^7.10.0",
diff --git a/scripts/optimize-screenshots.mjs b/scripts/optimize-screenshots.mjs
new file mode 100644
index 0000000..b4bfd24
--- /dev/null
+++ b/scripts/optimize-screenshots.mjs
@@ -0,0 +1,34 @@
+import sharp from "sharp";
+import { readdir, unlink, stat } from "node:fs/promises";
+import { join } from "node:path";
+
+/**
+ * Verkleinert die aufgenommenen Screenshots für das Repository.
+ * Die Aufnahme läuft mit doppelter Pixeldichte (scharfe Schrift), abgelegt
+ * wird die halbe Kantenlänge als WebP — das spart rund 90 Prozent.
+ */
+const DIR = "docs/screenshots";
+const files = (await readdir(DIR)).filter((name) => name.endsWith(".png"));
+
+let before = 0;
+let after = 0;
+
+for (const name of files) {
+ const source = join(DIR, name);
+ before += (await stat(source)).size;
+
+ const image = sharp(source);
+ const meta = await image.metadata();
+ const target = join(DIR, name.replace(/\.png$/, ".webp"));
+
+ await image
+ .resize({ width: Math.round((meta.width ?? 1400) / 2) })
+ .webp({ quality: 82, effort: 5 })
+ .toFile(target);
+
+ after += (await stat(target)).size;
+ await unlink(source);
+}
+
+const mb = (bytes) => (bytes / 1024 / 1024).toFixed(1);
+console.log(`${files.length} Bilder: ${mb(before)} MB -> ${mb(after)} MB`);
diff --git a/scripts/screenshots.mjs b/scripts/screenshots.mjs
new file mode 100644
index 0000000..760c5d7
--- /dev/null
+++ b/scripts/screenshots.mjs
@@ -0,0 +1,99 @@
+import { chromium } from "playwright";
+import fs from "node:fs";
+
+const OUT = "docs/screenshots";
+const BASE = "http://localhost:3011";
+fs.mkdirSync(OUT, { recursive: true });
+
+const browser = await chromium.launch();
+const errs = [];
+
+async function session(width, height, theme) {
+ const ctx = await browser.newContext({
+ viewport: { width, height },
+ deviceScaleFactor: 2,
+ colorScheme: theme === "light" ? "light" : "dark",
+ locale: "de-DE",
+ timezoneId: "Europe/Berlin",
+ });
+ await ctx.addInitScript((mode) => localStorage.setItem("flimmer-theme", mode), theme);
+ // Der Entwicklungs-Indikator von Next hat in einer Anleitung nichts verloren.
+ await ctx.addStyleTag?.({ content: "nextjs-portal { display: none !important; }" });
+ const page = await ctx.newPage();
+ await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }).catch(() => {});
+ page.on("pageerror", (e) => errs.push(String(e).slice(0, 160)));
+ return { ctx, page };
+}
+
+async function login(page) {
+ await page.goto(`${BASE}/anmelden`);
+ await page.fill("#email", "demo@flimmer.local");
+ await page.fill("#password", "flimmer-demo-2026");
+ await Promise.all([page.waitForURL(`${BASE}/`), page.click("button[type=submit]")]);
+}
+
+async function shot(page, name, url, opts = {}) {
+ await page.goto(`${BASE}${url}`, { waitUntil: "networkidle" });
+ await page.addStyleTag({ content: "nextjs-portal { display: none !important; }" }).catch(() => {});
+ await page.waitForTimeout(opts.wait ?? 900);
+ if (opts.before) await opts.before(page);
+ await page.screenshot({ path: `${OUT}/${name}.png`, fullPage: Boolean(opts.full) });
+ console.log(" ", name);
+}
+
+// --- Anmeldung (ohne Sitzung) ---
+{
+ const { ctx, page } = await session(1280, 800, "dark");
+ await shot(page, "01-anmeldung", "/anmelden");
+ await ctx.close();
+}
+
+// --- Angemeldet, dunkel ---
+{
+ const { ctx, page } = await session(1400, 900, "dark");
+ await login(page);
+
+ await shot(page, "02-uebersicht", "/", { full: true });
+ await shot(page, "03-suche", "/suche?q=sternenpfad");
+ await shot(page, "05-bibliothek", "/bibliothek");
+ await shot(page, "09-kalender", "/kalender");
+ await shot(page, "10-statistik", "/statistik", { full: true });
+ await shot(page, "11-einstellungen", "/einstellungen");
+ await shot(page, "12-benachrichtigungen", "/einstellungen/benachrichtigungen");
+ await shot(page, "13-api", "/einstellungen/api");
+ await shot(page, "14-daten", "/einstellungen/daten");
+ await shot(page, "15-verwaltung", "/verwaltung", { full: true });
+ await shot(page, "16-papierkorb", "/papierkorb");
+
+ // Detailseite und Universum brauchen IDs aus der Bibliothek
+ await page.goto(`${BASE}/bibliothek?q=nordlicht`, { waitUntil: "networkidle" });
+ const titleHref = await page.locator("a[href^='/titel/']").first().getAttribute("href");
+ if (titleHref) await shot(page, "04-serie", titleHref, { full: true });
+
+ await page.goto(`${BASE}/universen`, { waitUntil: "networkidle" });
+ await shot(page, "06-universen", "/universen");
+ const uniHref = await page.locator("a[href^='/universen/']").first().getAttribute("href");
+ if (uniHref) await shot(page, "07-universum", uniHref, { full: true });
+
+ await ctx.close();
+}
+
+// --- Hell ---
+{
+ const { ctx, page } = await session(1400, 900, "light");
+ await login(page);
+ await shot(page, "08-hell", "/bibliothek");
+ await ctx.close();
+}
+
+// --- Mobil ---
+{
+ const { ctx, page } = await session(430, 900, "dark");
+ await login(page);
+ await shot(page, "17-mobil-uebersicht", "/");
+ await shot(page, "18-mobil-serie", "/bibliothek");
+ await ctx.close();
+}
+
+console.log("Fehler:", errs.length ? [...new Set(errs)].slice(0, 5) : "keine");
+await browser.close();