Film: „Gesehen" lässt sich nicht mehr zurücknehmen #5

Closed
opened 2026-09-01 21:04:34 +00:00 by florian.wilde · 4 comments
Owner

Was passiert

Ist ein Film einmal als gesehen markiert, gibt es keinen Weg zurück. Der Statuswechsel auf Watchlist, Pausiert oder Abgebrochen ändert nichts — die grüne Plakette „✓ Gesehen" bleibt stehen, und der Fortschrittsbalken zeigt weiter 100 %.

Bei Serien geht es: einzelne Episoden und ganze Staffeln lassen sich abwählen. Nur beim Film ist es eine Einbahnstraße.

Warum

Drei Dinge greifen ineinander:

1. Der Knopf ist gar keiner mehr. In WatchMovieButton (src/components/library/NextEpisode.tsx:96) wird im Zustand „gesehen" ein <p> gerendert statt eines <button> — ein Etikett ohne Klickziel. Es gibt schlicht nichts anzuklicken.

2. Der Status kann das nicht heilen. „Gesehen" wird nicht aus dem Status abgeleitet, sondern aus den Sichtungen: movieWatched = (progress?.watched ?? 0) > 0 (src/app/titel/[id]/page.tsx:103). setStatus() rührt die Sichtungen nicht an (src/lib/library.ts:96) — zu Recht: eine Serie auf „Pausiert" zu setzen darf ja auch nicht die abgehakten Episoden löschen. Beim Film wirkt das aber wie ein kaputter Knopf, weil man genau das versucht.

3. Es fehlt die Gegenfunktion. Es gibt unmarkEpisodes() und unmarkSeason() (src/lib/library.ts:230 und :252), aber kein Gegenstück für Filme. undoWatches() gibt es zwar, das braucht aber die konkreten watchIds — und die hält nur die Einblendung direkt nach dem Klick. Ist sie weg oder die Seite neu geladen, sind sie unerreichbar.

Kurz: Serien haben einen Umschalter bekommen, Filme nur einen Hinweg.

Der Fallstrick beim Beheben

markMovieWatched() legt die Sichtung mit run: entry.currentRun an (src/lib/library.ts:318). Das Zurücknehmen muss genauso auf den aktuellen Durchlauf eingegrenzt sein. Ein deleteMany({ userId, entryId, episodeId: null }) ohne run würde bei einem Film, den man dreimal gesehen hat, alle drei Sichtungen löschen — und damit rückwirkend die Statistik verfälschen. Genau dafür ist run ja da.

Umzusetzen

  • unmarkMovie(userId, entryId) in src/lib/library.ts, eingegrenzt auf run: entry.currentRun und episodeId: null, danach reconcileStatus()
  • unmarkMovieAction in src/app/actions/library.ts, analog zu toggleEpisodeAction
  • WatchMovieButton zu einem echten Umschalter machen: der Zustand „gesehen" bleibt sichtbar grün, wird aber klickbar („Markierung entfernen") — so wie die Episodenhäkchen sich auch wieder abwählen lassen
  • Über DELETE /api/v1/bibliothek/{entryId}/sichtungen erreichbar machen, ohne dass man watchIds kennen muss (etwa {"action":"movie"} als Gegenstück zum POST) — samt docs/API.md und openapi.json

Nebenbei aufgefallen

Klickt man „Als gesehen markieren" bei einem schon markierten Film, antwortet markMovieWatched() mit watchIds: [] und der Meldung „War schon als gesehen markiert" (src/lib/library.ts:322). Die Einblendung bietet dann ein Zurücknehmen an, das nichts zurücknimmt. Sollte mit erledigt werden.

Abnahme

  • Ein als gesehen markierter Film lässt sich mit einem Klick wieder zurücksetzen
  • Danach steht der Fortschritt wieder auf 0 % und der Knopf bietet erneut „Als gesehen markieren"
  • Bei einem Film im zweiten Durchlauf entfernt das Zurücknehmen nur die Sichtung des laufenden Durchlaufs; die Statistik der früheren Durchläufe bleibt unverändert (Test)
  • Der Statuswechsel löscht weiterhin keine Sichtungen — weder bei Filmen noch bei Serien
  • Über die API ohne Kenntnis der watchIds möglich

Aufwand

Klein. Eine Funktion, eine Aktion, ein Knopf, ein Endpunkt — die Bausteine für Serien stehen schon und geben das Muster vor.


