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>
This commit is contained in:
Scarriffle
2026-10-04 12:11:06 +02:00
co-authored by Claude Opus 5
parent 8b67a7ec88
commit 637e95fbb1
5 changed files with 212 additions and 21 deletions
+34 -13
View File
@@ -20,10 +20,13 @@ import { icon } from '../icons';
*
* `geaendert` wird gerufen, wenn sich die Mediathekenliste geändert hat; die
* Seitenleiste steht auf ihr und muss dann neu gezeichnet werden. */
export function verwaltung(geaendert: () => void = () => {}): HTMLElement {
export function verwaltung(
geaendert: () => void = () => {},
signal: AbortSignal = new AbortController().signal,
): HTMLElement {
const wurzel = document.createElement('div');
wurzel.append(dashboard(), vermisstenbereich(geaendert), bibliotheksbereich(geaendert),
benutzerbereich(), serverbereich(geaendert));
wurzel.append(dashboard(signal), vermisstenbereich(geaendert),
bibliotheksbereich(geaendert, signal), benutzerbereich(), serverbereich(geaendert));
return wurzel;
}
@@ -51,7 +54,7 @@ function abschnitt(titel: string, knoepfe: HTMLElement[] = []): HTMLElement {
* **Fragt alle fünf Sekunden nach.** Ein dauerhaft offener Kanal wäre
* sparsamer, aber für eine Handvoll Zeilen auf einer Seite, die man selten
* offen hat, ist er der grössere Aufwand — auf beiden Seiten. */
function dashboard(): HTMLElement {
function dashboard(signal: AbortSignal): HTMLElement {
const bereich = document.createElement('section');
bereich.appendChild(abschnitt('Wird abgespielt'));
@@ -94,6 +97,10 @@ function dashboard(): HTMLElement {
liste.appendChild(fehlerzeile((fehler as Error).message));
return; // Nicht weiter nachfragen, wenn es scheitert.
}
// **Nur weitermachen, solange die Ansicht hängt.** Ohne diese Zeile
// legte jeder Besuch der Verwaltung einen weiteren Fünf-Sekunden-Takt
// an, und nach zehn Wechseln liefen zehn davon nebeneinander.
if (signal.aborted) return;
timer = window.setTimeout(() => void laden(), 5000);
}
@@ -229,7 +236,7 @@ function vermisstenbereich(geaendert: () => void): HTMLElement {
/* ---------- Bibliotheken ---------- */
function bibliotheksbereich(geaendert: () => void): HTMLElement {
function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLElement {
const bereich = document.createElement('section');
const anlegen = knopf('Mediathek hinzufügen', 'btn btn-primary');
@@ -264,7 +271,10 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
void laden();
async function laden(): Promise<void> {
liste.innerHTML = '<div class="spinner"></div>';
// **Der Spinner gehört an den ersten Aufbau, nicht an jedes Auffrischen.**
// Sonst leert sich die Liste sichtbar, bevor dasselbe wieder erscheint —
// und bei einem Takt im Hintergrund blinkt sie dauernd.
if (liste.childElementCount === 0) liste.innerHTML = '<div class="spinner"></div>';
try {
const alle = await bibliotheken();
liste.innerHTML = '';
@@ -302,7 +312,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
const los = knopf('Scannen', 'btn');
los.onclick = async () => {
los.disabled = true;
try { await scanStarten(b.id); await verfolgen(); }
try { await scanStarten(b.id); await verfolgen(true); }
catch (fehler) { stand.textContent = (fehler as Error).message; }
los.disabled = false;
};
@@ -316,7 +326,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
+ 'nicht über eine neue Suche. Zuordnungen von Hand bleiben damit stehen. '
+ 'Bei vielen Titeln dauert es.',
bestaetigen: 'Auffrischen',
tun: async () => { await metadatenAuffrischen(b.id); void verfolgen(); },
tun: async () => { await metadatenAuffrischen(b.id); void verfolgen(true); },
});
const umbenennen = knopf('Umbenennen', 'btn');
@@ -341,8 +351,16 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
});
/** Fragt nach, bis der Lauf durch ist — er läuft im Hintergrund. */
async function verfolgen(): Promise<void> {
/// Verfolgt einen Lauf bis zum Ende.
///
/// **`melden` entscheidet, ob am Schluss `geaendert()` gerufen wird.**
/// Beim Aufbau der Seite ist es `false`: ein längst beendeter Lauf meldete
/// sonst „fertig", liess neu zeichnen, und der Neuaufbau verfolgte wieder
/// denselben beendeten Lauf — eine Endlosschleife, die sich als vier
/// dauernd drehende Spinner zeigte.
async function verfolgen(melden: boolean): Promise<void> {
for (let i = 0; i < 900; i++) {
if (signal.aborted) return;
const s = await scanstand(b.id);
if (s.state === 'running') stand.textContent = 'Wird durchsucht…';
else if (s.state === 'enriching') stand.textContent = 'Metadaten werden geholt…';
@@ -353,9 +371,9 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
if (s.metadata?.skipped) teile.push('Metadaten übersprungen');
else if (s.metadata) teile.push(`${s.metadata.moviesMatched + s.metadata.seriesMatched} zugeordnet`);
if (s.metadata?.error) teile.push(s.metadata.error);
if (s.missing) teile.push(`${s.missing} nicht mehr gefunden`);
stand.textContent = teile.join(' · ');
await laden();
geaendert();
if (melden) { await laden(); geaendert(); }
return;
}
await new Promise(r => setTimeout(r, 1000));
@@ -366,7 +384,7 @@ function bibliotheksbereich(geaendert: () => void): HTMLElement {
knoepfe.className = 'knopfreihe';
knoepfe.append(los, auffrischen, umbenennen, entfernen);
d.append(knoepfe, stand);
void verfolgen();
void verfolgen(false);
return d;
}
@@ -404,7 +422,10 @@ function benutzerbereich(): HTMLElement {
void laden();
async function laden(): Promise<void> {
liste.innerHTML = '<div class="spinner"></div>';
// **Der Spinner gehört an den ersten Aufbau, nicht an jedes Auffrischen.**
// Sonst leert sich die Liste sichtbar, bevor dasselbe wieder erscheint —
// und bei einem Takt im Hintergrund blinkt sie dauernd.
if (liste.childElementCount === 0) liste.innerHTML = '<div class="spinner"></div>';
try {
const alle = await benutzer();
liste.innerHTML = '';
+8 -1
View File
@@ -2,6 +2,7 @@ import { inhalt, genresVon, empfehlungen as holeEmpfehlungen, type Bibliothek, t
import { leer, fehlerzeile } from '../util';
import { icons } from '../icons';
import { kachel, alsKachel, reihe } from '../card';
import { beiRueckkehr } from '../ansichten';
type Art = 'filme' | 'serien';
type Reiter = 'empfehlungen' | 'mediathek' | 'kategorien';
@@ -181,8 +182,14 @@ export function mediathek(
void laden();
// Beim Zurückkommen aus einer anderen Ansicht leise nachladen — die alte
// Liste bleibt stehen, bis die neue da ist.
beiRueckkehr(wurzel, () => void laden());
async function laden(): Promise<void> {
gitter.innerHTML = '<div class="spinner"></div>';
// Der Spinner nur beim ersten Mal; beim Nachladen bleibt stehen, was da
// ist, bis es ersetzt wird.
if (gitter.childElementCount === 0) gitter.innerHTML = '<div class="spinner"></div>';
try {
const seite = await inhalt(bibliothek, { search: suche, sort: ordnung, genre, take: 200 });