Commit Graph
159 Commits
Author SHA1 Message Date
ScarriffleandClaude Opus 5 ce38da1376 Die Auswahlliste klappte hinter dem Dialog auf
Ein `<dialog>` mit `showModal()` liegt in der Top-Layer des Browsers,
über allem — unabhängig von `z-index`. Die Liste hing am `body` und
verschwand deshalb hinter genau dem Dialog, in dem ihr Knopf steht.
Jetzt wird sie in den Dialog gehängt, wenn es einen gibt. Mit
`elementFromPoint` nachgeprüft: an ihrer Stelle liegt wirklich ein
Listeneintrag obenauf.

Dazu zwei Stellen, die zu der Vermutung geführt haben, die
Spracheinstellung wirke nicht:

„Übersetzungen holen" stand stur auf en-US. Neben einem Server, der auf
Deutsch steht, liest sich das wie eine Aussage über den Server.
Vorgewählt wird jetzt die eigene Sprache, sonst die erste, die nicht
schon die des Servers ist — und der Text nennt die Serversprache.

Und „Meine Sprache" sagt jetzt deutlich, wenn sie abweicht. Sie ändert
nicht, was der Server holt, nur was man selbst sieht: steht sie auf
Englisch, während der Server auf Deutsch holt, sieht die ganze
Mediathek englisch aus und jedes Auffrischen scheint wirkungslos.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 20:03:01 +02:00
ScarriffleandClaude Opus 5 e87d50e470 Wer die Genreliste durchblättern wollte, schloss sie dabei
Der Zuhörer hängt mit `capture` am Fenster, damit auch ein Bildlauf in
einem inneren Behälter die Liste schliesst — sie steht `position: fixed`
und bliebe sonst neben einem Knopf stehen, der längst weggescrollt ist.
Nur scrollt eine Liste mit zwanzig Genres selbst, und dieses Ereignis
kam dort ebenfalls an. Jetzt wird geprüft, woher es stammt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 19:22:42 +02:00
ScarriffleandClaude Opus 5 4b0323f955 Drei Auswahlfelder liessen sich gleichzeitig aufklappen
Jedes merkte sich nur seine eigene Liste. Nebeneinander geöffnet lagen
sie übereinander, und keine wusste von den anderen. Ein Auswahlfeld ist
ein Menü, und von denen ist höchstens eines offen — jetzt modulweit
vermerkt, genau wie beim Kontextmenü. Escape schliesst zusätzlich von
überall, nicht nur mit dem Fokus in der Liste.

Dazu stand unter jeder Mediathek „0 Titel freigeben". Der Knopf wurde
über `hidden` versteckt — das Attribut wirkt über `display: none` mit der
Spezifität von Null, und `.btn` setzt `display: inline-flex`. Die Klasse
`.hidden` trägt ein `!important` und setzt sich durch.

Und das Favicon kommt jetzt aus `icon.png`, dem gerenderten App-Icon
selbst. Meine nachgezeichnete Fassung war falsch: die Grafik füllt die
Fläche, statt klein in der Mitte zu sitzen. Ein Icon, das dem echten nur
ähnelt, driftet beim nächsten Entwurf wieder auseinander.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 19:01:58 +02:00
ScarriffleandClaude Opus 5 8809b9f46f Die Mediathek hörte bei I auf, und jede Folge stand doppelt
Zwei Befunde aus dem laufenden Betrieb.

Die Liste holte genau eine Seite. Der Server gibt höchstens 200 Einträge
je Zug — das ist richtig so —, nur fragte die Oberfläche nie nach der
zweiten. Bei 455 Filmen endete die Mediathek mitten im Alphabet, ohne
Hinweis, dass da noch etwas kommt. Jetzt wird beim Scrollen nachgeladen,
und die Zahl sagt „200 von 455" statt einer Behauptung, die man beim
Scrollen widerlegt.

Dabei kam heraus, dass ein IntersectionObserver eine Seite braucht, die
auch gezeichnet wird: im Fenster im Hintergrund löst er nicht aus, und
beim Prüfen blieb die Liste genau deshalb bei 200 stehen. Daneben steht
jetzt eine Prüfung am Scrollereignis, die überall greift.

Der zweite Befund: jede Folge zeigte zwei gleich aussehende Knopfreihen.
Es sind zwei Dateien — Plex' „Optimized for Mobile"-Kopien. Flaro kannte
„optimized", aber nicht „Optimized for Mobile", und sah ohnehin nur den
unmittelbaren Elternordner; Plex legt die Fassung aber als
`Plex Versions/Optimized for Mobile/<Serie>/<Staffel>/…` ab. Jetzt wird
drei Ebenen hoch gesucht, die Schublade `Plex Versions` benennt keine
Serie mehr, und an der Folge steht „Mobil" statt einer zweiten,
unerklärten Reihe.

Nicht um Plex zu bedienen — die Dateien liegen nun einmal so auf der
Platte.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 18:45:07 +02:00
ScarriffleandClaude Opus 5 960cca3dae Der Abschnitt verschwand alle drei Sekunden und kam wieder
`laden()` setzte `innerHTML = ''` und baute die Mediathekenliste neu —
während eines Scans alle drei Sekunden. Der ganze Abschnitt flackerte
sichtbar, und wer gerade auf einen Knopf zielte, klickte ins Leere.
Schlimmer: jeder Neuaufbau startete über `verfolgen(false)` einen
weiteren Takt. Nach einer Minute liefen zwanzig davon nebeneinander.

Jetzt wird fortgeschrieben statt neu gezeichnet: eine Zeile je
Mediathek, die bleibt, und nur Kopfzeile, Prüfungsknopf und
Freigabeknopf ändern sich. Belegt im Browser — nach einem Auffrischen
sind es dieselben Knoten, nur die Beschriftung ist eine andere.

Dieselbe Behandlung für die Seitenleiste. Und weil Seitenleiste und
Verwaltung getrennt nachfragten, zeigten sie zwei Momente — 95 hier, 84
dort, während im Hintergrund Doppelte zusammengelegt wurden. Die
Verwaltung reicht ihren Stand jetzt weiter: ein Abruf, zwei Anzeigen.

Dazu zwei Dinge, die beim Prüfen auffielen. Zurücksetzen und Entfernen
prallen jetzt serverseitig mit 409 ab, solange ein Lauf läuft — die
Oberfläche sperrt die Knöpfe, aber darauf darf sich der Server nicht
verlassen; ein Aufruf von Hand während eines Scans liess den Lauf an
einer DbUpdateConcurrencyException sterben. Und in der
Fortschrittszeile steht beim Holen der Folgenmetadaten jetzt der
Serienname: „Folge 2×13" allein sagt bei 3815 Folgen nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 18:21:43 +02:00
ScarriffleandClaude Opus 5 a6e9a6e425 Ein Klick auf einen Vorschlag sah aus wie gar kein Klick
Hinter der Auswahl steckt ein Abruf bei TMDB, der Titel, Besetzung und
Bilder nachlädt — mehrere Sekunden, in denen die Oberfläche aussah wie
vorher. Wer nichts sieht, klickt noch einmal, und dann laufen zwei.
Jetzt tritt die Liste zurück, die gewählte Zeile bekommt einen
Akzentrahmen und daneben steht „Wird zugeordnet…" mit laufendem Kreis.

Dazu das Plakat je Vorschlag. „Dark" von 2017 und „Dark Shadows" von
1966 sind an einer Textzeile kaum zu unterscheiden, am Bild in einer
Zehntelsekunde — und der Server lieferte die Adresse ohnehin schon mit,
sie wurde nur weggeworfen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:58:06 +02:00
ScarriffleandClaude Opus 5 cfb1e71762 3559 von 5884 Dateien verschwanden lautlos
Der Scan erfindet keine Serie mehr, wenn weder Ordner noch Dateiname
eine nennen — richtig so. Nur wurde die Datei dann übersprungen, ohne
Zeile, ohne Spur. In der Mediathek standen 58 Serien und niemand konnte
sagen, wo die anderen geblieben sind.

Jetzt bekommt jede solche Datei eine Zeile ohne Zuordnung. Damit ist sie
zählbar, auflistbar und nach einer besseren Regel nachträglich
zuzuordnen. Untersucht wird sie nicht: ffprobe auf 3559 Dateien kostet
eine halbe Stunde für Angaben, die niemand zu sehen bekommt, und sobald
die Zuordnung steht, holt der nächste Lauf es nach.

Dazu ein Abschnitt "Nicht zugeordnet" in der Verwaltung, nach Grund
gebündelt und mit den Pfaden zum Aufklappen. Zwei Gründe gibt es, und
sie verlangen Verschiedenes: fehlt die Folgennummer, muss umbenannt
werden; fehlt der Serienname, genügt ein Ordner darum herum. Die Liste
lässt sich als Datei herunterladen — eine, die man markieren und
kopieren muss, schickt niemand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:23:36 +02:00
ScarriffleandClaude Opus 5 a8bb98c44c Umsortiert wird gezogen, nicht geklickt
Die Pfeile sind weg; eine Mediathek wird in der Seitenleiste an ihren
Platz gezogen. Mit Pointer Events statt HTML5-Drag: letzteres kennt kein
Telefon, zeichnet ein Geisterbild, das sich nicht gestalten lässt, und
liefert während des Ziehens keine brauchbaren Koordinaten.

Mit Schwelle von sechs Bildpunkten — sonst verschiebt jeder leicht
wackelige Klick die Reihenfolge, und ein Klick ohne Bewegung führt
weiterhin zur Mediathek, auch im Bearbeitungsmodus. Die weichenden
Zeilen gleiten, die gezogene folgt dem Zeiger ohne Verzögerung; bei
„Bewegung reduzieren" gleitet nichts.

Die Pfeiltasten bleiben auf der fokussierten Zeile, nur ohne sichtbare
Knöpfe. Ziehen ist mit der Tastatur nicht bedienbar, und eine Funktion,
die nur mit der Maus erreichbar ist, ist für den, der keine benutzt, gar
keine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 17:09:28 +02:00
ScarriffleandClaude Opus 5 a22fa9b58e Drei Zähler für dieselbe Sache, und keiner davon offensichtlich falsch
Während eines Scans stand in der Mediathek 20, in der Verwaltung 14 und
in der Seitenleiste 0. Die Verwaltung frischte alle fünf Runden auf, die
Seitenleiste erst am Ende des Laufs. Jetzt laufen beide auf demselben
Takt, alle drei Runden.

Dazu: während ein Lauf läuft, sind Scannen, Metadaten auffrischen,
Zurücksetzen und Entfernen gesperrt — die ersten beiden prallen am
Server ab und sind nur verwirrend, die anderen beiden löschen Zeilen,
während der Lauf welche schreibt. Umbenennen und Prüfung bleiben
bedienbar. Die Sperre richtet sich nach dem Zustand, den der Server
meldet, nicht nach dem Klick: wer die Seite neu lädt, während ein Lauf
läuft, findet sie ebenfalls gesperrt vor.