Angelegt auf ausdrücklichen Wunsch direkt in Gitea, ohne Verknüpfung zu einem Nebula-Task.

## Was passiert Ist ein Film einmal als gesehen markiert, gibt es keinen Weg zurück. Der Statuswechsel auf Watchlist, Pausiert oder Abgebrochen ändert nichts — die grüne Plakette „✓ Gesehen" bleibt stehen, und der Fortschrittsbalken zeigt weiter 100 %. Bei Serien geht es: einzelne Episoden und ganze Staffeln lassen sich abwählen. Nur beim Film ist es eine Einbahnstraße. ## Warum Drei Dinge greifen ineinander: **1. Der Knopf ist gar keiner mehr.** In `WatchMovieButton` (`src/components/library/NextEpisode.tsx:96`) wird im Zustand „gesehen" ein `<p>` gerendert statt eines `<button>` — ein Etikett ohne Klickziel. Es gibt schlicht nichts anzuklicken. **2. Der Status kann das nicht heilen.** „Gesehen" wird nicht aus dem Status abgeleitet, sondern aus den Sichtungen: `movieWatched = (progress?.watched ?? 0) > 0` (`src/app/titel/[id]/page.tsx:103`). `setStatus()` rührt die Sichtungen nicht an (`src/lib/library.ts:96`) — zu Recht: eine Serie auf „Pausiert" zu setzen darf ja auch nicht die abgehakten Episoden löschen. Beim Film wirkt das aber wie ein kaputter Knopf, weil man genau das versucht. **3. Es fehlt die Gegenfunktion.** Es gibt `unmarkEpisodes()` und `unmarkSeason()` (`src/lib/library.ts:230` und `:252`), aber kein Gegenstück für Filme. `undoWatches()` gibt es zwar, das braucht aber die konkreten `watchIds` — und die hält nur die Einblendung direkt nach dem Klick. Ist sie weg oder die Seite neu geladen, sind sie unerreichbar. Kurz: Serien haben einen Umschalter bekommen, Filme nur einen Hinweg. ## Der Fallstrick beim Beheben `markMovieWatched()` legt die Sichtung mit `run: entry.currentRun` an (`src/lib/library.ts:318`). Das Zurücknehmen muss **genauso auf den aktuellen Durchlauf eingegrenzt** sein. Ein `deleteMany({ userId, entryId, episodeId: null })` ohne `run` würde bei einem Film, den man dreimal gesehen hat, alle drei Sichtungen löschen — und damit rückwirkend die Statistik verfälschen. Genau dafür ist `run` ja da. ## Umzusetzen - `unmarkMovie(userId, entryId)` in `src/lib/library.ts`, eingegrenzt auf `run: entry.currentRun` und `episodeId: null`, danach `reconcileStatus()` - `unmarkMovieAction` in `src/app/actions/library.ts`, analog zu `toggleEpisodeAction` - `WatchMovieButton` zu einem echten Umschalter machen: der Zustand „gesehen" bleibt sichtbar grün, wird aber klickbar („Markierung entfernen") — so wie die Episodenhäkchen sich auch wieder abwählen lassen - Über `DELETE /api/v1/bibliothek/{entryId}/sichtungen` erreichbar machen, ohne dass man `watchIds` kennen muss (etwa `{"action":"movie"}` als Gegenstück zum POST) — samt `docs/API.md` und `openapi.json` ## Nebenbei aufgefallen Klickt man „Als gesehen markieren" bei einem schon markierten Film, antwortet `markMovieWatched()` mit `watchIds: []` und der Meldung „War schon als gesehen markiert" (`src/lib/library.ts:322`). Die Einblendung bietet dann ein Zurücknehmen an, das nichts zurücknimmt. Sollte mit erledigt werden. ## Abnahme - [ ] Ein als gesehen markierter Film lässt sich mit einem Klick wieder zurücksetzen - [ ] Danach steht der Fortschritt wieder auf 0 % und der Knopf bietet erneut „Als gesehen markieren" - [ ] Bei einem Film im zweiten Durchlauf entfernt das Zurücknehmen **nur** die Sichtung des laufenden Durchlaufs; die Statistik der früheren Durchläufe bleibt unverändert (Test) - [ ] Der Statuswechsel löscht weiterhin keine Sichtungen — weder bei Filmen noch bei Serien - [ ] Über die API ohne Kenntnis der `watchIds` möglich ## Aufwand Klein. Eine Funktion, eine Aktion, ein Knopf, ein Endpunkt — die Bausteine für Serien stehen schon und geben das Muster vor. --- *Angelegt auf ausdrücklichen Wunsch direkt in Gitea, ohne Verknüpfung zu einem Nebula-Task.*
florian.wilde added the in-arbeit label 2026-09-01 21:54:45 +00:00
florian.wilde started working 2026-09-01 21:54:53 +00:00
Author
Owner

