Ein Universum kann mehrere Reihenfolgen haben, aber welche beim Oeffnen
erschien, liess sich nicht bestimmen — es war immer die
Erscheinungsreihenfolge. Das Feld isDefault gab es schon und wurde beim
Oeffnen auch ausgewertet; es fehlte nur der Weg, es zu setzen.
Der Standard gehoert dem Universum, nicht der einzelnen Person: wer
Schreibrecht hat, legt ihn fuer alle fest — so wie die Reihenfolgen selbst
auch geteilt sind. Die Rangfolge bleibt dadurch die schlichte: die Adresse
schlaegt den Standard schlaegt die erste.
setDefaultOrder() setzt zurueck und neu in einer Transaktion, damit nie zwei
Standards nebeneinander stehen. deleteOrder() faengt jetzt den Fall ab, dass
die Standardreihenfolge geloescht wird — dann uebernimmt die
Erscheinungsreihenfolge, statt dass die Auswahl auf "die erste" faellt.
Ueber die Schnittstelle geht es per PATCH /api/v1/universen/{id}. Der
Endpunkt prueft, dass die Reihenfolge zu genau diesem Universum gehoert:
ohne das setzte ein PATCH auf A den Standard in B, weil die Domaenenschicht
das Universum aus der Reihenfolge ableitet.
Vier Tests dazu: Umstellen samt Eindeutigkeit, Vorrang der Adresse,
Rueckfall beim Loeschen, Rechtepruefung. Klickweg und Endpunkt gegen einen
Produktionsbau geprueft.
Refs #4
Der Titelblock hatte mit line-clamp-2 zwar eine Ober-, aber keine
Untergrenze. Einzeilige Titel belegten dadurch 19 statt 39 Pixel, und alles
darunter rutschte um eine Zeilenhoehe nach oben — neben Karten mit
zweizeiligem Titel wirkte das Raster ausgefranst.
Der Titelblock bekommt jetzt mit min-h-[2lh] immer zwei Zeilen. Die Einheit
lh haengt an der tatsaechlichen Zeilenhoehe, damit der Wert nicht von einer
Aenderung an leading-snug abgehaengt wird.
In der Bibliothekskarte kam hinzu, dass der Fortschrittsbalken nur bei
Serien erscheint. Sein Platz wird jetzt auch dann freigehalten, wenn er
leer bleibt — als unsichtbarer Nachbau statt einer festen Pixelhoehe, damit
er sich von selbst nachzieht.
Nachgemessen bei 1400 px: Metazeilen einer Reihe liegen jetzt alle auf
derselben Oberkante (Suche 711 px, Bibliothek 715/1117/1520 px), und alle
Karten sind gleich hoch (353 bzw. 387 px). Vorher: 691 gegen 711 px und
Kartenhoehen von 333 bis 367 px. Bei Handybreite ebenfalls sauber.
Refs #2
Streaming-Verfügbarkeit beantwortet nur die halbe Frage: manche Filme laufen
nirgends mehr, die hat man gekauft und liegen zu Hause — in Jellyfin, Plex,
Emby, auf dem NAS oder als Scheibe.
Bewusst getrennt vom Feld „wo schaue ich das": das eine sagt, wo ein Titel
gerade läuft, und das ändert sich ständig; das andere sagt, was einem gehört,
und das bleibt. Auf der Detailseite steht die Sammlung deshalb vor der
Verfügbarkeit — was man hat, ist die verlässlichere Antwort.
Durchgezogen bis in Filter, Export, CSV-Import und REST-API.
next/font/google laedt Outfit beim Bauen von Google — das macht den Build von
einer Netzverbindung abhaengig und liess ihn im Container haengen. Die
variable Fassung liegt jetzt als Datei im Repository; weder beim Bauen noch
zur Laufzeit geht eine Anfrage nach aussen.
Ausserdem: npm blockiert seit Version 11 Install-Skripte. Fuer prisma und
@prisma/engines sind sie jetzt gezielt erlaubt, damit im Image die
plattformpassende Engine bereitliegt.
Drei Strategien: Poster und statische Dateien zuerst aus dem Zwischenspeicher,
Seiten zuerst aus dem Netz, API-Aufrufe gar nicht.
Ein Tracker, der veraltete Stände zeigt, wäre schlimmer als einer, der ehrlich
sagt: gerade keine Verbindung. Schreibende Anfragen laufen nie über den
Service Worker.
Nach einem Passwortwechsel fliegen alle anderen Geräte raus; ein gesperrtes
Konto wird sofort abgemeldet. Die letzte Administration lässt sich nicht
entmachten, und sich selbst kann niemand sperren.
Das öffentliche Profil zeigt Zahlen und zuletzt Gesehenes — keine Notizen,
keine Watchlist, keine E-Mail-Adresse. Was privat gemeint ist, bleibt privat.
Es braucht die Freigabe des Nutzers und die des Betreibers.
Dieselben Endpunkte bedienen Skripte und das eigene Frontend: erst wird ein
Bearer-Token geprüft, sonst das Sitzungscookie.
Sichtungsaktionen geben immer watchIds zurück — damit lässt sich genau diese
Aktion über DELETE zurücknehmen, ohne raten zu müssen, was dazugehörte.
Die OpenAPI-Beschreibung ist handgeschrieben statt aus Zod erzeugt: die
Schnittstelle ist überschaubar, und ein Generator brächte eine weitere
Abhängigkeit für einen Nutzen, den ein Dutzend Zeilen JSON auch hergeben.
Frischt laufende Serien auf, meldet neue Episoden und neu verfügbare
Watchlist-Titel, räumt Papierkorb und abgelaufene Sitzungen.
Grundsatz: Der Lauf darf nie an einem einzelnen Titel scheitern. Fehler werden
gezählt und protokolliert, der Lauf macht weiter.
Neue Episoden werden nur gemeldet, wenn sie zeitlich nicht in ferner
Vergangenheit liegen — sonst löste der erste Abgleich einer alten Serie hundert
Meldungen aus.
Der Zeitplan ist ein einfacher Timer statt einer Cron-Bibliothek: Flimmer
braucht genau einen täglichen Lauf. Wer mehr will, stösst /api/cron/sync von
aussen an.
Der Kalender ist als Tabelle ausgezeichnet — ein Kalender ist eine Tabelle,
und mit Screenreader wird daraus dann „Woche 3, Donnerstag, 24." statt einer
ununterscheidbaren Kachelwüste.
Umsortieren geht per Ziehen und über Pfeilknöpfe: reines Drag & Drop wäre mit
Tastatur nicht bedienbar und auf dem Handy fummelig — die Knöpfe sind der
verlässliche Weg, das Ziehen der bequeme.
Die Erscheinungsreihenfolge lässt sich gar nicht erst anfassen, weil sie sich
aus den Daten ergibt.
Die Detailseite hängt jetzt an der internen Titel-ID statt an der TMDB-ID —
nur so bekommen manuell angelegte Titel dieselbe Seite. Treffer aus der Suche
laufen über /tmdb/<art>/<id>, das den Titel holt und weiterleitet.
Der Fortschritt einer ganzen Liste wird in einer Abfrage gezählt statt je
Eintrag; bei 300 Titeln wären das sonst 300 Rundreisen zur Datenbank.
Entfernte Titel wandern für 30 Tage in den Papierkorb und behalten ihren
Fortschritt.
Server-Aktionen prüfen die Sitzung jeweils selbst — auf Angaben aus dem
Formular ist beim Zugriffsschutz kein Verlass.
Häkchen reagieren sofort und werden zurückgedreht, wenn der Server
widerspricht: Abhaken soll sich anfühlen wie ein Schalter, nicht wie ein
Formular. Jede Sichtungsaktion meldet sich unten kurz zurück und lässt sich
von dort zurücknehmen — die Wirkung passiert weit weg vom Klick.
Die Suchanfrage steht in der URL: Treffer sind teilbar, der Zurück-Knopf
funktioniert, und die Suche selbst bleibt eine Server-Komponente.
Posterkacheln zeigen bei fehlendem Bild die Bildmarke über dem
Signaturverlauf. Die Rückfalldarstellung liegt dauerhaft unter dem Bild —
ein onError-Handler hilft nicht, wenn das Bild schon vor der Hydration
scheitert, und der Browser zeigt dann sein kaputtes Bildsymbol.
Der Katalog ist bewusst gemeinsam für alle Nutzer und über (Typ, TMDB-ID)
dedupliziert: zehn Leute mit derselben Serie brauchen die Episodenliste nur
einmal. Persönliches steckt ausschließlich in LibraryEntry und Watch.
Poster laufen über /api/image statt direkt zu TMDB — einmal holen, danach
vom eigenen Server ausliefern. Größe und Pfad werden streng geprüft, damit
daraus kein offener Proxy wird.
Staffel 0 (Specials) wird importiert, zählt aber nicht zum Fortschritt;
Episoden ohne eigene Laufzeit fallen auf die durchschnittliche Episodenlänge
der Serie zurück.
/api/health prüft die Datenbankverbindung mit und antwortet bei Ausfall mit
503 — eine App ohne Datenbank ist nicht gesund, auch wenn der Prozess läuft.
Layout mit Inline-Skript, das data-theme setzt, bevor der erste Pixel
gemalt wird — sonst blitzt beim Laden das falsche Farbschema auf. Logo als
React-Komponente statt als Bilddatei: nimmt Theme-Farben an, spart einen
Netzwerkabruf und die Verläufe liegen einmalig im Root-Layout.
Im hellen Modus greifen eigene Tokens für Verlaufstext und Wortmarke; der
vollfarbige Signaturverlauf fällt auf warmem Weiß sonst unter WCAG AA.
Passwörter mit bcrypt, Sitzungen als opake Token in der Datenbank — im
Cookie steht das Klartext-Token, in der Tabelle nur dessen SHA-256. Damit
lassen sich einzelne Geräte gezielt abmelden, was reine JWT-Sitzungen nicht
hergeben.
Der erste registrierte Zugang wird automatisch Administrator, sonst käme
man nach dem Aufsetzen nicht in die Verwaltung. Die Registrierung ist
instanzweit abschaltbar; solange es keinen Nutzer gibt, bleibt sie offen.
Enthält außerdem API-Schlüssel je Nutzer als Grundlage für die spätere
REST-API und die Instanz-Einstellungen in der Tabelle AppSetting.
Logo als SVG in drei Varianten (Icon, Icon+Wortmarke horizontal, monochrom);
die Wortmarke ist als Pfad konstruiert und braucht daher keine installierte
Schrift. Daraus generiert scripts/generate-icons.ts Favicon (SVG + ICO),
PWA-Icons, Maskable-Icons, Apple-Touch-Icon und OG-Bild.
Die Farbwelt liegt als CSS-Variablen in globals.css und wird ueber
@theme inline an Tailwind durchgereicht — Dark/Light und spaetere Themes
bauen darauf auf. Alle Kombinationen erfuellen WCAG AA.