Die Reihenfolge der Mediatheken wird jetzt in der Seitenleiste
umsortiert, über „Seitenleiste bearbeiten" neben der Überschrift. Sie
wirkt dort, also gehört der Griff dorthin — vorher schob man in den
Einstellungen und musste danach nachsehen, ob es das war, was man
wollte.

Der Zuordnungsdialog zeigt die Herkunft: Ordner, Zahl der Dateien und
ein paar Namen. Liegt eine Zuordnung völlig daneben, sagt der angezeigte
Titel nichts mehr darüber, welche Datei gemeint ist.

Und SQLite läuft jetzt im WAL-Modus. In der Vorgabe sperrt ein Schreiber
die ganze Datei: solange der Scan schrieb, kam keine Leseanfrage durch.
Genau so sah es aus — Bilddialog und Protokoll liefen während eines
Laufs sporadisch ins Leere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 16:31:50 +02:00
ScarriffleandClaude Opus 5 ac04901ccc Neun gemeldete Fehler, und die Frage, welcher Build gerade läuft
Ein grosser Commit, weil die Punkte einander bedingen: ohne lesbare
Fehlermeldung war die Serienerkennung nicht zu diagnostizieren, und ohne
Build-Kennung nicht einmal festzustellen, ob überhaupt der gebaute Stand
läuft. Zwei der neun Meldungen betrafen Funktionen, die seit Stunden im
Quelltext stehen.

Version und Build. Die Version steht fest; daneben eine Kennung aus
sechs Zeichen, die sich bei jedem Docker-Bau ändert. Sie steht in der
Verwaltung, unter /health und im Protokoll bei jedem Start. Damit ist
"reden wir vom selben Build?" in zwei Sekunden beantwortet statt in
einem halben Tag.

Serien. Lag eine Datei direkt im Serienordner und trug vor der
Folgennummer nichts, wurde der ganze Dateiname zum Serientitel: aus 95
Serien wurden 737, jede mit einer Folge, bei jedem Lauf neu. Dazu lief
der Scan in Verweise hinein — derselbe Ordner unter zwei Namen ergab
zwei Serien. Und weder neue noch umgezogene Dateien standen in der
Pfadliste des Laufs, weshalb ein doppelt gesehener Pfad eine zweite
Zeile mit gleichem (LibraryId, Path) anlegte und der
Eindeutigkeitsindex den ganzen Speichervorgang scheitern liess. Jede
Serienanlage wird jetzt im Tagesprotokoll begründet.

Fehlermeldungen. "An error occurred while saving the entity changes"
stand englisch in einer deutschen Oberfläche und nannte weder Ursache
noch nächsten Schritt. Jetzt geht der vollständige Text ins Protokoll
und ein deutscher Satz ohne Fehlercode in die Anzeige — auch für
"Failed to fetch", das aus dem Browser kam und nie übersetzt wurde.

Protokolle. Die Ansicht holte die ganze Datei und zeigte 800 Zeilen
davon; bei zwei Megabyte brach die Verbindung ab. Gelesen wird jetzt
serverseitig rückwärts, mit einem Knopf für mehr.

Auswahlfelder. Die drei <select> zeichnete das Betriebssystem: ein
helles Systemmenü mitten in der dunklen Oberfläche. Ersetzt durch ein
eigenes Listenfeld mit Tastaturbedienung.

Bildwahl. Drei Raster untereinander in einem scrollenden Dialog hiess
zwei Scrollebenen übereinander. Jetzt ein Reiter je Bildtyp.

Sprache. Die Serversprache stand in der Serverliste zwischen Name,
Metadaten und Fassung — gesucht und nicht gefunden. Sie hat jetzt einen
eigenen Abschnitt ganz oben.

Dazu: ein Knopf, der den erfassten Inhalt einer Mediathek verwirft, ohne
Name und Pfad mitzunehmen; ein Abschnitt Speicher mit den drei Pfaden,
ihrer Belegung und dem Leeren des Bildspeichers; und die Korrektur einer
Zuordnung frischt nur noch die Kachel auf statt die ganze Seite.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 15:55:56 +02:00
ScarriffleandClaude Opus 5 fe44d45323 Verschobene Dateien hinterliessen eine Leiche
Seit verschwundene Dateien markiert statt gelöscht werden, blieb nach jeder
Umbenennung die alte Zeile als "vermisst" stehen. Damit galt die alte Folge
nicht als verwaist, die alte Serie nicht als leer — und nach dem Umbenennen
eines Serienordners stand alles doppelt da.

Jetzt erkennt der Scan eine Verschiebung an Grösse und Änderungszeit und
hängt den vorhandenen Eintrag um, statt einen zweiten anzulegen.

Der Zeitstempel musste dafür auf Sekunden gerundet werden: er geht durch
die Datenbank und kommt nicht auf den Tick genau zurück. Mit Vergleich auf
Gleichheit schlug die Erkennung ausnahmslos fehl — der Test zeigte es,
nachdem er zweimal dasselbe falsche Ergebnis lieferte.

Grösse und Zeit müssen beide stimmen, und das Merkmal muss eindeutig sein.
Grösse allein reichte nicht: zwei gleich grosse Dateien gibt es, und die zu
vertauschen wäre schlimmer als eine Leiche.

Dazu: die Zahl der Titel läuft während eines Scans mit, statt erst am Ende
zu springen. Alle fünf Runden, nicht jede — ein Abruf je Sekunde für eine
Zahl wäre Verschwendung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:34:49 +02:00
ScarriffleandClaude Opus 5 b64497acd5 Nicht jede erfolgreiche Antwort hat einen Körper
Bei jedem Scanstart stand in der Verwaltung "JSON.parse: unexpected end of
data" — eine Fehlermeldung für etwas, das tadellos funktioniert hatte.

Gemessen: der Scan-Endpunkt antwortet mit 202 Accepted, Content-Length 0
und ohne Content-Type. Er läuft im Hintergrund weiter, da gibt es noch
nichts zu melden. Der Abruf-Helfer behandelte aber nur 204 als leer und
liess JSON.parse auf eine leere Zeichenkette los.

Jetzt entscheidet der Inhalt, nicht der Status. Eine Liste von
Status-Codes wäre wieder eine, die beim nächsten Endpunkt unvollständig
ist.

Dazu: die Zeile "Metadaten" nannte die Sprache ein zweites Mal, obwohl sie
seit kurzem eine eigene Zeile hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:22:47 +02:00
ScarriffleandClaude Opus 5 98565e14b5 Der Ordner bestimmt die Serie, nicht der Dateiname
Aus einer Serie wurden siebenhundert. Dateien wie "S01E001_Aang.mkv"
tragen vor der Folgennummer nichts, also wurde der ganze Dateiname zum
Serientitel — jede Folge eine eigene Serie. Und umgekehrt ergab
"Doctor Who (2005) - S12E04.mkv" im Ordner "Doctor Who" zwei Serien, weil
die Jahresangabe nur im Dateinamen stand.

Jetzt gilt: was unter Serien/Pingu/ liegt, gehört zu Pingu. Egal wie tief,
egal wie die Datei heisst. Der Ordner ist die verlässlichere Angabe, weil
ihn ein Mensch angelegt hat; so macht es Plex seit je und aus demselben
Grund. Staffel und Folge liest weiterhin der Dateiname.

Ohne bekannte Wurzel bleibt es beim Dateinamen — dort könnte über dem
Staffelordner irgendetwas stehen. Diesen Weg gehen nur die Tests.

Dazu vier Dinge aus der Benutzung:

Die Mediatheken standen scheinbar in zufälliger Reihenfolge. SQLite
vergleicht Text zeichengenau, und dort stehen alle Grossbuchstaben vor
allen kleinen: "Serien" landete über "film". Jetzt ohne Rücksicht auf
Gross- und Kleinschreibung — und umsortierbar über zwei Pfeile. Nicht über
Ziehen: das ist schöner, aber mit der Tastatur unbedienbar und auf dem
Telefon ein Glücksspiel.

Die Sprache des Servers steckte im Dialog "Metadaten", zusammen mit dem
TMDB-Schlüssel — also dort, wo niemand nach einer Sprache sucht. Wer die
Mediathek auf Englisch vorfand, sah nur "Meine Sprache" und hielt das für
die einzige Möglichkeit. Jetzt eine eigene Zeile.

Das Kontextmenü gab es nur per Rechtsklick, und den findet nur, wer ihn
vermutet — auf dem Telefon gibt es ihn gar nicht. Jetzt drei Punkte auf
jeder Kachel, beim Überfahren und bei Tastaturfokus, auf Geräten ohne
Zeiger dauerhaft. Und "Zuordnung korrigieren" steht endlich darin: wer
zwanzig falsch erkannte Titel vor sich hat, soll sie dort geradeziehen
können statt über vierzig Seitenwechsel.

Folgen ohne eigenes Bild nehmen das der Serie. TMDB liefert Folgenbilder
nur für einen Teil der Folgen und bei kleineren Serien für gar keine; eine
graue Fläche sagt nichts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 14:20:00 +02:00
ScarriffleandClaude Opus 5 fa18c0d7fa Zwei Namen für denselben Titel brachen den ganzen Lauf ab
Der Eindeutigkeitsindex aus Punkt 3 stellte die Regel auf, dichtete aber
den einen Weg nicht ab, auf dem sie verletzt wird: der Enricher setzt die
TMDB-Kennung, und sobald zwei Einträge derselben Mediathek dieselbe
bekamen, brach EF die gesamte Speicherung ab — mit "An error occurred
while saving the entity changes", einer Meldung, die den Grund nicht nennt
und den ganzen Scan mitnimmt.

Jetzt wird die Kennung nicht blind gesetzt, sondern vorgemerkt und nach
der Schleife zusammengelegt. Zwei Tests halten den Fall fest: der Anbieter
antwortet auf jede Anfrage mit demselben Film beziehungsweise derselben
Serie, also genau die Lage, die vorher den Lauf zerriss.

Dazu der wahrscheinliche Grund für die siebenhundert Serien: der
Serientitel kam ausschliesslich aus dem Dateinamen. Heisst eine Datei nur
"S01E01.mkv", steht vor der Folgennummer nichts, und der ganze Dateiname
wurde zum Serientitel — zwanzig Folgen ergaben zwanzig Serien namens
"S01E01", "S01E02", und so weiter. Jetzt sagt der Ordner den Namen, wenn
die Datei ihn nicht nennt; der Staffelordner wird dabei übersprungen, sonst
hiesse die Serie "Staffel 1". Plex und Jellyfin lesen ihn seit je so.

Dazu die Prüfliste: Folgen werden nach Serie gebündelt, weil zwanzig neue
Folgen eine Entscheidung sind und nicht zwanzig. Mit Mehrfachauswahl —
anhaken und gemeinsam freigeben oder auf einen Zeitpunkt legen. Und der
Zeitpunkt ist ein datetime-local statt eines Textfelds mit Formvorschrift,
bei dem jeder Tippfehler erst nach dem Absenden auffiel.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 13:58:33 +02:00
ScarriffleandClaude Opus 5 228c318ad2 Die Medientabelle spricht durchgehend dieselbe Sprache
Die Tonzeile nahm bereits die Formulierung vom Server, die Videozeile baute
ihren Text weiter selbst — "240p (H264)" neben "AAC · Mono", zwei
Schreibweisen in derselben Tabelle untereinander. Die gemessene Auflösung
wandert in den Tooltip, wo sie hingehört: 1040 ist keine Stufe, sondern ein
beschnittenes Bild.