🤖 Claude Code hat mit der Umsetzung begonnen.

  • Branch: fix/5-gesehen-zuruecknehmen
  • Verstanden als: Ein als gesehen markierter Film soll sich mit einem Klick wieder zurücksetzen lassen — so wie sich Episoden und Staffeln abwählen lassen —, und zwar nur die Sichtung des laufenden Durchlaufs, damit frühere Durchläufe in der Statistik stehen bleiben.
  • Geplantes Vorgehen:
    • Zuerst reproduzieren und den Durchlauf-Fallstrick mit einem Test festnageln, bevor ich etwas ändere
    • unmarkMovie() eingegrenzt auf run: entry.currentRun und episodeId: null, danach reconcileStatus()
    • WatchMovieButton zu einem echten Umschalter machen; den irreführenden „Zurücknehmen"-Hinweis bei einem schon markierten Film gleich mit erledigen
    • Über DELETE /api/v1/bibliothek/{entryId}/sichtungen mit {"action":"movie"} erreichbar machen, samt docs/API.md und openapi.json
    • Klickweg gegen einen Produktionsbau prüfen — im Entwicklungsmodus hydratisiert unter Playwright nichts, das hat mich bei #4 Zeit gekostet

Zeiterfassung läuft.

🤖 **Claude Code hat mit der Umsetzung begonnen.** - Branch: `fix/5-gesehen-zuruecknehmen` - Verstanden als: Ein als gesehen markierter Film soll sich mit einem Klick wieder zurücksetzen lassen — so wie sich Episoden und Staffeln abwählen lassen —, und zwar nur die Sichtung des laufenden Durchlaufs, damit frühere Durchläufe in der Statistik stehen bleiben. - Geplantes Vorgehen: - Zuerst reproduzieren und den Durchlauf-Fallstrick mit einem Test festnageln, bevor ich etwas ändere - `unmarkMovie()` eingegrenzt auf `run: entry.currentRun` und `episodeId: null`, danach `reconcileStatus()` - `WatchMovieButton` zu einem echten Umschalter machen; den irreführenden „Zurücknehmen"-Hinweis bei einem schon markierten Film gleich mit erledigen - Über `DELETE /api/v1/bibliothek/{entryId}/sichtungen` mit `{"action":"movie"}` erreichbar machen, samt `docs/API.md` und `openapi.json` - Klickweg gegen einen Produktionsbau prüfen — im Entwicklungsmodus hydratisiert unter Playwright nichts, das hat mich bei #4 Zeit gekostet Zeiterfassung läuft.
florian.wilde worked for 9 minutes 2026-09-01 22:04:33 +00:00
florian.wilde added zu-testen and removed in-arbeit labels 2026-09-01 22:04:59 +00:00
Author
Owner

✅ Umsetzung abgeschlossen — bereit zum Testen.

