Elf Abschnitte in einer Spalte waren zehn zu viel

Die Verwaltung war eine Seite mit 1786 Zeilen. Wer das Protokoll sehen
wollte, scrollte an Mediatheken, Prüfliste, Benutzern und Speicher
vorbei — und jede neue Einstellung hätte sie länger gemacht.

Jetzt hat sie eine eigene Leiste mit sieben Bereichen, jeder mit seiner
Adresse: `#/verwaltung/protokolle` lässt sich ablegen, und ein Neuladen
bleibt dort stehen. Die Leiste sieht aus wie die Hauptleiste daneben,
damit sie als Navigation gelesen wird und nicht als Knopfreihe; am
Telefon wird aus der Spalte ein Streifen zum Schieben.

**Gebaut wird nur, was offen ist.** Das ist nicht nur sparsamer: das
Abspielfenster fragt im Fünf-Sekunden-Takt nach, die Mediathekenliste
beobachtet laufende Scans. Gemessen: nach dem Wechsel in einen anderen
Bereich kommt zwölf Sekunden lang keine Anfrage mehr.

`admin.ts` ist in acht Dateien zerlegt — eine je Bereich, dazu `teile.ts`
für die gemeinsame Überschrift und `index.ts` für Leiste und Weiche.

Zwei Dinge mussten dabei nachziehen:

**Die Markierung in der Hauptleiste.** Sie verglich stur zwei Segmente;
bei `#/verwaltung/metadaten` traf das auf keinen Eintrag, und
„Verwaltung" verlor seinen Balken. Jetzt gilt auch ein Segment.