Dazu: ein Spurtitel, der nur die Sprache wiederholt, fällt weg. "Deutsch ·
AAC · Mono · ‚Deutsch'" sagt dasselbe zweimal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 13:19:21 +02:00
ScarriffleandClaude Opus 5 632d3efa35 Neue Funde erst prüfen, dann veröffentlichen
Je Mediathek einstellbar, Vorgabe aus: wer eine Mediathek anlegt, will sie
sehen. Angeschaltet warten neue Funde unter "Zu prüfen", bis jemand sie
freigibt — oder ihnen einen Termin gibt.

Ein Zeitpunkt, kein Zustandsfeld. PublishAt null heisst wartet, in der
Vergangenheit heisst veröffentlicht, in der Zukunft heisst geplant. Die
Sichtbarkeit wird bei der Abfrage gerechnet statt von einem Dienst
umgeschaltet; damit stimmt sie nach einem Neustart, nach einer
Zeitumstellung und auch dann, wenn der Server zum geplanten Zeitpunkt gar
nicht lief. Ein Ticker hätte in jedem dieser Fälle das Falsche getan.

Das eigentliche Risiko waren die zwanzig Abfragestellen in vier Dateien.
Schriebe jede ihren eigenen Filter, reichte eine vergessene — und
auffallen würde es nicht beim Programmieren, sondern wenn jemand im
Haushalt einen Titel sieht, den er nicht sehen sollte. Deshalb ein
globaler Abfragefilter in FlaroContext, der sich nicht vergessen lässt,
und eine Middleware, die je Anfrage setzt, wer Unveröffentlichtes sehen
darf. Vorgabe am Kontext ist "alles sehen", damit Scanner und Metadaten
ohne Benutzer arbeiten können; für jede HTTP-Anfrage entscheidet die
Middleware.

Der Test zählt die Inhaltsrouten auf und prüft jede einzeln auf die
Abwesenheit eines wartenden Titels. Eine neue Route, die den Filter
umgeht, fällt dort durch.

Die Migration trägt für alle vorhandenen Titel das Hinzufügedatum als
Veröffentlichung nach. Ohne diese zwei Zeilen stünde jeder auf null — und
der gesamte Bestand wäre für alle ausser Administratoren verschwunden,
sofort beim Start im Container und ohne Meldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 13:03:24 +02:00
ScarriffleandClaude Opus 5 eb63271e92 Ein Kontextmenü auf jeder Kachel — mitsamt Bildauswahl
Gesehen, Favorit und Weglegen gab es ausschliesslich auf der Detailseite.
In einer Übersicht mit zwanzig Titeln hiess "die hab ich alle gesehen"
vierzig Seitenwechsel. Im ganzen Quelltext stand kein einziger
contextmenu-Zuhörer.

Drei Wege hinein, weil es drei Arten gibt zu bedienen: Rechtsklick für die
Maus, Shift+F10 und die Kontextmenü-Taste für die Tastatur, langer Druck
für den Finger. Nur den Rechtsklick anzubieten hiesse, das Menü auf dem
Telefon zu verstecken. Der lange Druck bricht bei jeder Bewegung ab, sonst
löst Scrollen ihn aus.

Die Aktionen wirken auf der Kachel selbst. Die Liste neu zu laden verlöre
Bildlauf und Filter — für einen Haken.

Dazu die Bildauswahl. Es gab genau einen Bild-Endpunkt, und der lieferte
nur aus: wählte TMDB ein schlechtes Plakat, blieb es. Jetzt stehen Plakat,
Hintergrund und Schriftzug zur Wahl, dazu eigene Bilder bis 10 MB. Die
Kandidaten kosten nichts — TMDB liefert sie beim Einzelabruf ohnehin mit,
und bisher wurde alles ausser dem Schriftzug weggeworfen.

Drei Merker je Titel, nicht einer: man kann ein eigenes Plakat wollen und
trotzdem automatische Hintergründe. Ohne sie überschriebe das nächste
Auffrischen jede Handauswahl, und wer einmal ein besseres Plakat gesucht
hat, suchte es jedes Mal neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:43:52 +02:00
ScarriffleandClaude Opus 5 e1399c808f Die Detailseite trägt wieder
Drei Dinge, eins davon war mein Fehlbefund: Regie und Drehbuch werden sehr
wohl angezeigt — nur standen sie irgendwo zwischen dreissig Gesichtern.
Jetzt kommt der Stab zuerst, weil man beim Suchen eher die Regie im Kopf
hat als den zwölften Darsteller.

Bei Zeichentrick hängt TMDB "(voice)" an den Rollennamen, und so stand es
auch da — englisch, mitten im deutschen Text. Daraus wird "Stimme: Gru".

Ähnliche Titel als eigene Reihe, nachgeladen, damit die Seite sofort
steht. Aus dem eigenen Bestand gerechnet, nicht aus TMDBs
Empfehlungsliste: ein Vorschlag, den man nicht abspielen kann, ist
Werbung. Gemeinsame Menschen wiegen dreifach gegenüber gemeinsamen
Genres — dass zwei Filme "Drama" sind, sagt wenig; dieselbe Regie sagt
viel. Kostet keine einzige fremde Anfrage, der Katalog steht ohnehin in
der Datenbank. Findet sich nichts, erscheint die Reihe gar nicht.

Und der Hintergrund: er begann bei left: 240px, also hinter der
Seitenleiste, und war mit opacity .28 und blur(2px) weder Bild noch
Fläche. Jetzt über die volle Breite, kräftiger unscharf — er taucht die
Seite in die Farbe des Films, so wie bei Plex.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:37:55 +02:00
ScarriffleandClaude Opus 5 11a44f6a90 Ein Aufmacher über die volle Breite
Die Startseite begann bei einer Reihe aus 150 px breiten Plakaten — das
grösste Element über dem Falz war 225 px hoch. Plex und Netflix öffnen mit
einem Bild über die ganze Breite, und das ist der grösste Einzelunterschied
im Eindruck. Ein Layout-, kein Farbproblem.

Bis zu sieben Titel, aus dem, was die Startseite ohnehin holt: erst
Angefangenes, dann Neuzugänge, dann Vorschläge. Kein zusätzlicher Abruf.

Nur mit Hintergrundbild. Ein Hero ohne Bild ist schlechter als keiner —
eine graue Fläche über den halben Bildschirm sagt nichts und nimmt den
Reihen den Platz. Gibt es keinen einzigen Titel mit Backdrop, entfällt er
ersatzlos.

Zwei Verläufe statt einem: der von unten lässt die erste Reihe sich
darüberschieben, der von links sorgt dafür, dass der Text trägt, auch wenn
das Bild dort hell ist.

Pause bei Mauszeiger und bei Tastaturfokus — wer gerade liest, soll nicht
nach neun Sekunden woanders landen. Bei "Bewegung reduzieren" wechselt er
gar nicht von selbst. Die Punkte sind echte Knöpfe mit Beschriftung; ein
div ist für Tastatur und Vorlesesoftware kein Bedienelement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:34:57 +02:00
ScarriffleandClaude Opus 5 6bd9cdb12d Eine Formensprache statt vier Radien und sechs Schriftgrössen
Die Oberfläche wirkte zusammengesetzt, nicht entworfen. Vier Gründe, alle
am Quelltext belegbar.

Die Farben waren von der App abgedriftet: elevated, placeholder, text,
text-dim, text-muted und die Trennlinie wichen in der letzten Stelle ab,
und der Standardakzent war in der App Lilac, im Browser Orchid. Einzeln
unsichtbar, zusammen genau der Grund, warum App und Browser nicht wie
dasselbe Programm aussahen. Alles auf FlaroDesign zurückgeführt.

Vier konkurrierende Radien — 6 auf Plakaten, 8 auf Karten, 999 auf jedem
Knopf, zweimal nacktes 3 — sind jetzt drei, und die ersten beiden stimmen
wieder mit FlaroDesign überein. Pillen bleiben, wo sie hingehören: Suche
und Filter.

Sechs Überschriftengrössen ohne Verhältnis zueinander sind eine Skala.
Der Kacheltitel war mit 13 px/500 leiser als die Metazeile daneben; das
erzeugt keine Hierarchie, sondern Rauschen.

Und der Akzent sass auf neun Dingen gleichzeitig. Wenn alles Akzent ist,
markiert er nichts. Der Primärknopf ist jetzt hell mit dunkler Schrift —
genau das macht Netflix, dessen Rot nur im Schriftzug vorkommt.

Dazu drei Dinge, die vorher ganz fehlten: ein sichtbarer Fokusring auf
allem Bedienbaren, ohne den die Oberfläche mit der Tastatur praktisch
unbedienbar war; Rücksicht auf "Bewegung reduzieren", das im Projekt
nirgends vorkam; und ein Hover, der mehr tut als zwei Bildpunkte zu
heben — mitsamt einem Abspielknopf, der nicht länger das halbe Plakat
verdeckt.

Tools/tokens-pruefen.py vergleicht beide Quellen und läuft im
Veröffentlichungsskript mit. Kein Generator: erzeugter Swift liest sich
schlechter als geschriebener, und die Prüfung fängt dasselbe ab.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:31:00 +02:00
ScarriffleandClaude Opus 5 0cd88acf70 Aus "DEU EAC3 6.0" wird "Deutsch · Dolby Digital Plus · 5.1"
Die Werte gingen roh von ffprobe in die Anzeige. Jedes Stück war richtig
und keines verständlich: EAC3 heisst Dolby Digital Plus, MOV_TEXT heisst
schlicht Text, und sechs Kanäle sind 5.1 — "6.0" ist eine Schreibweise,
die es nicht gibt. Die Auflösung stand als gemessene Höhe da, also "1040p"
für ein beschnittenes Breitbild, was aussieht wie ein Fehler.

Benennung macht daraus Sätze, und zwar im Kern: die Apps brauchen
dieselben, und zweimal formuliert heisst dieselbe Tonspur am Fernseher
anders als im Browser.

CultureInfo taugte dafür nicht, zweimal gemessen: sein NativeName liefert
den Namen in der Sprache selbst — "English", "français" —, und für ein
erfundenes Kürzel wie "xyz" erfindet es stillschweigend eine Kultur, statt
zu scheitern. Also eine eigene Liste, über das zwei- und das
dreibuchstabige Kürzel, weil beide in Mediendateien vorkommen und ISO
639-2 für manche Sprachen zwei kennt.

Unbekanntes bleibt stehen statt zu "Unbekannt" zu werden — die rohe
Angabe ist immer noch eine Auskunft. Und "und" verschwindet ganz: ffmpeg
schreibt das für unbekannt, und als Sprache angezeigt sieht es aus wie
eine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:28:17 +02:00
ScarriffleandClaude Opus 5 921533050b Zwei Titel je Film, und die Sprache lässt sich wählen
Die Mediathek sah zweisprachig aus — "Despicable Me" neben "Ich einfach
unverbesserlich". Es gab nur ein Titelfeld, und welche Sprache darin
landete, entschied die Einstellung zum Zeitpunkt des Scans.

