Vier Fehler, und bei zweien log der Kommentar daneben

**Der Einstellungsknopf lag hinter der Abspielleiste.** Gemessen: `.pl`
steht auf Stufe 150, `#chrome` auf gar keiner, und die kleine Leiste spannte
über die ganze Breite — genau über die unteren 72 Bildpunkte der
Seitenleiste, wo „Einstellungen" klebt. Weil der Spieler bei jedem Wechsel
der Adresse minimiert, stand sie zuverlässig im Weg, sobald man dorthin
wollte.

Die Leiste beginnt jetzt hinter der Seitenleiste. Nicht über die Stufen
gelöst, und das ist der Punkt: `#chrome` höher zu setzen hiesse, die
Seitenleiste läge auch über dem grossen Spieler — es ist dieselbe Hülle.
Dass diese Leiste Platz wegnimmt statt sich darüberzulegen, stand für
Inhalt, Hinweise und Auswahlleiste ohnehin schon da; die Seitenleiste war
die vergessene vierte. Gemessen, 1400 Bildpunkte breit: Leiste ab 240,
Knopf bei 0, keine Überschneidung, und ein Treffertest in der Knopfmitte
trifft den Knopf.

**Ein Klick auf eine Kategorie kam nie an.** Dort sprang der Code in die
Mediathek und suchte danach `.filterzeile select`, um den Wert zu setzen.
Die Filterzeile hat kein `<select>` — `auswahl()` baut einen Knopf, mit
Absicht und begründet. Der Zweig lief also nie, und man landete in der
ungefilterten Mediathek. Der Kommentar daneben behauptete das Gegenteil.

Das Genre steht jetzt in der Adresse: `#/m/{id}/mediathek/Horror`. Das
löst zugleich das zweite Problem — die Mediathek wird unter ihrem Weg
gemerkt, und stünde das Genre nur im Kopf, zeigte ein zweiter Besuch die
Liste des ersten. Geprüft: Klick setzt den Weg und die Pille, Neuladen
behält beides, Zurück führt auf die Kategorien, Vorwärts wieder hinein.

**Zurückgezogene Einladungen blieben für immer stehen.** Der Knopfblock
steckte ganz in `if (!e.revokedAt)`, eine erledigte Zeile hatte also gar
keinen Knopf. Die Serverbegründung — eine Zeile wegzuwerfen hiesse,
hinterher nicht mehr zu wissen, dass es den Link je gab — bleibt richtig,
sie gilt nur nicht ewig: eine Liste, aus der nichts verschwindet, wird
unlesbar, und dann sieht man die eine Zeile nicht mehr, um die es ging.

Deshalb kein umgedeutetes DELETE, sondern `?purge=true`, und das greift nur
bei einem Link, der ohnehin keine Tür mehr öffnet. Ein gültiger wird mit
einem deutschen Satz abgewiesen.

**Poster in der falschen Sprache.** „365 Tage" stand mit polnischem Plakat
da. Kein fehlender Anbieter: für den Schriftzug gab es die sprachgewichtete
Wahl längst, fürs Plakat wurde das blanke `poster_path` der Hauptantwort
genommen — bei TMDB das Bild der Originalsprache, wenn in der gewünschten
keines eingetragen ist. Die sortierte Liste lag im selben Abruf schon vor
und wurde ausschliesslich für den Schriftzug benutzt.

Plakat und Hintergrund nehmen jetzt dieselbe Rangfolge: eingestellte
Sprache, dann sprachlos, dann Englisch. Sprachlos vor Englisch ist keine
Verlegenheit — ein Bild ohne Sprachkennung trägt keinen Text, und ein
Plakat ohne Titel ist für einen deutschen Bestand besser als dasselbe mit
englischem. Im Bilderdialog stehen die Kandidaten jetzt ebenso sortiert.
Bestehende Titel ändern sich beim nächsten Auffrischen; von Hand gesetzte
Plakate bleiben unangetastet.

676 Tests grün, davon zehn neue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-08 17:46:46 +02:00
co-authored by Claude Opus 5
parent c6df9fdab0
commit 2809821c9b
9 changed files with 391 additions and 25 deletions
@@ -260,4 +260,75 @@ public class EinladungTests
Assert.That(leer.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
});
}
/// <summary>Eine zurückgezogene Einladung lässt sich endgültig entfernen.</summary>
///
/// <remarks>
/// <b>Warum das überhaupt nötig ist.</b> Zurückziehen behält die Spur
/// absichtlich — man will wissen, dass es den Link gab, wenn jemand
/// Unerwartetes im Haushalt auftaucht. Nur verlässt die Zeile die Liste
/// dann nie, und eine Liste, aus der nichts verschwindet, wird
/// irgendwann unlesbar. Weggeworfen wird deshalb nur, was ohnehin keine
/// Tür mehr öffnet.
/// </remarks>
[Test]
public async Task Eine_zurueckgezogene_Einladung_laesst_sich_entfernen()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var einladung = await Einladen(alsAdmin);
(await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}")).EnsureSuccessStatusCode();
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
var liste = await alsAdmin.GetFromJsonAsync<List<Einladungseintrag>>("/api/v1/invites");
Assert.Multiple(() =>
{
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
Assert.That(liste, Is.Empty, "die Zeile muss die Liste verlassen");
});
}
/// <summary>Ein gültiger Link lässt sich nicht wegwischen.</summary>
///
/// <remarks>
/// Sonst wäre „Entfernen" ein stilles Zurückziehen mit Gedächtnisverlust:
/// der Link gälte weiter, stünde aber nirgends mehr. Erst zurückziehen,
/// dann entfernen — ein Handgriff mehr, mit Absicht.
/// </remarks>
[Test]
public async Task Ein_gueltiger_Link_laesst_sich_nicht_entfernen()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var einladung = await Einladen(alsAdmin);
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
var liste = await alsAdmin.GetFromJsonAsync<List<Einladungseintrag>>("/api/v1/invites");
Assert.Multiple(() =>
{
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.BadRequest));
Assert.That(liste, Has.Count.EqualTo(1));
Assert.That(liste![0].Valid, Is.True, "und er gilt danach immer noch");
});
}
/// <summary>Ein aufgebrauchter Link darf weg, auch ohne Zurückziehen.</summary>
[Test]
public async Task Ein_aufgebrauchter_Link_laesst_sich_entfernen()
{
using var server = new FlaroServer();
var admin = await server.SetUpAndSignIn();
var alsAdmin = server.AlsAngemeldeter(admin.Token);
var einladung = await Einladen(alsAdmin, wieOft: 1);
(await Einloesen(server.OhneGedaechtnis(), einladung.Token, "gast")).EnsureSuccessStatusCode();
var weg = await alsAdmin.DeleteAsync($"/api/v1/invites/{einladung.Id}?purge=true");
Assert.That(weg.StatusCode, Is.EqualTo(HttpStatusCode.NoContent));
}
}