**„5 Titel warten — ansehen".** Der Knopf sprang mit `scrollIntoView` zur
Prüfliste. Die steht nicht mehr im selben Dokument — er wechselt jetzt
den Bereich. Die Hervorhebung danach entfällt damit, und mit ihr ihr CSS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Scarriffle
2026-10-06 16:39:50 +02:00
co-authored by Claude Opus 5
parent 94c6604e8b
commit 1e4a231d1b
13 changed files with 1979 additions and 1796 deletions
+218
View File
@@ -0,0 +1,218 @@
import {
sprachen, spracheSetzen, uebersetzungenHolen, meineSprache, metadatenEinstellung,
metadatenSetzen, sprachverteilung, abweichendeAuffrischen,
} from '../../api';
import { esc, knopf, fehlerzeile, meldung } from '../../util';
import { dialog } from '../../dialog';
import { icon } from '../../icons';
import { abschnitt } from './teile';
/** Sprache und Herkunft der Metadaten. */
/* ---------- Server ---------- */
/** Sprache — eigener Abschnitt, ganz oben.
*
* **Warum nicht in der Serverliste.** Dort stand sie, zwischen „Name",
* „Metadaten", „Doppelte Einträge" und „Fassung". Gesucht wurde sie
* trotzdem, gefunden nicht — und das ist die Antwort: eine Einstellung, die
* man sucht und nicht findet, steht am falschen Platz, egal wie gut sie
* beschriftet ist.
*
* **Oben, weil sie alles andere färbt.** In dieser Sprache werden Metadaten
* geholt und gespeichert; wer sie spät umstellt, frischt den ganzen Bestand
* noch einmal auf.
*/
export function sprachbereich(): HTMLElement {
const bereich = document.createElement('section');
bereich.appendChild(abschnitt('Sprache'));
const liste = document.createElement('div');
liste.className = 'einstellungen';
bereich.appendChild(liste);
void (async () => {
try {
const stand = await metadatenEinstellung();
const eigeneSprache = meineSprache();
liste.innerHTML = '';
// **Die Sprache des Servers als eigene Zeile.**
//
// Sie 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.
liste.appendChild(zeile(
'Sprache des Servers',
sprachen.find(x => x.wert === stand.language)?.text ?? stand.language,
'In dieser Sprache werden Titel und Beschreibungen geholt, und sie '
+ 'bestimmt die Altersfreigabe. Gilt für alle. Vorhandene Titel behalten '
+ 'ihre alte Sprache — dafür gibt es bei jeder Mediathek „Metadaten '
+ 'auffrischen".',
() => dialog({
titel: 'Sprache des Servers',
text: 'Die Umstellung wirkt auf neue Funde sofort. Für den vorhandenen '
+ 'Bestand drückst du danach bei jeder Mediathek „Metadaten auffrischen" '
+ '— sonst behalten die Titel ihre alte Sprache.',
felder: [{
name: 'sprache', beschriftung: 'Sprache', auswahl: sprachen, wert: stand.language,
}],
bestaetigen: 'Übernehmen',
tun: async wert => {
await metadatenSetzen({ language: wert('sprache') });
location.reload();
},
})));
// **Die eigene Sprache, getrennt von der des Servers.** Die obere
// bestimmt, was geholt wird; diese nur, was man selbst zu sehen bekommt.
// **Was tatsächlich vorliegt — nicht was eingestellt ist.**
//
// Der eigentliche Fehler war nicht, dass Titel englisch blieben, sondern
// dass sich nicht feststellen liess, ob sie es sind: nirgends stand, in
// welcher Sprache ein Titel geholt wurde. Hier steht es, als Zahl.
void sprachverteilung().then(v => {
const benennen = (k: string | null) => k === null
? 'unbekannt (vor dieser Fassung geholt)'
: sprachen.find(x => x.wert === k)?.text ?? k;
const zeileBestand = zeile(
'Bestand',
v.anteile.length === 0
? 'Noch nichts zugeordnet'
: v.anteile.map(a => `${a.anzahl} × ${benennen(a.sprache)}`).join(' · '),
v.abweichend === 0
? 'Alles liegt in der Serversprache vor.'
: `${v.abweichend} Titel liegen in einer anderen Sprache vor. Das Auffrischen `
+ 'holt genau diese neu — nicht den ganzen Bestand.');
if (v.abweichend > 0) {
const los = knopf(`${v.abweichend} auffrischen`, 'btn btn-primary');
los.onclick = async () => {
los.disabled = true;
try {
await abweichendeAuffrischen();
los.textContent = 'Läuft…';
} catch (fehler) {
zeileBestand.appendChild(fehlerzeile(meldung(fehler)));
los.disabled = false;
}
};
zeileBestand.querySelector('.knopfreihe')?.appendChild(los);
}
// Direkt unter die Serversprache, denn dort wird die Frage gestellt.
liste.insertBefore(zeileBestand, liste.children[1] ?? null);
}).catch(() => { /* Ohne Verteilung fehlt nur die Zeile. */ });
// **Weicht sie ab, steht das deutlich dabei.**
//
// Eine eigene Sprache ä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 „Metadaten
// auffrischen" scheint wirkungslos. Genau in diese Falle läuft man,
// ohne je zu erfahren, warum.
const weichtAb = eigeneSprache !== null && eigeneSprache !== stand.language;
const serversprache = sprachen.find(x => x.wert === stand.language)?.text ?? stand.language;
liste.appendChild(zeile(
weichtAb ? 'Meine Sprache — weicht ab' : 'Meine Sprache',
eigeneSprache ? (sprachen.find(x => x.wert === eigeneSprache)?.text ?? eigeneSprache)
: 'Wie der Server',
weichtAb
? `Du siehst alles auf ${sprachen.find(x => x.wert === eigeneSprache)?.text ?? eigeneSprache}, `
+ `obwohl der Server auf ${serversprache} holt. Titel ohne Übersetzung bleiben `
+ `auf ${serversprache}. Auf „Wie der Server" stellen, um zu sehen, was wirklich `
+ 'gespeichert ist.'
: 'Gilt nur für dich. Was nicht übersetzt vorliegt, erscheint weiterhin in '
+ 'der Sprache des Servers.',
() => dialog({
titel: 'Meine Sprache',
text: 'Die Metadaten liegen in der Sprache des Servers vor. Eine andere '
+ 'zu wählen heisst: du siehst, was übersetzt vorliegt — und für den '
+ 'Rest die Serversprache. Ein Administrator kann die Übersetzungen '
+ 'unten für alle Titel auf einmal holen.',
felder: [{
name: 'sprache', beschriftung: 'Sprache',
auswahl: [{ wert: '', text: 'Wie der Server' }, ...sprachen],
wert: eigeneSprache ?? '',
}],
bestaetigen: 'Übernehmen',
tun: async wert => {
const neu = wert('sprache');
await spracheSetzen(neu || null);
location.reload();
},
})));
liste.appendChild(zeile(
'Übersetzungen holen',
`Füllt eine zweite Sprache neben ${serversprache} auf`,
`Der Bestand liegt auf ${serversprache} vor. Das hier legt zusätzlich `
+ 'eine zweite Fassung an, für Benutzer, die eine andere Sprache gewählt '
+ 'haben — am Bestand selbst ändert es nichts. Kostet je Titel eine '
+ 'Anfrage an TMDB.',
() => dialog({
titel: 'Übersetzungen holen',
text: `Die Titel des Servers bleiben auf ${serversprache}. Geholt wird `
+ 'eine zusätzliche Fassung, die nur Benutzer mit genau dieser Sprache '
+ 'zu sehen bekommen.',
felder: [{
name: 'sprache', beschriftung: 'Für welche Sprache',
// **Nicht stur en-US.** Stand dort „Englisch", während der Server
// auf Deutsch steht, las sich das wie eine Aussage über den
// Server — und führte zu der Vermutung, die Spracheinstellung
// wirke nicht. Vorgewählt wird die eigene Sprache, sonst die
// erste, die nicht schon die des Servers ist.
auswahl: sprachen,
wert: eigeneSprache
?? sprachen.find(x => x.wert !== stand.language)?.wert
?? stand.language,
}],
bestaetigen: 'Holen',
tun: async wert => {
const bericht = await uebersetzungenHolen(wert('sprache'));
alert(`${bericht.gesamt} Übersetzungen geholt, ${bericht.fehlend} ohne Treffer.`);
},
})));
} catch (fehler) {
liste.innerHTML = '';
liste.appendChild(fehlerzeile(meldung(fehler)));
}
})();
return bereich;
function zeile(
name: string, wert: string, hinweis: string | null, bearbeiten?: () => void,
): HTMLElement {
const d = document.createElement('div');
d.className = 'einstellung';
const oben = document.createElement('div');
oben.className = 'knopfreihe';
const text = document.createElement('div');
text.className = 'zeile';
text.style.flex = '1';
text.innerHTML = `<strong>${esc(name)}</strong><span class="winzig">${esc(wert)}</span>`;
oben.appendChild(text);
if (bearbeiten) {
const b = knopf('Ändern', 'btn');
b.prepend(icon('stift'));
b.onclick = bearbeiten;
oben.appendChild(b);
}
d.appendChild(oben);
if (hinweis) {
const h = document.createElement('div');
h.className = 'hinweis';
h.textContent = hinweis;
d.appendChild(h);
}
return d;
}
}