Movie und Series tragen jetzt zusätzlich OriginalTitle. Gesucht wird über
beide: wer "Despicable Me" tippt, findet "Ich einfach unverbesserlich".
Auf der Detailseite steht der Originaltitel unter dem übersetzten, wenn er
abweicht.

Die Sprache der Metadaten ist in der Verwaltung eine Auswahl statt eines
Textfelds. Vorher musste man "de-DE" wissen, und ein Tippfehler darin nahm
TMDB stillschweigend als Englisch an.

Dazu eine eigene Sprache je Benutzer. Das Ehrliche daran steht im Dialog:
die Daten liegen in einer Sprache vor, und wer eine andere wählt, bekommt,
was übersetzt vorliegt — für den Rest die Serversprache, nicht ein leeres
Feld. ItemTranslation hält die zweite Sprache, gefüllt über eine
Sammelaktion. Nicht nebenbei beim Öffnen: das verteilte die Wartezeit über
Wochen und träfe immer den, der gerade etwas sucht.

Alle Sprachen für alle Titel vorzuhalten wäre der Bestand mal Sprachen an
Abrufen und Speicher, für einen Haushalt mit zweien. Deshalb auf Ansage.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:23:47 +02:00
ScarriffleandClaude Opus 5 f359807561 Der Fortschritt sagt, woran er gerade ist — und schreibt es mit
Vorher stand in der Verwaltung minutenlang "Wird durchsucht…", ohne
Hinweis, ob überhaupt etwas passiert. Bei vierzigtausend Dateien ist das
nicht zu unterscheiden von einem hängenden Lauf.

Scanschritt trägt Phase, Gegenstand und Zählstand und bildet daraus den
Satz selbst — im Kern und nicht in der Oberfläche, weil die Apps denselben
brauchen und zweimal formuliert er auseinanderdriftet. Sechs Phasen von
"Suche Medien in /media/Filme" über "Suche ‚Ich einfach unverbesserlich'
bei der Metadatenquelle — 44 von 1204" bis "Räume auf und lege Doppeltes
zusammen". Pfade werden auf die letzten zwei Bestandteile gekürzt.

ScanAsync und EnrichAsync bekommen je einen optionalen IProgress; optional
heisst, dass kein vorhandener Aufrufer bricht. Der Koordinator hält nur
den letzten Schritt — eine Liste über vierzigtausend Titel wäre ein Leck
im Arbeitsspeicher.

Dazu das Tagebuch: eine Datei je Tag unter config/logs, nach lokaler Zeit
und nicht nach UTC, weil ein Scan um 23 Uhr am heutigen Datum gesucht
wird. Dreissig Tage Aufbewahrung, einstellbar über FLARO_LOG_TAGE — ohne
Grenze läuft die config-Partition eines NAS lautlos voll. Es wirft nie:
ein volles Dateisystem darf keinen Scan abbrechen.

In der Verwaltung lesbar, nach Fehlern filterbar und herunterladbar. Das
Containerprotokoll ist nach einem Neustart weg und nur über SSH
erreichbar — die Frage "warum wurde der Film gestern Nacht nicht erkannt?"
liess sich damit nicht beantworten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:15:18 +02:00
ScarriffleandClaude Opus 5 637e95fbb1 Die Verwaltung zeichnete sich endlos neu
Gemessen am laufenden Server: nach dem Scan lief eine Schleife mit zwei
Abrufen je Runde, sichtbar als vier dauernd drehende Spinner.

Der Weg dahin: verfolgen() lief bei jedem Aufbau der Mediathekenzeile, sah
den Zustand finished, meldete das über geaendert(), und geaendert() war
mediathekenNeuLaden — also neu zeichnen. Der Neuaufbau startete verfolgen()
erneut, das wieder finished sah. Dass der Lauf längst vorbei war, machte
keinen Unterschied; der Zustand bleibt im Speicher stehen.

Zwei Dinge dagegen. verfolgen() meldet nur noch, wenn es selbst einen Lauf
angestossen hat — beim Aufbau zeigt es den Stand an und schweigt. Und die
Verwaltung frischt über zaehlerAuffrischen() nur noch die Seitenleiste auf
statt die ganze Seite neu zu zeichnen.

Dazu zwei Befunde aus dem Plan, die sich beim Nachsehen als falsch
erwiesen: die Takte werden sehr wohl abbestellt, dafür gibt es seit je
einen MutationObserver, und das Dashboard tauscht seinen Inhalt erst nach
dem Abruf. Geblieben ist der Spinner in den Listen, der bei jedem
Auffrischen erschien — der kommt jetzt nur noch beim ersten Aufbau.

Dazu der Zwischenspeicher für Ansichten. Der Router hängt sie aus statt
sie zu zerstören; beim Zurückkommen stehen sie sofort wieder da, mitsamt
Bildlauf, offenem Reiter und gesetzten Filtern, und laden leise nach.
Gemessen: null Abrufe und 4 von 4 Kacheln beim Zurückkommen aus der
Verwaltung. Höchstens fünf Ansichten, Verfall nach fünf Minuten,
weggeworfen bei allem, was den Bestand ändert. Detailseite und Verwaltung
sind ausgenommen — beide zeigen Laufendes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 12:11:06 +02:00
ScarriffleandClaude Opus 5 8b67a7ec88 Verschwundene Dateien werden gemeldet, nicht stillschweigend gelöscht
Vorher räumte der Scan jede Datei weg, die er nicht mehr sah, und mit der
letzten Datei fiel der Titel samt Sehfortschritt aller Benutzer. Für eine
wirklich gelöschte Datei ist das richtig. Für eine Platte im Ruhezustand,
ein noch nicht eingehängtes Volume oder einen umbenannten Ordner ist es
Datenverlust — und für den Scan sehen alle vier Fälle gleich aus.

Jetzt trägt MediaFile ein MissingSince. Der Eintrag bleibt stehen, steht
in der Verwaltung unter "Nicht mehr gefunden", und gelöscht wird auf
Ansage. Taucht die Datei wieder auf, verschwindet die Markierung von
selbst.

Dazu die Notbremse, und sie ist der eigentliche Grund für den ganzen
Punkt: Fehlt der Wurzelpfad, oder liegt dort keine einzige Mediendatei
mehr, obwohl vorher welche da waren, bricht der Lauf ab und sagt warum.
Ohne sie erklärte ein nicht eingehängtes Volume beim Start des NAS den
gesamten Bestand für verschwunden — bei 458 Titeln.

Der Abschnitt in der Verwaltung erscheint nur, wenn es etwas zu melden
gibt. Ein dauerhaft leerer wäre Rauschen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:46:55 +02:00
ScarriffleandClaude Opus 5 63c7ebb6d5 Ein Film kommt je Mediathek genau einmal vor
Der Scan verglich den Titel aus dem Dateinamen zeichengenau. Ein Punkt
Unterschied — Dr. Dolittle 2 gegen Dr Dolittle 2 —, oder eine Datei mit
Jahresangabe neben einer ohne, und es entstanden zwei Einträge. Die
Metadaten ordneten beide demselben TMDB-Eintrag zu, beide bekamen
denselben Titel, dasselbe Jahr, dasselbe Plakat, und nichts führte sie je
wieder zusammen.

Drei Ebenen, weil eine nicht trägt.

Titelschluessel vergleicht ohne Satzzeichen, ohne Akzente und ohne
Grossschreibung; ein fehlendes Jahr passt auf jedes, zwei bekannte
verschiedene trennen. Bewusst nicht aggressiver: Spider-Man und
Spiderman bleiben getrennt, und die beiden Dune von 1984 und 2021
sowieso. Ein zusammengelegter Film lässt sich nicht mehr trennen, ohne
zu wissen, welche Datei wohin gehörte.

Ein gefilterter Eindeutigkeitsindex auf (LibraryId, TmdbId) stellt die
Regel in die Datenbank statt in Code, der sie einhalten soll. Gefiltert,
weil ohne Metadaten alle Einträge null tragen.

Zusammenlegung räumt auf, was kein Vergleich erkennen könnte: Ich
einfach unverbesserlich und Despicable Me sind derselbe Film, und das
verrät allein die TMDB-Kennung. Läuft nach jedem Scan, und von Hand über
einen Knopf in der Verwaltung. Der Sehfortschritt wird gefaltet, nicht
umgehängt — die weitere Stelle gewinnt, gesehen gewinnt, weggelegt
gewinnt nicht.

Die Migration räumt zuerst auf und legt dann erst den Index an.
Andersherum scheiterte sie auf jeder Datenbank mit Dubletten, und zwar
beim Start im Container, wo die Meldung niemand sieht. Gemessen an einer
Datenbank mit zwei Dubletten und zwei Sehständen: 3 Filme auf 2, beide
Dateien erhalten, Position 1800 statt 300 behalten.

Dazu Fassungen. MediaFile.VersionLabel liest Mobil, 4K oder Director's
Cut aus dem Namen, sonst aus der gemessenen Bildhöhe. Die Liste ist
geschlossen, damit eine Fassung nicht YIFY heisst. Und der Fassungsname
beendet jetzt den Titel wie jedes andere Beiwerk — vorher wurde aus
"The Matrix (1999) - Mobil.mp4" der Film "The Matrix - Mobil", also ein
zweiter Eintrag neben dem Original. Genau die Dublette, um die es ging.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:39:10 +02:00
ScarriffleandClaude Opus 5 b25c7a4712 Ordner durchklicken, statt den Pfad zu wissen
Beim Anlegen einer Mediathek verlangte das Feld den Pfad im Container —
nicht den auf dem NAS. Diese Unterscheidung kennt nur, wer die
Compose-Datei selbst geschrieben hat, und ein Tippfehler darin ergibt
eine leere Mediathek ohne erkennbaren Grund.

Neben dem Feld steht jetzt ein Knopf, der die Ordner des Servers im
selben Dialog aufklappt: Pfadzeile in anklickbaren Stücken, Unterordner
darunter, unten der Knopf mit dem gewählten Pfad darauf.

Im selben Dialog und nicht in einem zweiten darüber: zwei gestapelte
Dialoge sind beim Schliessen immer eine Frage zu viel.

Das Feld bleibt tippbar. Wer den Pfad kennt, ist damit schneller, und
bei einem tiefen Baum ist es die einzige geduldschonende Möglichkeit.

An der Wurzel bleiben /proc, /sys, /usr und Verwandte aussen vor. Das
ist keine Absperrung — wer den Pfad kennt, tippt ihn weiterhin ein —,
sondern eine Liste aus zwanzig Einträgen, von denen achtzehn nie eine
Mediathek enthalten, ist unbrauchbar.

