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
+102
View File
@@ -0,0 +1,102 @@
import { type Bibliothek } from '../../api';
import { icon, type IconName } from '../../icons';
import { dashboard, serverbereich, speicherbereich } from './uebersicht';
import { bibliotheksbereich, vermisstenbereich } from './mediatheken';
import { pruefbereich } from './pruefung';
import { ohneZuordnungBereich, unzugeordnetbereich } from './zuordnung';
import { sprachbereich } from './metadaten';
import { benutzerbereich } from './benutzer';
import { protokollbereich } from './protokolle';
/** Welcher Teil der Verwaltung offen ist. */
export type Bereich =
| 'uebersicht' | 'mediatheken' | 'pruefung' | 'zuordnung'
| 'metadaten' | 'benutzer' | 'protokolle';
const BEREICHE: { wert: Bereich; text: string; bild: IconName }[] = [
{ wert: 'uebersicht', text: 'Übersicht', bild: 'verwaltung' },
{ wert: 'mediatheken', text: 'Mediatheken', bild: 'filme' },
{ wert: 'pruefung', text: 'Zu prüfen', bild: 'hakenKreis' },
{ wert: 'zuordnung', text: 'Zuordnung', bild: 'suche' },
{ wert: 'metadaten', text: 'Metadaten', bild: 'stift' },
{ wert: 'benutzer', text: 'Benutzer', bild: 'person' },
{ wert: 'protokolle', text: 'Protokolle', bild: 'protokoll' },
];
/** Macht aus einem Adressteil einen Bereich — Unbekanntes wird zur Übersicht. */
export function alsBereich(wert: string | undefined): Bereich {
return BEREICHE.some(b => b.wert === wert) ? wert as Bereich : 'uebersicht';
}
/** Die Verwaltung.
*
* **Was etwas anlegt oder ändert, steht in einem Dialog**, nicht als Block
* in der Seite. Ein Formular, das immer dasteht, nimmt Platz für etwas weg,
* das man einmal im Monat braucht — und sieht aus wie eine Eingabe, die
* noch aussteht.
*
* **Warum eine zweite Leiste.** Vorher hingen elf Abschnitte untereinander in
* einer Seite — 1786 Zeilen, und jede neue Einstellung machte sie länger. Wer
* das Protokoll sehen wollte, scrollte an den Mediatheken, der Prüfliste und
* dem Speicher vorbei. Jetzt hat jeder Bereich seine eigene Adresse: man kann
* ihn als Lesezeichen ablegen, und ein Neuladen bleibt dort stehen.
*
* **Nur der offene Bereich wird gebaut.** Das ist nicht nur sparsamer — das
* Abspielfenster der Übersicht fragt im Takt von fünf Sekunden nach, und die
* Mediathekenliste beobachtet laufende Scans. Was nicht dasteht, fragt auch
* nicht.
*
* `geaendert` wird gerufen, wenn sich die Mediathekenliste geändert hat; die
* Seitenleiste steht auf ihr und muss dann neu gezeichnet werden. */
export function verwaltung(
welcher: Bereich = 'uebersicht',
geaendert: (bekannt?: Bibliothek[]) => void = () => {},
signal: AbortSignal = new AbortController().signal,
): HTMLElement {
const wurzel = document.createElement('div');
wurzel.className = 'verwaltung';
const leiste = document.createElement('nav');
leiste.className = 'verw-leiste';
leiste.setAttribute('aria-label', 'Bereiche der Verwaltung');
BEREICHE.forEach(b => {
const k = document.createElement('button');
k.type = 'button';
k.className = 'verw-eintrag' + (b.wert === welcher ? ' check' : '');
k.appendChild(icon(b.bild));
k.append(document.createTextNode(b.text));
if (b.wert === welcher) k.setAttribute('aria-current', 'page');
k.onclick = () => { location.hash = `#/verwaltung/${b.wert}`; };
leiste.appendChild(k);
});
const buehne = document.createElement('div');
buehne.className = 'verw-buehne';
inhalt().forEach(a => buehne.appendChild(a));
wurzel.append(leiste, buehne);
return wurzel;
function inhalt(): HTMLElement[] {
switch (welcher) {
case 'mediatheken':
return [bibliotheksbereich(geaendert, signal), vermisstenbereich(geaendert)];
case 'pruefung': {
// **Die Prüfliste hört auf sich selbst.** Wird etwas freigegeben,
// ändert sich der Bestand — und die Liste darüber muss nachziehen.
const p = pruefbereich(() => { geaendert(); p.auffrischen(); });
return [p.wurzel];
}
case 'zuordnung':
return [ohneZuordnungBereich(), unzugeordnetbereich()];
case 'metadaten':
return [sprachbereich()];
case 'benutzer':
return [benutzerbereich()];
case 'protokolle':
return [protokollbereich()];
default:
return [dashboard(signal), serverbereich(geaendert), speicherbereich()];
}
}
}