Was geändert wurde

  • unmarkMovie() als fehlendes Gegenstück zu unmarkEpisodes(), eingegrenzt auf run: entry.currentRun und episodeId: null, danach reconcileStatus()
  • unmarkMovieAction analog zu toggleEpisodeAction
  • Die grüne Plakette bleibt stehen, ist jetzt aber ein echter Knopf („— Markierung entfernen"), mit Hover-Zustand
  • DELETE /api/v1/bibliothek/{entryId}/sichtungen nimmt jetzt auch {"action":"movie"} — ohne Kenntnis der watchIds; docs/API.md und openapi.json nachgezogen

Geänderte Dateien

  • src/lib/library.ts
  • src/app/actions/library.ts
  • src/components/library/NextEpisode.tsx
  • src/app/api/v1/bibliothek/[entryId]/sichtungen/route.ts
  • src/app/api/v1/openapi.json/route.ts
  • docs/API.md
  • tests/library.test.ts

Der Nebenpunkt aus der Beschreibung trifft nicht zu

Ich hatte vermutet, die Einblendung biete bei einem schon markierten Film ein wirkungsloses Zurücknehmen an. Nachgesehen: report() (src/components/Toasts.tsx:59) blendet den Knopf nur bei ids.length > 0 ein. Das Angebot gab es also nie — hier war nichts zu reparieren.

Geprüft

59 Tests grün (drei neue), Typecheck sauber. Klickweg gegen einen Produktionsbau:

Schritt Gesehen-Knopf Markieren-Knopf Fortschritt
vorher 0 1 0 %
nach dem Markieren 1 0 100 %
nach dem Zurücknehmen 0 1 0 %
nach Neuladen 0 1 0 %

Über die Schnittstelle: POST {"action":"movie"} → 1 Sichtung, DELETE {"action":"movie"} → 200, 0 Sichtungen, 0 %.

So testest du es

  1. git switch fix/5-gesehen-zuruecknehmen
  2. npm run dev (Port 3011), einen Film öffnen und „Als gesehen markieren" klicken
  3. Erwartet: Die grüne Plakette „Gesehen" trägt jetzt den Zusatz „— Markierung entfernen" und lässt sich anklicken.
  4. Klicken → Fortschritt zurück auf 0 %, der Knopf bietet wieder „Als gesehen markieren". Seite neu laden: bleibt so.
  5. Rewatch-Probe: markieren, „Erneut schauen" starten, wieder markieren, dann zurücknehmen. Die Statistik muss die Sichtung des ersten Durchlaufs behalten.

Aufgewendete Zeit: 00:10
Pull Request: #9

Nach Freigabe: /issue-freigabe 5

✅ **Umsetzung abgeschlossen — bereit zum Testen.** <!-- claude-status branch: fix/5-gesehen-zuruecknehmen pr: 9 zeit_sekunden: 580 --> **Was geändert wurde** - `unmarkMovie()` als fehlendes Gegenstück zu `unmarkEpisodes()`, eingegrenzt auf `run: entry.currentRun` und `episodeId: null`, danach `reconcileStatus()` - `unmarkMovieAction` analog zu `toggleEpisodeAction` - Die grüne Plakette bleibt stehen, ist jetzt aber ein echter Knopf („— Markierung entfernen"), mit Hover-Zustand - `DELETE /api/v1/bibliothek/{entryId}/sichtungen` nimmt jetzt auch `{"action":"movie"}` — ohne Kenntnis der `watchIds`; `docs/API.md` und `openapi.json` nachgezogen **Geänderte Dateien** - `src/lib/library.ts` - `src/app/actions/library.ts` - `src/components/library/NextEpisode.tsx` - `src/app/api/v1/bibliothek/[entryId]/sichtungen/route.ts` - `src/app/api/v1/openapi.json/route.ts` - `docs/API.md` - `tests/library.test.ts` **Der Nebenpunkt aus der Beschreibung trifft nicht zu** Ich hatte vermutet, die Einblendung biete bei einem schon markierten Film ein wirkungsloses Zurücknehmen an. Nachgesehen: `report()` (`src/components/Toasts.tsx:59`) blendet den Knopf nur bei `ids.length > 0` ein. Das Angebot gab es also nie — hier war nichts zu reparieren. **Geprüft** 59 Tests grün (drei neue), Typecheck sauber. Klickweg gegen einen Produktionsbau: | Schritt | Gesehen-Knopf | Markieren-Knopf | Fortschritt | |---|---|---|---| | vorher | 0 | 1 | 0 % | | nach dem Markieren | 1 | 0 | 100 % | | nach dem Zurücknehmen | 0 | 1 | 0 % | | nach Neuladen | 0 | 1 | 0 % | Über die Schnittstelle: `POST {"action":"movie"}` → 1 Sichtung, `DELETE {"action":"movie"}` → 200, 0 Sichtungen, 0 %. **So testest du es** 1. `git switch fix/5-gesehen-zuruecknehmen` 2. `npm run dev` (Port 3011), einen Film öffnen und „Als gesehen markieren" klicken 3. Erwartet: Die grüne Plakette „Gesehen" trägt jetzt den Zusatz „— Markierung entfernen" und lässt sich anklicken. 4. Klicken → Fortschritt zurück auf 0 %, der Knopf bietet wieder „Als gesehen markieren". Seite neu laden: bleibt so. 5. Rewatch-Probe: markieren, „Erneut schauen" starten, wieder markieren, dann zurücknehmen. Die Statistik muss die Sichtung des **ersten** Durchlaufs behalten. **Aufgewendete Zeit:** 00:10 **Pull Request:** #9 Nach Freigabe: `/issue-freigabe 5`
Author
Owner

📝 Nebenbei aufgefallen — bewusst nicht in diesem PR erledigt:

Der Status bleibt beim Zurücknehmen stehen. Beim Prüfen fiel auf: nimmt man die Sichtung eines Films zurück, der dadurch auf „Abgeschlossen" gesprungen war, bleibt dieser Status stehen. Der Eintrag steht dann auf „Abgeschlossen" bei 0 %, während der Knopf wieder „Als gesehen markieren" anbietet. reconcileStatus() (src/lib/library.ts:372) behandelt den Fall watched === 0 nicht — es gibt nur Zweige für „alles gesehen" und „mehr als null gesehen".

Das ist kein neuer Fehler: bei Serien war der Zustand schon immer erreichbar, indem man alle Episoden abwählt. Durch den neuen Knopf wird er bei Filmen allerdings leicht erreichbar.

Ich habe es aus zwei Gründen nicht mitgeändert:

  1. Die Beschreibung dieses Issues begründet ausdrücklich, dass Status und Sichtungen getrennte Belange sind („setStatus() rührt die Sichtungen nicht an — zu Recht"). Den Status beim Zurücknehmen automatisch umzustellen, würde diese Trennung wieder aufweichen.
  2. reconcileStatus() gilt für Serien und Filme gleichermaßen. Eine Änderung dort wäre ein Verhaltenseingriff weit über dieses Issue hinaus, und es ist nicht offensichtlich, worauf zurückgefallen werden sollte — auf WATCHLIST, oder auf den Stand davor?

Der Datenbestand ist in jedem Fall unversehrt, und der Status lässt sich direkt darunter mit einem Klick setzen. Wäre aber eine eigene, gut umrissene Aufgabe fürs Dashboard.

📝 **Nebenbei aufgefallen** — bewusst nicht in diesem PR erledigt: **Der Status bleibt beim Zurücknehmen stehen.** Beim Prüfen fiel auf: nimmt man die Sichtung eines Films zurück, der dadurch auf „Abgeschlossen" gesprungen war, bleibt dieser Status stehen. Der Eintrag steht dann auf **„Abgeschlossen" bei 0 %**, während der Knopf wieder „Als gesehen markieren" anbietet. `reconcileStatus()` (`src/lib/library.ts:372`) behandelt den Fall `watched === 0` nicht — es gibt nur Zweige für „alles gesehen" und „mehr als null gesehen". Das ist **kein neuer Fehler**: bei Serien war der Zustand schon immer erreichbar, indem man alle Episoden abwählt. Durch den neuen Knopf wird er bei Filmen allerdings leicht erreichbar. Ich habe es aus zwei Gründen nicht mitgeändert: 1. Die Beschreibung dieses Issues begründet ausdrücklich, dass Status und Sichtungen **getrennte** Belange sind („`setStatus()` rührt die Sichtungen nicht an — zu Recht"). Den Status beim Zurücknehmen automatisch umzustellen, würde diese Trennung wieder aufweichen. 2. `reconcileStatus()` gilt für Serien und Filme gleichermaßen. Eine Änderung dort wäre ein Verhaltenseingriff weit über dieses Issue hinaus, und es ist nicht offensichtlich, worauf zurückgefallen werden sollte — auf `WATCHLIST`, oder auf den Stand davor? Der Datenbestand ist in jedem Fall unversehrt, und der Status lässt sich direkt darunter mit einem Klick setzen. Wäre aber eine eigene, gut umrissene Aufgabe fürs Dashboard.
Author
Owner

🚀 Freigegeben und nach main übernommen.

  • Merge-Commit: fd1ab29
  • Pull Request: #9 (gemergt)
  • Gesamtaufwand: 00:10

Vor dem Merge noch einmal geprüft: Typecheck sauber, 59 Tests grün.

Issue wird geschlossen.

🚀 **Freigegeben und nach `main` übernommen.** <!-- claude-status merge: fd1ab2918adcf32c72dc4aa18154c632bb674be2 pr: 9 zeit_sekunden: 580 --> - Merge-Commit: `fd1ab29` - Pull Request: #9 (gemergt) - Gesamtaufwand: 00:10 Vor dem Merge noch einmal geprüft: Typecheck sauber, 59 Tests grün. Issue wird geschlossen.
florian.wilde removed the zu-testen label 2026-09-01 22:07:08 +00:00
Sign in to join this conversation.
1 Participants
Notifications
Total Time Spent: 9 minutes
florian.wilde
9 minutes
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: florian.wilde/flimmer#5