Ein Ordner ohne Leserecht wird gemeldet, nicht geworfen. Ohne das flöge
eine UnauthorizedAccessException bis in die Antwort und würde zur 500,
während die ehrliche Auskunft lautet: den darfst du nicht lesen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 11:22:16 +02:00
ScarriffleandClaude Opus 5 8a191d0faf Die Einrichtung läuft auch ohne HTTPS
crypto.randomUUID gibt es nur im secure context — HTTPS oder localhost,
sonst nichts. Wer Flaro auf einem NAS betreibt und ihn unter
http://192.168.x.x:8097 aufruft, hat die Funktion nicht, und die
Geräteerkennung warf mitten in die Einrichtung „crypto.randomUUID is not
a function". Das Konto liess sich nicht anlegen.

Das ist nicht der Sonderfall. Ein Heimserver im eigenen Netz hat kein
Zertifikat, und genau dort wird die Einrichtung durchlaufen.

crypto.getRandomValues ist nicht so beschränkt und liefert denselben
Zufall; daraus wird die UUID von Hand zusammengesetzt. Fällt auch das
aus, bleibt Math.random — kein Zufall für Geheimnisse, aber hier geht es
nur darum, zwei Browser auseinanderzuhalten, und gar keine Kennung wäre
schlechter als eine schwache.

Geprüft mit 20000 Durchläufen: alle entsprechen der Form von Fassung 4,
keine Dublette.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 09:04:30 +02:00
ScarriffleandClaude Opus 5 f2b5ee633b Dialoge statt Blöcke, bessere Filter, ein Dashboard — und eine NAS-Anleitung
**Was etwas anlegt oder ändert, steht jetzt in einem Dialog.** Das Formular
zum Anlegen einer Mediathek nahm als Block in der Seite Platz für etwas weg,
das man einmal im Monat braucht — und sah aus wie eine Eingabe, die noch
aussteht. Dasselbe für Benutzer, Umbenennen, den Servernamen und die
Metadaten-Einstellungen.

Gebaut mit dem `<dialog>` des Browsers, nicht mit einem nachgebauten: Escape
schliesst, der Hintergrund wird unbedienbar, der Tastaturfokus bleibt
gefangen, Vorlesesoftware kündigt ihn als Dialog an. Jede Zeile, die das
selbst baut, ist eine Zeile, die es schlechter kann. Scheitert das
Speichern, bleibt der Dialog offen — ihn zu schliessen hiesse, die Eingabe
wegzuwerfen und die Meldung dorthin zu schreiben, wo gerade niemand hinsieht.

**Die Filter sehen nicht mehr nach Eingabe aus.** Bei Plex stehen dort
Wörter mit einem kleinen Pfeil; ein umrandetes Auswahlfeld sagt „hier fehlt
noch etwas", und das stimmt nicht. Gesetzte Filter werden farbig, daneben
steht die Anzahl, und „Filter zurücksetzen" erscheint nur, wenn es etwas
zurückzusetzen gibt. Dazu neu: gesehen / noch nicht gesehen.

Das siebt vorerst in der Oberfläche, nicht am Server — er kann es noch
nicht. Der Preis steht in der Zahl daneben: sie zeigt dann „3 von 7".

---

**„Wird abgespielt" — wer gerade was schaut.**

Abgeleitet aus den Fortschrittsmeldungen, nicht aus einer eigenen Anmeldung
am Spieler: jeder Client meldet beim Abspielen ohnehin alle zehn Sekunden,
wo er steht. Wer das als Lebenszeichen liest, braucht kein zweites
Verfahren, das ein Client vergessen könnte — bleibt die Meldung aus, läuft
der Eintrag nach einer Minute von selbst ab.

Im Arbeitsspeicher, nicht in der Datenbank: „wer schaut gerade was" ist eine
Frage über den Augenblick. Nach einem Neustart lautet die Antwort „niemand",
und das zu speichern hiesse, Wiedergaben anzuzeigen, die es nicht gibt.

Zwei Geräte desselben Menschen sind zwei Zeilen — derselbe Mensch kann auf
dem Fernseher und auf dem Telefon gleichzeitig etwas laufen lassen.
Geschlüsselt wird deshalb über das Gerätetoken, nicht über den Benutzer und
nicht über den Gerätenamen: zwei iPhones heissen beide „iPhone".

**Nur für Administratoren.** Wer wann welchen Film anschaut, ist eine
Auskunft über Menschen, nicht über den Server. In einem Haushalt mag das
harmlos sein; die Vorgabe eines offenen Projekts darf es trotzdem nicht sein.

Der Spieler meldet beim Schliessen ausdrücklich „beendet". Ohne das stünde
ein geschlossener Film noch eine Minute als „läuft" — und wer gerade den
Spieler zugemacht hat, sähe sich selbst beim Schauen zu.

---

**`docs/NAS-EINRICHTEN.md`** beantwortet die Frage, die offen war: **es ist
ein einziges Image.** Die Weboberfläche wird beim Bauen hineingelegt und vom
Server selbst ausgeliefert — ein Container, drei Volumes, kein Proxy, keine
Datenbank daneben.

Drei Wege aufs NAS: über die Container-Registry deines Gitea (die läuft —
nachgeprüft), als Datei über `docker save`, oder dort bauen. Dazu der
Schritt, den man am leichtesten überspringt und am teuersten bezahlt: die
Architektur. Ein Mac baut ohne Angabe arm64, ein Intel-NAS braucht amd64,
und `exec format error` sagt das nicht deutlich.

Eine Zahl darin war geraten und ist korrigiert: `docker save | gzip` ergibt
nicht 90 MB, sondern 230 — die Ebenen eines Images sind bereits gepackt.

378 Tests grün, sechs davon neu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 08:03:07 +02:00
ScarriffleandClaude Opus 5 a7ec3a73ba Angemeldet bleiben — und eine 401 reisst nicht mehr die Sitzung ab
Erst die ehrliche Hälfte: dass es heute **andauernd** abgemeldet hat, geht
auf mein Konto. Ich habe die Datenbank dreimal geleert, um mit frischen
Metadaten zu bauen; jedes Mal verfallen alle Token. Kein Schalter der Welt
hätte das verhindert.

Die andere Hälfte ist ein echter Fehler, und er macht aus einem Aussetzer
eine Abmeldung:

**Eine einzelne 401 warf die Anmeldung weg** — und rief dabei
`/auth/logout`, der Server zog das Token also *aktiv zurück*. War die
Ablehnung nur ein Aussetzer, war eine intakte Anmeldung damit absichtlich
zerstört, und zwar unwiderruflich. Jetzt wird einmal nachgefragt: gilt das
Token wirklich nicht mehr, ist die Sitzung vorbei; antwortet der Server, war
es diese eine Anfrage. Mehrere gleichzeitige Anfragen laufen in dieselbe
Prüfung — sonst stellte eine Startseite mit acht Abfragen achtmal dieselbe
Frage. Antwortet der Server **gar nicht**, ist das kein Beleg gegen das
Token: jemanden abzumelden, weil das WLAN hakt, ist der falsche Schluss.

**Abmelden scheitert nie mehr.** Vorher verlangte der Weg eine gültige
Anmeldung — und genau dann, wenn man ihn am dringendsten braucht, gibt es
keine. Nach dem Leeren des Datenbestands zeigte das Cookie im Browser auf
ein Token, das es nicht mehr gab; es ging bei jedem Bild erneut hinaus, und
der Server lehnte es jedes Mal ab. Achtzig Ablehnungen in einer Stunde, und
nichts davon liess sich von aussen beenden. Jetzt wird das Cookie immer
gelöscht; widerrufen wird, was sich widerrufen lässt.

---

**Und der Schalter, um den es ging.** „Angemeldet bleiben" steht jetzt im
Anmeldefeld, mit Haken als Vorgabe — am eigenen Rechner will sich niemand
täglich neu anmelden.

Umgesetzt über die beiden Speicher, die der Browser ohnehin richtig
aufräumt: mit Haken `localStorage` und ein Cookie für ein Jahr, ohne Haken
`sessionStorage` und ein Sitzungscookie, das mit dem Fenster verschwindet.
Ein eigenes Verfallsdatum zu verwalten wäre die schlechtere Variante — eine
Uhr, die man stellen kann, gegen zwei Mechanismen, die es schon gibt.

Das Token selbst läuft weiterhin **nie** ab; es wird widerrufen, nicht
vergessen. Was hier entschieden wird, ist allein, wie lange der Browser sich
erinnert.

372 Tests grün, vier davon neu. Am Container nachgemessen: mit Haken
`max-age=31536000`, ohne Haken kein `max-age`, und ein Abmelden mit
ungültigem Cookie antwortet 204 statt 401.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 07:40:19 +02:00
ScarriffleandClaude Opus 5 8c395ac051 Die Oberfläche nach Plex' Vorbild — Hintergrund, Schriftzug, Besetzung
Nach den Bildern deiner Plex-Installation umgebaut. Übernommen ist der
Aufbau, nicht die Farbe: Plex ist bernsteingelb, Flaro bleibt Orchidee.

**Kopfleiste oben** mit Marke, Suche und Konto. Die Suche gehört dorthin und
nicht in jede Mediathek: wer einen Titel sucht, weiss oft gar nicht, in
welcher er liegt. Sie geht quer durch alles.

**Seitenleiste mit Icons** — echte SVG, keine Zeichen aus einer Schriftart.
Ein ▶ aus Unicode sieht auf jedem System anders aus, lässt sich nicht
einfärben und springt in der Grundlinie. Die aktive Zeile bekommt einen
schmalen Balken links statt eines leuchtenden Blocks; das liest sich in
einer langen Liste ruhiger.

**Die Kachel steht jetzt an einer Stelle** statt an fünfen. Sie trägt den
Haken für Gesehenes in der Ecke — wie bei Plex, ein Blick über das Raster
genügt —, die Zahl offener Folgen an derselben Stelle, den Fortschrittsbalken
unten und beim Überfahren einen Abspielknopf. Folgen sind dreizeilig: Serie,
Folgentitel, „S14 · F22".

**Reihen haben Pfeile.** Zwanzig Kacheln passen nicht in den Bildschirm, und
quer zu scrollen geht mit dem Rad nur mit Umschalttaste. Die Überschrift
nennt die Mediathek beim Namen: „Zuletzt in Dokumentationen hinzugefügt".

**Mediatheken haben drei Reiter** — Empfehlungen, Mediathek, Kategorien.
Darunter eine Filterzeile mit Genre, Sortierung und der Anzahl. Die
Kategorien sind farbige Kacheln; die Farbe kommt aus dem Namen, nicht aus
einer festen Liste, die jedes Genre kennen müsste, das TMDB je erfindet.

**Die Detailseite** hat jetzt den unscharfen Hintergrund über die ganze
Seite, den **Schriftzug des Films** statt der Systemschrift, eine Zeile mit
Jahr, Genres, Laufzeit, Altersfreigabe und Bewertung, runde Knöpfe für
gesehen/Favorit/weglegen, die Medienangaben als Tabelle — Datei, Video, Ton,
Untertitel — und zuletzt Besetzung & Stab mit runden Porträts.

**Dazu am Server:** ein Name. Ohne ihn zeigten die Clients den Hostnamen,
und die App schrieb „localhost" über die Anmeldung. Vorgabe ist der
Rechnername des Containers.

---

