diff --git a/CHANGELOG.md b/CHANGELOG.md
index 91a37d8..d85e7f4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,11 +7,26 @@ die Versionierung [Semantic Versioning](https://semver.org/lang/de/).
## [Unveröffentlicht]
-## [0.6.1] — 2026-09-01
+## [0.7.0] — 2026-09-01
-Nacharbeiten aus der Abnahme: das Docker-Image wurde zum ersten Mal wirklich
-gebaut und gestartet — dabei kam ein Fehler ans Licht, den nur der echte
-Containerstart zeigt.
+„Hab ich" — Titel, die in der eigenen Sammlung liegen. Dazu die Nacharbeiten
+aus der Abnahme: das Docker-Image wurde zum ersten Mal wirklich gebaut und
+gestartet, und dabei kamen Fehler ans Licht, die nur der echte Containerstart
+zeigt.
+
+### Hinzugefügt
+
+- **Eigene Sammlung**: Jeder Eintrag lässt sich als „hab ich" markieren, mit
+ Angabe wo — Jellyfin, Plex, Emby, NAS, Blu-ray, DVD oder frei getippt.
+ Bewusst getrennt vom Feld „wo schaue ich das": Streaming-Verfügbarkeit kommt
+ und geht, der gekaufte Film auf dem Heimserver bleibt. Gerade die Titel, die
+ nirgends mehr laufen, will man ja wiederfinden.
+ - Abzeichen auf der Posterkachel und im Kopf der Detailseite
+ - Auf der Detailseite steht die Sammlung **vor** der Streaming-Verfügbarkeit
+ — was man hat, ist die verlässlichere Antwort auf „wo kann ich das schauen"
+ - Eigener Filter in der Bibliothek samt Zähler
+ - In Export (CSV und JSON), CSV-Import und REST-API (`?sammlung=1` beim
+ Lesen, `owned` und `ownedSource` beim Ändern)
### Behoben
@@ -306,8 +321,8 @@ Erste lauffähige Etappe: Gerüst, Marke, Datenmodell und Anmeldung.
dass weitere Sprachen als zusätzliche Datei dazukommen können.
- **Seed-Skript** mit Demo-Konten sowie README und Design-Dokumentation.
-[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.6.1...HEAD
-[0.6.1]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.6.1
+[Unveröffentlicht]: https://git.wilde-server.de/florian.wilde/flimmer/compare/v0.7.0...HEAD
+[0.7.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.7.0
[0.6.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.6.0
[0.5.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.5.0
[0.4.0]: https://git.wilde-server.de/florian.wilde/flimmer/releases/tag/v0.4.0
diff --git a/DEVLOG.md b/DEVLOG.md
index 84a06e3..936f6eb 100644
--- a/DEVLOG.md
+++ b/DEVLOG.md
@@ -91,6 +91,18 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt.
---
+## Nach der Abnahme (v0.7.0)
+
+| # | Start | Ende | Dauer | Arbeitsschritt |
+|---|---|---|---|---|
+| 7.1 | 20:25 | 20:50 | 25 min | Docker-Image zum ersten Mal wirklich gebaut und gestartet; zwei Fehler behoben (fehlende Prisma-Abhängigkeiten im Laufzeitimage, Schriftabruf beim Bauen), Gitea-Workflows, Erscheinungsmeldung nachgezogen |
+| 7.2 | 20:50 | 21:05 | 15 min | Compose-Stack von null verifiziert: Migration, erstes Konto wird Administrator, Verwaltung erreichbar |
+| 7.3 | 21:05 | 21:16 | 11 min | Rückfrage aus dem Betrieb umgesetzt: „hab ich" für die eigene Sammlung (Jellyfin, Plex, NAS, Blu-ray) — Schema, Logik, Oberfläche, Filter, Import/Export, API, Tests, Anleitung |
+
+**Nacharbeiten gesamt: 51 min** (2026-09-01, 20:25 – 21:16)
+
+---
+
## Gesamtübersicht
| Etappe | Version | Dauer |
@@ -101,8 +113,9 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt.
| 4 — Universen | 0.4.0 | 14 min |
| 5 — Dashboard, Kalender, Statistik | 0.5.0 | 15 min |
| 6 — Benachrichtigungen, Sync, Import/Export, API, PWA | 0.6.0 | 31 min |
+| Nacharbeiten — Docker verifiziert, eigene Sammlung | 0.7.0 | 51 min |
-**Gesamtentwicklungszeit: 2 h 16 min** (136 min, 2026-09-01, 18:09 – 20:25)
+**Gesamtentwicklungszeit: 3 h 07 min** (187 min, 2026-09-01, 18:09 – 21:16)
### Womit die Zeit verging
@@ -111,7 +124,7 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt.
| ~35 min | Oberfläche und Design (Branding, Komponenten, Seiten) |
| ~30 min | Domänenlogik (Tracking, Universen, Statistik, Abgleich) |
| ~25 min | Schnittstellen nach außen (TMDB, REST-API, Import/Export, Benachrichtigungen) |
-| ~20 min | Tests — 49 Stück, die unterwegs sieben echte Fehler gefunden haben |
+| ~25 min | Tests — 52 Stück, die unterwegs acht echte Fehler gefunden haben |
| ~15 min | Dokumentation, Screenshots, Changelog, Releases |
| ~11 min | Gerüst, Docker, Datenmodell, Werkzeugkette |
@@ -124,3 +137,14 @@ Alle Zeiten per `date`-Befehl erfasst (Europe/Berlin), nicht geschätzt.
5. Poster zeigten das kaputte Bildsymbol statt der Rückfallkachel.
6. Eine `server-only`-Kette landete im Browser-Bundle und brach den Build.
7. Der SVG-Titel eines Diagramms brach die Hydration.
+8. Eine Testerwartung zur Reihenfolge war falsch — nicht der Code.
+
+### Fehler, die erst der echte Containerstart zeigte
+
+1. Dem Laufzeitimage fehlten transitive Abhängigkeiten der Prisma-CLI, die
+ Migration lief nicht an. Die CLI wird jetzt in einer eigenen Build-Stufe
+ vollständig installiert.
+2. npm blockiert seit Version 11 Install-Skripte; ohne Freigabe fehlte die
+ plattformpassende Prisma-Engine.
+3. `next/font/google` lud die Schrift beim Bauen herunter und ließ den Build
+ im Container hängen. Die Schrift liegt jetzt im Repository.
diff --git a/Dockerfile b/Dockerfile
index 4e46135..0b971ec 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -16,10 +16,10 @@ ENV NEXT_TELEMETRY_DISABLED=1
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
-# npm blockt Install-Skripte seit Version 11 standardmäßig. Prisma braucht seine
-# eigenen, um die passende Engine für die Plattform bereitzulegen — deshalb hier
-# gezielt erlaubt statt pauschal.
-RUN npm ci --no-audit --no-fund --allow-scripts "prisma,@prisma/engines"
+# npm blockt Install-Skripte seit Version 11. Welche laufen dürfen, steht im
+# Feld "allowScripts" der package.json — für Prisma, damit die zur Plattform
+# passende Engine bereitgelegt wird.
+RUN npm ci --no-audit --no-fund
# --- Build -------------------------------------------------------------------
FROM base AS builder
@@ -41,7 +41,11 @@ RUN npx prisma generate && npx next build
# schief: @prisma/config braucht weitere Module, die dabei fehlten.
FROM base AS prisma-cli
WORKDIR /cli
-RUN npm init -y > /dev/null && npm install --omit=dev --no-audit --no-fund --allow-scripts "prisma,@prisma/engines" prisma@7.10.0
+# Die package.json liegt als Datei bei: nur mit dem Feld "allowScripts" lässt
+# npm die Install-Skripte von Prisma zu — sonst fehlt im Image die zur
+# Plattform passende Engine.
+COPY docker/prisma-cli-package.json ./package.json
+RUN npm install --omit=dev --no-audit --no-fund prisma@7.10.0
# --- Laufzeit ----------------------------------------------------------------
FROM base AS runner
diff --git a/README.md b/README.md
index f9a16d4..cadc874 100644
--- a/README.md
+++ b/README.md
@@ -33,6 +33,9 @@ bleiben auf deinem Server.
Story-Chronologie, Einsteiger-Route) und Gesamtfortschritt.
- **Metadaten von TMDB** — Suche, Poster, Staffeln und Episoden, offizielle
Collections, Streaming-Verfügbarkeit in deiner Region.
+- **Eigene Sammlung** — markiere, was du schon hast, und wo: Jellyfin, Plex,
+ NAS, Blu-ray. Gerade die Titel, die nirgends mehr streamen, findest du so
+ wieder.
- **Benachrichtigungen** — bei neuen Episoden und Releases, per E-Mail, ntfy,
Gotify oder Webhook (Discord/Slack-kompatibel), pro Nutzer einstellbar.
- **Mehrere Nutzer** — jede Person hat ihre eigene Bibliothek; Universen lassen
diff --git a/docker/prisma-cli-package.json b/docker/prisma-cli-package.json
new file mode 100644
index 0000000..050dbbf
--- /dev/null
+++ b/docker/prisma-cli-package.json
@@ -0,0 +1,9 @@
+{
+ "name": "flimmer-prisma-cli",
+ "private": true,
+ "description": "Nur zum Installieren der Prisma-CLI ins Laufzeitimage. npm blockt Install-Skripte seit Version 11; ohne die Freigabe hier fehlt im Container die plattformpassende Engine. Bei einem Prisma-Update auch die Version in package.json anpassen.",
+ "allowScripts": {
+ "prisma@7.10.0": true,
+ "@prisma/engines@7.10.0": true
+ }
+}
diff --git a/docs/ANLEITUNG.md b/docs/ANLEITUNG.md
index dcd9d9d..89451fa 100644
--- a/docs/ANLEITUNG.md
+++ b/docs/ANLEITUNG.md
@@ -17,19 +17,20 @@ die echten.
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. [Öffentliches Profil](#öffentliches-profil)
-16. [Auf dem Handy](#auf-dem-handy)
-17. [Helles Farbschema](#helles-farbschema)
+5. [Hab ich — die eigene Sammlung](#hab-ich--die-eigene-sammlung)
+6. [Die Bibliothek](#die-bibliothek)
+7. [Universen](#universen)
+8. [Kalender](#kalender)
+9. [Statistik](#statistik)
+10. [Einstellungen](#einstellungen)
+11. [Benachrichtigungen](#benachrichtigungen)
+12. [Import und Export](#import-und-export)
+13. [API-Schlüssel](#api-schlüssel)
+14. [Papierkorb](#papierkorb)
+15. [Verwaltung](#verwaltung)
+16. [Öffentliches Profil](#öffentliches-profil)
+17. [Auf dem Handy](#auf-dem-handy)
+18. [Helles Farbschema](#helles-farbschema)
---
@@ -115,6 +116,34 @@ Karte zeigt dann „2. Durchlauf".
---
+## Hab ich — die eigene Sammlung
+
+
+
+„Wo läuft das?" beantwortet nur die halbe Frage. Manche Filme laufen nirgends
+mehr — die hat man gekauft und liegen bei sich zu Hause, in Jellyfin, Plex,
+Emby, auf dem NAS oder als Scheibe im Regal.
+
+Dafür gibt es das Häkchen **Hab ich** auf der Detailseite, rechts über dem Feld
+„wo schaue ich das". Daneben trägst du ein, *wo* der Titel liegt; das Feld
+schlägt die gängigen Möglichkeiten vor, nimmt aber auch alles andere an.
+
+Was das bewirkt:
+
+- Auf der Detailseite steht die Sammlung **vor** der Streaming-Verfügbarkeit —
+ was du hast, ist die verlässlichere Antwort.
+- Poster in der Bibliothek bekommen ein Abzeichen, und statt des
+ Streamingdienstes steht dort deine Quelle.
+- In der Filterleiste gibt es einen eigenen Knopf **Hab ich** samt Zähler.
+- Export und Import führen die Angabe mit (Spalten `HabIch` und `Sammlung`),
+ ebenso die REST-API.
+
+Bewusst getrennt vom Feld „wo schaue ich das": Das eine sagt, wo ein Titel
+gerade läuft — das ändert sich ständig. Das andere sagt, was dir gehört, und
+das bleibt.
+
+---
+
## Die Bibliothek

diff --git a/docs/API.md b/docs/API.md
index 86b41df..4297673 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -57,8 +57,9 @@ GET /api/v1/bibliothek?status=WATCHING&typ=SERIES&genre=Drama&q=nord&limit=50&o
POST /api/v1/bibliothek
```
-Filter: `q`, `status`, `typ`, `genre`, `tag`, `dienst`, `limit`, `offset`.
-Status: `WATCHING`, `PAUSED`, `COMPLETED`, `DROPPED`, `WATCHLIST`.
+Filter: `q`, `status`, `typ`, `genre`, `tag`, `dienst`, `sammlung`, `limit`,
+`offset`. Status: `WATCHING`, `PAUSED`, `COMPLETED`, `DROPPED`, `WATCHLIST`.
+`sammlung=1` liefert nur Titel, die in der eigenen Sammlung liegen.
Aufnehmen — entweder mit interner Kennung oder direkt mit der TMDB-Kennung:
@@ -75,7 +76,9 @@ DELETE /api/v1/bibliothek/{entryId}
```
`GET` liefert den Eintrag samt `progress` und `nextEpisode`.
-`PATCH` ändert `status`, `rating` (1–10), `notes`, `whereToWatch`, `favorite`.
+`PATCH` ändert `status`, `rating` (1–10), `notes`, `whereToWatch`, `favorite`
+sowie `owned` (liegt der Titel in der eigenen Sammlung?) und `ownedSource`
+(wo genau — Jellyfin, Plex, Blu-ray …).
`DELETE` legt den Eintrag in den Papierkorb — dort bleibt er 30 Tage.
### Sichtungen
@@ -161,6 +164,14 @@ GET /api/v1/export?format=csv
JSON enthält zusätzlich alle Sichtungen und Universen.
+## Beispiel: was ich habe, aber noch nicht gesehen habe
+
+```bash
+curl -s -H "Authorization: Bearer $KEY" \
+ "$HOST/api/v1/bibliothek?sammlung=1&status=WATCHLIST" \
+| jq -r '.items[] | "\(.title) — liegt bei: \(.ownedSource // "ohne Angabe")"'
+```
+
## Beispiel: Serien mit offener Episode auflisten
```bash
diff --git a/docs/screenshots/20-sammlung.webp b/docs/screenshots/20-sammlung.webp
new file mode 100644
index 0000000..56b5aff
Binary files /dev/null and b/docs/screenshots/20-sammlung.webp differ
diff --git a/package.json b/package.json
index 5d4d2a9..e5c4a50 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "flimmer",
- "version": "0.6.1",
+ "version": "0.7.0",
"private": true,
"description": "Flimmer — selbst hostbarer Serien- und Film-Tracker",
"license": "MIT",
diff --git a/prisma/migrations/20260901190508_sammlung/migration.sql b/prisma/migrations/20260901190508_sammlung/migration.sql
new file mode 100644
index 0000000..b45dfb7
--- /dev/null
+++ b/prisma/migrations/20260901190508_sammlung/migration.sql
@@ -0,0 +1,6 @@
+-- AlterTable
+ALTER TABLE "LibraryEntry" ADD COLUMN "owned" BOOLEAN NOT NULL DEFAULT false,
+ADD COLUMN "ownedSource" TEXT;
+
+-- CreateIndex
+CREATE INDEX "LibraryEntry_userId_owned_idx" ON "LibraryEntry"("userId", "owned");
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index e6098e7..30b3d16 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -289,6 +289,13 @@ model LibraryEntry {
/// „Wo schaue ich das" — Vorschlag aus der Verfügbarkeit, überschreibbar
whereToWatch String?
+ /// Liegt der Titel in der eigenen Sammlung? Unabhängig davon, ob ihn gerade
+ /// ein Streamingdienst führt — gekaufte Filme laufen irgendwann nirgends mehr.
+ owned Boolean @default(false)
+
+ /// Wo genau: „Jellyfin", „Blu-ray", „NAS", „Plex" …
+ ownedSource String?
+
favorite Boolean @default(false)
/// Aktueller Durchlauf: 0 = Erstsichtung, 1 = erster Rewatch …
@@ -309,6 +316,7 @@ model LibraryEntry {
@@unique([userId, titleId])
@@index([userId, status])
@@index([userId, updatedAt])
+ @@index([userId, owned])
@@index([deletedAt])
}
diff --git a/prisma/seed.ts b/prisma/seed.ts
index c01caed..44a4b6c 100644
--- a/prisma/seed.ts
+++ b/prisma/seed.ts
@@ -244,6 +244,9 @@ type EntryPlan = {
startedDaysAgo?: number;
/** Abstand zwischen zwei Episoden in Tagen. */
spacingDays?: number;
+ /** Liegt der Titel in der eigenen Sammlung? */
+ owned?: boolean;
+ ownedSource?: string;
};
const ENTRY_PLANS: EntryPlan[] = [
@@ -315,8 +318,8 @@ const ENTRY_PLANS: EntryPlan[] = [
},
{ key: "zwischenstopp", status: "WATCHLIST", tags: ["vorgemerkt"] },
- { key: "sternenpfad-1", status: "COMPLETED", rating: 8, seen: true, tags: ["sternenpfad"], startedDaysAgo: 340 },
- { key: "sternenpfad-2", status: "COMPLETED", rating: 9, seen: true, favorite: true, tags: ["sternenpfad"], startedDaysAgo: 320 },
+ { key: "sternenpfad-1", status: "COMPLETED", rating: 8, seen: true, tags: ["sternenpfad"], startedDaysAgo: 340, owned: true, ownedSource: "Jellyfin" },
+ { key: "sternenpfad-2", status: "COMPLETED", rating: 9, seen: true, favorite: true, tags: ["sternenpfad"], startedDaysAgo: 320, owned: true, ownedSource: "Jellyfin" },
{ key: "sternenpfad-3", status: "COMPLETED", rating: 7, seen: true, tags: ["sternenpfad"], startedDaysAgo: 250 },
{ key: "sternenpfad-4", status: "WATCHLIST", tags: ["sternenpfad"] },
{ key: "sternenpfad-null", status: "WATCHLIST", tags: ["sternenpfad", "kino"] },
@@ -330,7 +333,7 @@ const ENTRY_PLANS: EntryPlan[] = [
startedDaysAgo: 180,
},
{ key: "stille-bilder-2", status: "WATCHLIST" },
- { key: "hochwasser", status: "COMPLETED", rating: 10, seen: true, favorite: true, startedDaysAgo: 60 },
+ { key: "hochwasser", status: "COMPLETED", rating: 10, seen: true, favorite: true, startedDaysAgo: 60, owned: true, ownedSource: "Blu-ray" },
{ key: "pfand", status: "DROPPED", rating: 4, notes: "Nach vierzig Minuten abgebrochen.", startedDaysAgo: 40 },
{ key: "die-vermessung", status: "COMPLETED", rating: 7, seen: true, startedDaysAgo: 400 },
];
@@ -355,6 +358,8 @@ async function seedLibrary(userId: string, titleIds: Map
+ + + In deiner Sammlung + {source ? · {source} : null} + +
+ ); +} + function Fact({ label, children }: { label: string; children: React.ReactNode }) { return ({item.type === "SERIES" ? "Serie" : "Film"} {item.year ? ` · ${item.year}` : ""} - {item.whereToWatch ? ` · ${item.whereToWatch}` : ""} + {item.owned && item.ownedSource + ? ` · ${item.ownedSource}` + : item.whereToWatch + ? ` · ${item.whereToWatch}` + : ""}
{showProgress && ( diff --git a/src/components/library/LibraryFilters.tsx b/src/components/library/LibraryFilters.tsx index 0b4ec18..6d60ff7 100644 --- a/src/components/library/LibraryFilters.tsx +++ b/src/components/library/LibraryFilters.tsx @@ -56,7 +56,9 @@ export function LibraryFilters({ facets }: { facets: LibraryFacets }) { const status = params.get("status"); const type = params.get("typ"); const sort = params.get("sortierung") ?? "aktualisiert"; - const activeCount = ["genre", "tag", "dienst", "typ"].filter((key) => params.get(key)).length; + const activeCount = ["genre", "tag", "dienst", "typ", "sammlung"].filter((key) => + params.get(key), + ).length; return (