**Der Fehler, den das erste Bild zeigte:** die Lupe in der Suchleiste war
dreihundert Bildpunkte gross und lag über der halben Seite. Ein rohes
`<svg>` ohne Breite füllt seinen Container; die Regel galt nur für `.icon`.
Jetzt für jedes.

368 Tests grün. Am Container nachgesehen: „Der Untergang" mit eigenem
Schriftzug, FSK 12, Bewertung 7,9, Genres Drama/Historie/Kriegsfilm, beide
Tonspuren in der Tabelle, 22 Porträts darunter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-04 01:04:27 +02:00
ScarriffleandClaude Opus 5 230beb9664 Mehrere Mediatheken, jede für sich
Bisher gab es in der Seitenleiste „Filme" und „Serien" — zwei feste Zeilen,
hinter denen alles zusammenfiel, was der Server kannte. Wer „Filme" und
„Dokumentationen" getrennt angelegt hatte, sah sie wieder in einem Topf.
Das nahm die Entscheidung zurück, sie zu trennen.

Jetzt führt die Seitenleiste **jede Mediathek einzeln**, mit Namen und Zahl,
wie bei Plex und Jellyfin. Die Startseite bekommt je Mediathek eine Reihe
mit dem zuletzt Hinzugefügten; steht mehr drin, als die Reihe zeigt, führt
„Alle zeigen" dorthin. Die Mediathekenseite trägt den Namen im Titel, sucht
innerhalb der Mediathek und lässt sich sortieren.

**Am Server dafür:**

`?sort=` auf `/movies` und `/series` — `title`, `added`, `year`. Der Titel
immer als letzter Vergleich, sonst hätten zwei Filme aus demselben Jahr keine
feste Reihenfolge und Seite 2 zeigte denselben Film noch einmal oder gar
nicht.

Die Liste der Bibliotheken nennt je Zeile einen Zählwert. „Dokumentationen ·
0" sagt mehr als ein Eintrag, hinter dem sich nach dem Klick nichts findet —
da ist der Unterschied zwischen „noch nicht gescannt" und „falscher Pfad"
wenigstens sichtbar. `null` heisst „nicht gezählt", nicht „leer".

Umbenennen und Entfernen, beides nur für Administratoren. **Umbenennen, mehr
nicht:** die Art zu ändern hiesse, Filme in Serien zu verwandeln; den Pfad
zu ändern hiesse, einen Bestand gegen einen anderen zu tauschen, während die
Einträge stehenbleiben. Beides ist kein Bearbeiten, sondern eine neue
Bibliothek.

**Beim Entfernen bleiben die Mediendateien liegen.** Weg sind der Eintrag,
die Zuordnungen und der Sehfortschritt aller Benutzer dazu; die Oberfläche
sagt das vorher, und eine Prüfung hält fest, dass die Datei danach noch da
ist. Ein Server, der beim Aufräumen in der Oberfläche fremde Filme löscht,
ist einer, den man nicht noch einmal benutzt.

---

**Der Fehler, den der Browser zeigte, und er betrifft jeden Betrieb:** nach
einem Server-Update lief weiter die alte Oberfläche.

Die Dateien unter `assets/` tragen eine Prüfsumme im Namen — ändert sich der
Inhalt, ändert sich der Name. `index.html` trägt keine; sie nennt nur deren
Namen. Lag sie im Zwischenspeicher, bekam der Browser weiter die alte App:
nicht kaputt, sondern veraltet, und das fällt niemandem auf. Nachgemessen:
geladen war `index-C0K3V8eg.js`, ausgeliefert wurde längst
`index-DomHN4_b.js`.

Jetzt zwei Lebensdauern — `no-cache` für die Seite, ein Jahr und `immutable`
für die Bausteine. Mit Prüfung, und am Container nachgemessen.

Der Beispielbestand bekommt zwei weitere Mediatheken, „Dokumentationen" und
„Konzerte". Mit nur Filme und Serien liesse sich gar nicht prüfen, ob
mehrere wirklich getrennt bleiben — zwei Bestände **derselben Art** sind der
Fall, in dem es auffällt.

358 Tests grün. Am Container nachgewiesen: vier Mediatheken in der
Seitenleiste mit Zählwerten, je eine Reihe auf der Startseite, und
„Dokumentationen" zeigt zwei Titel statt sieben.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 22:57:25 +02:00
ScarriffleandClaude Opus 5 6d5824b889 Weiterschauen, sichtbar gemacht
Die Oberfläche zeigt jetzt, was der Server seit Stufe 4 weiss.

Auf der Startseite zwei neue Reihen, ganz oben: **Weiterschauen** und
**Nächste Folge**. Sie überschneiden sich nicht — was angefangen ist, steht
links, was ansteht, rechts. Eine leere Reihe wird gar nicht erst gezeigt.

Auf jeder Kachel ein Balken, und zwar **ab der ersten Sekunde**. Bei einem
Zweieinhalbstünder sind zwei Minuten 1,3 % — anderthalb Bildpunkte. Der
Balken hat deshalb eine Mindestbreite, sonst wäre das Versprechen „wir merken
uns jede Sekunde" ausgerechnet dort unsichtbar, wo es zählt. Serien tragen
die Zahl offener Folgen als Plakette in der Ecke.

Auf der Detailseite stehen **beide Knöpfe nebeneinander**: „Weiterschauen ab
0:42" und „Von vorn". Auch bei einem gesehenen Film — der Server wirft die
Stelle nicht weg, wenn das Häkchen kommt, und sie hier zu verschweigen hiesse,
genau den Unterschied wegzuwerfen, um dessentwillen er sie behält. Dazu ein
Schalter für gesehen/ungesehen.

Der Spieler meldet alle zehn Sekunden, beim Pausieren, am Ende und beim
Schliessen. Nicht bei jedem `timeupdate`: das feuert viermal je Sekunde, und
daraus Anfragen zu machen hiesse, den Server mit Meldungen zu bewerfen, die
sich um 250 ms unterscheiden. Eine verlorene Meldung kostet ein paar Sekunden
Fortschritt — dafür mitten im Film einen Fehler einzublenden wäre die
schlechtere Hälfte des Tauschs.

**Nebenbei ein Fehler beseitigt, der nur mit grosser Mediathek aufgefallen
wäre:** die Detailseite holte 200 Titel und suchte darin nach der Kennung.
Bei 201 Filmen hätte sie vom letzten behauptet, es gebe ihn nicht mehr. Jetzt
gibt es den Einzelabruf, und sie benutzt ihn.

Im Container durchgespielt: „Der Untergang" von vorn, die MKV wird umgepackt,
läuft durch — danach steht „Gesehen" unter dem Titel, der Balken ist voll,
und der Knopf bietet beides an.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:33:56 +02:00
ScarriffleandClaude Opus 5 91eb934048 Die Weboberfläche — im Zuschnitt von Plex, vom Server selbst ausgeliefert
Einrichtung, Anmeldung, Mediathek, Detailseite mit Abspielen und
Herunterladen, Zuordnung korrigieren, Verwaltung. Die Oberfläche aus der
Jellyfin-Zeit ist dabei vollständig ersetzt: `types.ts`, `prefs.ts`,
`contrast.ts`, `views/cards.ts`, `views/player.ts` und `views/settings.ts`
sprachen Jellyfins Schnittstelle und hatten hier nichts mehr zu suchen.

Seitenleiste links, waagerechte Reihen auf der Startseite, Plakate im
Verhältnis 2:3, unscharfer Hintergrund hinter dem Kopf der Detailseite —
das ist der Zuschnitt von Plex, weil er gefällt. **Die Farbe bleibt Flaros
Orchidee**, nicht Plex' Orange; der Aufbau ist das Vorbild, nicht das
Aussehen.

Welche Bibliothek gezeigt wird, entscheidet die Seitenleiste. Eine zweite
Umschaltung innerhalb der Seite wäre dieselbe Entscheidung an zwei Orten.

---

**Der Zugang liegt jetzt auch in einem Cookie.** Ein `<img>`, ein `<video>`
und ein Download-Link senden keine eigenen Kopfzeilen — eine App kann ihr
Token in jede Anfrage legen, ein Browser nicht. Das Token stattdessen in
die Adresse zu schreiben wäre der naheliegende Ausweg und stünde danach in
jedem Zugriffsprotokoll, in jedem Verlauf und in jedem weitergegebenen
Link.

`HttpOnly`, damit kein eingeschlichenes Skript herankommt. `SameSite=Strict`
gegen Anfragen von fremden Seiten. `Secure` nur über https — sonst liesse
sich die Oberfläche im Heimnetz über http gar nicht benutzen. Die Apps
benutzen weiterhin die Kopfzeile: ein zweiter Weg zum selben Token, kein
zweites Verfahren.

---

**Der Fehler, den erst der echte Container zeigte:** eine weisse Seite,
ohne Hinweis worauf.

Die Rückfallregel verlangt für jede Anfrage ohne eigenen Endpunkt einen
angemeldeten Benutzer, und `/assets/index.js` hat keinen. Die statischen
Dateien standen unter `UseAuthorization` — ein neuer Besucher bekam 401 auf
genau das Skript, das ihm die Anmeldemaske zeichnen sollte. `/` kam durch,
weil der SPA-Rückfall `AllowAnonymous` trägt; alles mit Dateiendung nicht.
Jetzt liegen sie davor. Mit Prüfung.

Beim Bau desselben Images der zweite: seit der Kontext die Projektwurzel
ist, überschrieb `COPY server/` die `obj/`-Verzeichnisse aus dem Container
mit denen vom Arbeitsrechner — die zeigen auf osx-arm64, gebaut wird
linux-arm64, `dotnet publish` brach mit NETSDK1047 ab. Dafür gibt es jetzt
eine `.dockerignore` in der Wurzel; die unter `server/` sucht Docker seit
dem Umzug nicht mehr.

---

Vier Prüfungen schlugen danach fehl, und zwar zu Recht: sie hiessen „ohne
Anmeldung gibt es nichts", benutzten aber den Client, der nach dem Anmelden
ein Cookie hält. Sie prüften also den Angemeldeten. Jetzt sagen sie
ausdrücklich, dass sie ohne Zugang fragen.

313 Tests grün. Im Container nachgewiesen: Anmelden, Plakate, Abspielen,
Staffeln und Folgen, Verwaltung — und ein Neuladen auf `/verwaltung`, das
nicht in einem 404 endet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 21:07:29 +02:00
ScarriffleandClaude Opus 5 a7cf6c5def Halo heisst ab hier Flaro — die Marke stand im Weg
Der Upload zu App Store Connect scheiterte am Namen. „Halo" ist eine
eingetragene Marke von Microsoft; im App Store heisst zwar keine App exakt
so, aber reservierte und markenrechtlich belegte Namen tauchen in keiner
Suche auf. Der einzige verlässliche Test war die Eingabe dort, und die ging
daneben.

**Der Name wurde diesmal vorher geprüft.** Zwanzig Kandidaten gegen den
deutschen und amerikanischen App Store, auf exakte Treffer und auf ähnliche
Namen. „Kavo" wäre dieselbe Wand gewesen — exakt belegt in beiden Stores, in
Deutschland von KaVo Dental GmbH. „Flare" ist mit 42 ähnlichen Apps je Store
der am dichtesten besetzte Name der ganzen Runde. **Flaro** ist in beiden
frei und liest sich als eigenes Wort, nicht als Flare mit Tippfehler.

Das bleibt eine Store-Suche und keine Markenrecherche — reservierte Namen
ohne veröffentlichte App sieht sie nicht. Aber es ist mehr Prüfung, als beim
letzten Mal stattfand.

---

2160 Textvorkommen in 273 Dateien, 49 Pfade. Betroffen ist diesmal auch der
Server: `Flaro.Api`, `Flaro.Core`, `Flaro.Data`, `Flaro.Media`,
`Flaro.Metadata`, `FlaroContext`, `FlaroPaths`, `flaro.db`, die Variablen
`FLARO_DATA`, `FLARO_CACHE`, `FLARO_TRANSCODE`, `FLARO_WRITE_SIDECARS`, und
das Image.

**Vor dem Ersetzen geprüft**, ob „halo" irgendwo als Teil eines anderen
Wortes steckt — und ob es im TMDB-Token vorkommt. Tat es nicht; ein blindes
Ersetzen hätte den sonst zerlegt.

`docs/superpowers/specs/` bleibt wie beim letzten Mal unangetastet: datierte
Entwurfsdokumente sind ein Protokoll getroffener Entscheidungen. Das
Server-Dokument hat stattdessen einen Nachtrag bekommen, damit es niemanden
in die Irre führt.

**Die Bundle-ID wandert mit**, zum zweiten Mal an einem Tag:
`com.scarriffleservices.flaro`. Wer die vorige Fassung installiert hat, hat
jetzt zwei Karteileichen auf dem Gerät — beide löschen, dann ist Ruhe.

308 Server-Tests grün, 368 im Swift-Paket, null Warnungen. iOS und tvOS
bauen, das Image baut, und ein frischer Container läuft durch Einrichtung
und Anmeldung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 19:38:27 +02:00
ScarriffleandClaude Opus 5 4b9a1e7b67 Flixy heißt ab hier Halo
Reine Umbenennung, kein Verhalten geändert — bewusst als eigener Commit,
damit „umbenannt" und „umgebaut" im Nachhinein auseinanderzuhalten sind.

712 mal `Flixy`, 778 mal `flixy`, 7 mal `FLIXY` in 169 Dateien, dazu 25
Pfade. Ziele: `Halo`, `HaloTV`, `HaloTopShelf`, `HaloUITests`, `HaloKit` mit
`HaloDesign`, `HaloDownloads`, `HaloPlayback` und `HaloProbe`. `MediaKit`,
`JellyfinKit` und `DownloadWidget` tragen den Namen nicht und bleiben.

`docs/superpowers/specs/` ist ausgenommen. Datierte Entwurfsdokumente sind
ein Protokoll getroffener Entscheidungen; sie nachträglich umzuschreiben
hiesse, die Vergangenheit zu glätten.

**Die Bundle-ID wandert mit** — ausdrücklich so gewollt, mit bekanntem Preis:
`com.scarriffleservices.halo` ist für iOS eine *andere* App. Die installierte
Fassung bleibt daneben stehen, mit Downloads und Anmeldung in ihrem Container;
Halo startet leer. Einmal neu anmelden, heruntergeladene Folgen einmal neu
laden. Dasselbe gilt für die App-Gruppe, die sich TV-App und Top Shelf teilen.

**Eine Folge davon, die beim Entscheiden noch nicht auf dem Tisch lag:** der
Aufräumschritt aus dem vorigen Commit kommt an die alten Plex-Token nicht mehr
heran. Der Schlüsselbund grenzt nach Team und Bundle-Kennung ab — was unter
`…flixy` liegt, ist für `…halo` unerreichbar. Erledigt wird es trotzdem, nur
anders: **die alte App löschen.** iOS räumt deren Schlüsselbund-Einträge beim
Deinstallieren mit ab. Der Code bleibt stehen, weil er für jeden Stand vor
dieser Umbenennung richtig ist und im Jahr einen Startvergleich kostet.

Die Projektdatei ist aus `project.yml` neu erzeugt, der alte Ordner gelöscht
statt umbenannt — sie steht ohnehin nicht im Repo.

Das Verzeichnis des Repos selbst heißt weiter `Flixy`; das umzubenennen ist
ein Handgriff ausserhalb des Projekts.

368 Tests grün. iOS und tvOS bauen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-10-03 11:12:32 +02:00
ScarriffleandClaude Opus 5 efed8717a3 „Auto" gibt nicht mehr auf, es versucht es kleiner
Der Fehlstart im Zug hat den eigentlichen Mangel gezeigt: die ganze neue
Regelung misst **ab dem ersten Bild**. Kommt nie eines, misst sie nichts,
greift nichts — und „Auto" hat sich als das erwiesen, was es wörtlich
heisst: „nimm, was in den Einstellungen steht". Steht dort „Original",
bittet die App über eine Zugverbindung um eine 4K-Datei und wartet
vierzig Sekunden auf etwas, das nie kommen kann.

Statt aufzugeben geht der Watchdog jetzt eine Sprosse tiefer und baut
neu auf — bei Filmen über `restart()`, bei Sendern über den Tuner-Abbau.
Gezählt gegen dasselbe Budget wie die Sicherheitsleine, sonst schickt
eine tote Leitung die App endlos im Kreis.

Scheitert auch das, nennt die Meldung jetzt den Grund und den Weg statt
nur „keine Daten mehr": wer ohne Grenze angefragt hat, soll erfahren,
dass es daran liegt und wo es sich ändern lässt.

Und „Strom" heisst Stream. Zweiunddreissig Stellen, dazu das
Gedächtnis ergänzt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-27 18:30:10 +02:00
ScarriffleandClaude Opus 5 9a4b6b1e06 Ein Kreisel ist kein Pausenknopf
Während der Player auf Daten wartete, stand im mittleren Knopf das
Abspieldreieck und der Ladekreis lag dahinter. Das las sich als
„angehalten": man drückte auf Abspielen, und es geschah nichts — es lief
ja bereits, es kam nur nichts an.

Jetzt sitzt der Kreisel **im** Knopf, dort wo sonst das Symbol steht. Der
freistehende Kreisel in der Bildmitte bleibt für den Fall, dass die
Bedienelemente ausgeblendet sind. Zwei übereinander gibt es nicht mehr.

`togglePlayPause` zählt das Warten jetzt zum Laufen. Vorher führte ein
Tipp in diesem Moment zu `play()` auf etwas, das schon spielte.

Dazu auf dem Mac, wie im Browser auch:

- Ein Klick ins Bild hält an, statt erst die Bedienelemente zu holen. Die
  kommen dort von allein, sobald sich der Zeiger regt.
- Leertaste hält an, Pfeil links und rechts springen.

Mit dem Finger bleibt es beim Alten: ohne Zeiger gibt es keine Bewegung
ohne Berührung, und ein Tipp, der sofort anhält, nähme den einzigen Weg
zu Scrubber, Tonspur und Qualität.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 20:33:24 +02:00
ScarriffleandClaude Opus 5 3af6c3112f Das Web ist jetzt ein Projekt
Schritt 2 des Plans, fertig. Aus 1700 Zeilen in einer Datei sind zwölf
Module geworden: api, prefs, contrast, icons, util, types und die
Ansichten für Anmeldung, Startseite, Mediathek, Einstellungen, Kacheln
und Player. Alles typgeprüft, und die Prüfung läuft scharf —
noUncheckedIndexedAccess inbegriffen.

An einer Stelle hat der Compiler mich zu Recht aufgehalten: Object.fromEntries
verliert die Typen jedes einzelnen Feldes, und geblieben wäre nur ein
doppelter Cast, der die Prüfung stilllegt statt sie zu bestehen. Jetzt
steht es ausgeschrieben da.

hls.js wird erst beim Abspielen geladen. Es ist größer als der ganze Rest
zusammen, und wer nur stöbert, braucht es nie: der Start fällt damit von
632 auf 39 Kilobyte.

Geprüft mit einem erfundenen Medienserver dahinter: Seite kommt an,
gebaute Dateien werden ausgeliefert, Anfragen gehen weiter, und die
Startseite zeichnet Insel, Aufmacher und Reihen.

Die einzelne Datei bleibt liegen, bis das Projekt sich bewährt hat.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:31:56 +02:00
ScarriffleandClaude Opus 5 a04784ca15 Das Web bekommt ein Fundament und einen eigenen Server
Schritt 2 des Plans, erste Hälfte. Vite mit TypeScript, und ein Server
ohne eine einzige Abhängigkeit — er soll auf einem kleinen Container
laufen, nicht sein halbes Verzeichnis mitschleppen.

Der Server ist nicht Zierde. Er löst zwei Dinge, gegen die keine Zeile im
Frontend etwas ausrichten kann: Anfragen laufen über dieselbe Adresse wie
die Seite, womit CORS erledigt ist, auch beim Streamen. Und läuft der
Medienserver im Klartext, während die Seite verschlüsselt ausgeliefert
wird, blockiert der Browser sonst den Strom.

Geprüft: Weiterreichen mitsamt Autorisierung und richtig gesetztem Host,
Ausliefern gebauter Dateien, unbekannte Wege landen auf der Seite — und
ein Ausbruchsversuch mit "../../../etc/passwd" liefert die Seite statt
der Passwortdatei.

Der Körper wird durchgereicht statt gepuffert: ein Film ist ein Strom,
und ihn erst vollständig einzulesen hieße, ihn zwischenzuspeichern.

Die einzelne Datei bleibt vorerst, sie läuft ja.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:25:11 +02:00
ScarriffleandClaude Opus 5 87b9fae94b Die Fortschrittsmeldungen waren stumm
Sie hingen an einem verschluckten catch. Beim Suchen nach dem verlorenen
Fortschritt war deshalb nirgends zu sehen, ob sie überhaupt ankommen —
ohne await zu senden ist richtig, ohne Hinsehen nicht.

Jetzt steht in der Konsole, was angenommen und was abgelehnt wurde.

Dazu ein Hinweis auf die Regel des Servers, die den eigentlichen Fall
erklärt: Jellyfin merkt sich eine Fortsetzposition nur zwischen fünf und
neunzig Prozent der Laufzeit. Eine halbe Minute von einem Film liegt
darunter, und der Server verwirft sie bewusst — "kurz reingeschaut" ist
kein Weiterschauen. Wer das nicht weiß, sucht den Fehler bei sich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 19:02:01 +02:00
ScarriffleandClaude Opus 5 b51bb16de0 Der Fortschritt ging beim Schließen verloren
Beim Schließen wurde das Video abgeräumt, und dabei fiel currentTime auf
null — bevor die Stopp-Meldung hinausging. Jellyfin bekam "gestoppt bei
0:00" und löschte den Fortschritt. Der Film war zur Hälfte gesehen und
stand trotzdem nicht unter "Weiterschauen".

Jetzt wird die Stelle getrennt vom Element gehalten und die Meldung
gesendet, bevor abgeräumt wird. Nachgemessen: bei 45 Sekunden
geschlossen, 45 Sekunden gemeldet. Dazu PlayMethod und CanSeek, ohne die
Jellyfin die Meldung keiner Art von Wiedergabe zuordnet, und sofortige
Meldung bei Anhalten und Weiterlaufen — wer pausiert und weggeht, hatte
sonst bis zur nächsten Zehn-Sekunden-Marke nichts hinterlassen.

Und das eingefrorene Bild bei laufendem Ton nach einem
Anwendungswechsel: Der Browser hält das Entpacken an, während der Tab
verdeckt ist, und hls.js findet danach nicht von selbst in den Puffer
zurück. Jetzt wird beim Zurückkommen an derselben Stelle neu angestoßen,
mit einem winzigen Sprung — ohne den bleibt das zuletzt gezeigte Bild
stehen, obwohl längst wieder Segmente ankommen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:56:10 +02:00
ScarriffleandClaude Opus 5 fa8d9de2e9 Die Knöpfe hatten Flächen, die es in der App nie gab
Ich habe nach dem Code gebaut und dabei das Wesentliche übersehen: In
PlayerView gibt es genau ein "background", und das ist die Abdunklung
über dem ganzen Bild. Die Knöpfe selbst sind blanke weiße Symbole.

Meine runden, hinterlegten Flächen waren eine Erfindung — und der Grund,
warum der Qualitätsknopf schief aussah: ein Wort passt in keinen Kreis.
Jetzt steht "Original" als das da, was es ist, nämlich Text.

Dazu die Spultasten als Kreispfeile mit der Sekundenzahl darin, wie
gobackward.10 und goforward.30 sie zeichnen, statt als "−10" und "+30"
in Textform. Und der Abspielpfeil in 52 Punkt, der Größe aus der App.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:50:09 +02:00
ScarriffleandClaude Opus 5 6d8440ede8 Unten steht jetzt nur noch der Scrubber, wie in der App
Der strukturelle Unterschied, den ich übersehen hatte: in der iPad-App
liegt unten nichts als der Regler über die volle Breite, darunter Zeit
links und Restzeit rechts. Bei mir standen Zeit, Regler und Restzeit
nebeneinander — der Regler wirkte dadurch wie ein Formularfeld zwischen
zwei Beschriftungen — und darunter lag noch eine Werkzeugleiste mit
Lautstärkeregler und Tempo-Auswahlfeld, die es in der App gar nicht gibt.

Alles Einstellbare sitzt jetzt oben rechts beisammen: Tonspur,
Untertitel, Qualität, Geschwindigkeit, Ton, Vollbild. Das Tempo als
Liste wie die anderen; ein Auswahlfeld mitten in der Bedienleiste war das
einzige Bauteil, das nach Formular aussah. Die Lautstärke über Mausrad
auf dem Tonknopf und über die Pfeiltasten.

Und ein Fehler, der genau das verhinderte, was gewünscht war: Die
Bedienfläche lag als Ganzes über dem Bild und schluckte jeden Klick.
"Ins Bild tippen hält an" funktionierte damit nur, solange die Bedienung
unsichtbar war — also fast nie. Jetzt nehmen nur ihre Knöpfe Klicks
entgegen. Nachgemessen: Klick pausiert, Pfeil rechts spult 30 Sekunden.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 18:44:41 +02:00
ScarriffleandClaude Opus 5 24e40f7403 Der Web-Player folgt jetzt dem Bauplan des iPad-Players
Mein Menü machte genau den Fehler, den der iPad-Code im Kommentar
ausschließt: alles in einer Liste. Dort steht, warum das falsch ist —
"die Tonspur stellt man einmal am Anfang um, die Untertitel mitten im
Film. Ein gemeinsames Blatt bedeutete jedes Mal Scrollen an der anderen
Liste vorbei."

Jetzt drei Knöpfe in der Kopfzeile, jeder mit seiner eigenen Liste
darunter, und jede Liste hängt sichtbar an ihrem Knopf. Die Tonspur
erscheint auch dann, wenn es nur eine gibt — welche läuft, will man
wissen, nicht nur ob man wechseln kann. Die Untertitel-Taste ist
gefüllt, solange welche laufen.

Die Qualität steht als Text im Knopf, nicht als Symbol. Auch das aus dem
iPad-Player übernommen: "Drei Schieberegler sagen nicht, was eingestellt
ist; '1080p' schon — und genau das ist die Frage, wegen der man hintippt."

Dazu eine gleichmäßige Abdunklung statt zweier Verläufe an den Rändern.
Die Verläufe ließen die Mitte frei — wo die Transportknöpfe stehen, die
damit auf hellen Szenen nichts hinter sich hatten.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:53:17 +02:00
ScarriffleandClaude Opus 5 9da446b295 Qualität, Tonspur und Untertitel im Player
Sie standen bisher nur in den Einstellungen — also dort, wo man sie nicht
braucht. Man merkt beim Schauen, dass der Ton die falsche Sprache hat,
nicht vorher.

Jetzt ein Menü über der Bedienleiste, nicht als Blatt: man stellt hier
etwas ein, das man am Bild beurteilen will, und ein Blatt verdeckt genau
das. Die Auswahl kommt vom Server — welche Spuren es gibt, weiß man erst
nach dem Aushandeln, und seine Vorauswahl gilt beim ersten Lauf.

Anders als die App, die im Lauf umschaltet, muss der Browser neu
aushandeln: bei einem umgerechneten HLS-Strom bestimmt der Server beim
Aushandeln, was drin ist. Also wird neu geladen — und die Stelle
mitgenommen, sonst begänne jeder Sprachwechsel den Film von vorn.
Nachgemessen: bei 27:30 gewechselt, bei 27:30 weiter.

Dabei zwei Dinge mitgenommen, die sonst später weh getan hätten: der
Fehler-Horcher wird nur einmal angehängt (sonst meldete sich nach zwei
Wechseln ein einziger Fehler dreimal), und ein früherer HLS-Strom wird
abgeräumt, bevor der nächste anläuft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:47:31 +02:00
ScarriffleandClaude Opus 5 66b963f2c5 Keine Emojis als Symbole — und der Rückfall tat das Gegenteil
Zwei Dinge.

Erstens: Symbole sind jetzt Vektoren, keine Schriftzeichen. Ein Lautsprecher
als Emoji wird auf jeder Plattform anders gezeichnet, trägt fremde Farben,
sitzt schief auf der Grundlinie und lässt sich weder in Strichstärke noch
Farbe anpassen — es sieht nach Platzhalter aus, weil es einer ist. Elf
Stellen umgestellt, die Pfade stehen an einer Stelle beisammen.

Zweitens, und das ist der Grund, warum nichts lief: Nennt der Server keine
eigene Adresse, fiel der Code auf "static=true" zurück — also auf die Datei,
wie sie auf der Platte liegt. Bei einem MKV mit DTS ist das für einen Browser
unlesbar, und er meldet bloß "Format nicht unterstützt". Der Rückfall tat
genau das Gegenteil dessen, was er sollte. Jetzt verlangt er master.m3u8,
also ausdrücklich einen HLS-Strom.

Damit so etwas nicht wieder im Dunkeln liegt, nennt die Fehleranzeige nun
auch, was der Server geantwortet hat und welche Adresse verwendet wurde —
der Zugangsschlüssel darin gekürzt.

Dazu: während des Ladens treten die Transportknöpfe zurück. Der Kreisel lag
vorher genau hinter dem größten von ihnen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:43:03 +02:00
ScarriffleandClaude Opus 5 1db70379d7 Ein Player, und Serien starten die nächste Folge
Zwei berechtigte Beschwerden auf einmal.

Das war kein Player, sondern das nackte <video controls> des Browsers in
einem Blatt — ein eingebettetes Vorschaufenster. Jetzt Vollbild mit
eigener Bedienung: Titel und Folge oben, Spulen und Anhalten in der
Mitte, Fortschritt mit Restzeit unten, dazu Lautstärke, Tempo und
Vollbild. Die Bedienung blendet nach drei Sekunden Ruhe aus und kommt
bei jeder Bewegung zurück; steht das Bild, bleibt sie stehen — sie
auszublenden, während niemand spielt, versteckt den einzigen Knopf, der
weiterhilft. Tastatur: Leertaste, Pfeile, Escape.

Dunkle Knöpfe statt hell durchscheinender: über einer sonnigen Szene
verschwanden helle vollständig. Und die Sekundenzahl steht als Text, statt
im Spulpfeil zu stecken — übereinander war beides unlesbar.

Und bei einer Serie "spielt man nicht ab" zu antworten war Unsinn. Ich
hatte den Knopf entfernt, statt ihn richtig zu machen. Jetzt heißt er
"Weiterschauen" und startet die Folge, bei der man stehengeblieben ist —
über dieselbe Frage, die auch die Startseite stellt. Gibt es keine, wird
es die erste.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:38:53 +02:00
ScarriffleandClaude Opus 5 ecfa454136 Das Web spielt jetzt ab — und sagt, wenn es nicht geht
Es fehlte der erste Schritt. Die Stream-Adresse wurde selbst
zusammengebaut, und damit fehlten PlaySessionId, die ausgehandelte
MediaSourceId und die Adresse, die der Server eigentlich liefert. Im
Swift-Code steht seit jeher, warum das nicht geht: "eine selbst
zusammengebaute Adresse wäre eine Vermutung darüber."

Jetzt vier Schritte wie in der App: PlaybackInfo mit Geräteprofil, die
Quelle aus der Antwort wählen, die Adresse des Servers nehmen, die
Sitzungskennung mitführen.

Vor allem aber sind Fehler nicht mehr unsichtbar. Vorher endete jeder
Fehlschlag in einem stummen schwarzen Kasten — ein 401 sah aus wie ein
404 und wie ein Netzproblem. Geprüft: Ablehnung, fehlende Quelle und
nicht abspielbares Format nennen jetzt alle drei Ross und Reiter.

Serien und Staffeln haben keinen Abspielknopf mehr. Sie sind Ordner;
/Videos/{serienId} zeigt auf nichts. Wer über eine Serienkachel getestet
hat, konnte gar nichts starten.

Dazu Fortschrittsmeldung an /Sessions/Playing — ohne die erscheint der
Titel auf dem Server nie als "läuft gerade", und "Weiterschauen" bliebe
im Web für immer leer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:29:17 +02:00
ScarriffleandClaude Opus 5 14e1fdbe3f Web: Reihen ordnen und ausblenden
Phase 3 des Abgleichs. Dieselbe Rechnung wie Arrangement in der App,
samt ihrer wichtigsten Eigenschaft: unbekannte Reihen landen am Ende,
statt zu verschwinden. Sie zu verwerfen wäre die einfachere Regel — und
der Grund dafür, dass eine neu angelegte Mediathek nie aufgetaucht wäre.

Ausgeblendete stehen in den Einstellungen weiter unten in der Liste,
durchgestrichen. Ohne das könnte man sie nie wieder einblenden.

Knöpfe statt Ziehen: auf dem Zeigegerät ist Ziehen schöner, auf einem
Trackpad fummelig und mit der Tastatur gar nicht zu machen. Zwei Pfeile
können alle drei.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 17:12:11 +02:00