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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab8a66d898
commit
ac04901ccc
+304
-102
@@ -4,14 +4,15 @@ import {
|
||||
vermisste, vermisstesEntfernen, type VermissteDatei,
|
||||
protokolltage, protokollAdresse, sprachen, spracheSetzen, uebersetzungenHolen,
|
||||
meineSprache, zuPruefen, veroeffentlichen, zurueckziehen, pruefungSetzen,
|
||||
mediathekFreigeben, reihenfolgeSetzen,
|
||||
mediathekFreigeben, reihenfolgeSetzen, mediathekZuruecksetzen,
|
||||
type Pruefeintrag,
|
||||
benutzer, benutzerAnlegen, metadatenEinstellung, metadatenSetzen,
|
||||
servernameSetzen, systemauskunft,
|
||||
laufendeWiedergaben, serverzustand, bildAdresse,
|
||||
type Bibliothek, type Wiedergabe,
|
||||
speicherorte, bildspeicherLeeren,
|
||||
type Bibliothek, type Wiedergabe, ApiFehler,
|
||||
} from '../api';
|
||||
import { esc, dauer, knopf, fehlerzeile, leer } from '../util';
|
||||
import { esc, dauer, groesse, knopf, fehlerzeile, leer, meldung } from '../util';
|
||||
import { dialog, nachfragen } from '../dialog';
|
||||
import { icon, icons } from '../icons';
|
||||
|
||||
@@ -29,9 +30,9 @@ export function verwaltung(
|
||||
signal: AbortSignal = new AbortController().signal,
|
||||
): HTMLElement {
|
||||
const wurzel = document.createElement('div');
|
||||
wurzel.append(dashboard(signal), pruefbereich(geaendert), vermisstenbereich(geaendert),
|
||||
bibliotheksbereich(geaendert, signal), benutzerbereich(), serverbereich(geaendert),
|
||||
protokollbereich());
|
||||
wurzel.append(dashboard(signal), sprachbereich(), pruefbereich(geaendert),
|
||||
vermisstenbereich(geaendert), bibliotheksbereich(geaendert, signal), benutzerbereich(),
|
||||
serverbereich(geaendert), speicherbereich(), protokollbereich());
|
||||
return wurzel;
|
||||
}
|
||||
|
||||
@@ -99,7 +100,7 @@ function dashboard(signal: AbortSignal): HTMLElement {
|
||||
zustand.textContent = teile.join(' · ');
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile((fehler as Error).message));
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
return; // Nicht weiter nachfragen, wenn es scheitert.
|
||||
}
|
||||
// **Nur weitermachen, solange die Ansicht hängt.** Ohne diese Zeile
|
||||
@@ -238,7 +239,7 @@ function pruefbereich(geaendert: () => void): HTMLElement {
|
||||
await laden();
|
||||
geaendert();
|
||||
} catch (fehler) {
|
||||
liste.prepend(fehlerzeile((fehler as Error).message));
|
||||
liste.prepend(fehlerzeile(meldung(fehler)));
|
||||
} finally {
|
||||
freigeben.disabled = false;
|
||||
planen.disabled = false;
|
||||
@@ -316,7 +317,7 @@ function pruefbereich(geaendert: () => void): HTMLElement {
|
||||
nur.onclick = async () => {
|
||||
nur.disabled = true;
|
||||
try { await veroeffentlichen(e.ids); await laden(); geaendert(); }
|
||||
catch (fehler) { d.appendChild(fehlerzeile((fehler as Error).message)); nur.disabled = false; }
|
||||
catch (fehler) { d.appendChild(fehlerzeile(meldung(fehler))); nur.disabled = false; }
|
||||
};
|
||||
|
||||
const oben = document.createElement('div');
|
||||
@@ -328,7 +329,7 @@ function pruefbereich(geaendert: () => void): HTMLElement {
|
||||
zurueck.onclick = async () => {
|
||||
zurueck.disabled = true;
|
||||
try { await zurueckziehen(e.ids); await laden(); }
|
||||
catch (fehler) { d.appendChild(fehlerzeile((fehler as Error).message)); zurueck.disabled = false; }
|
||||
catch (fehler) { d.appendChild(fehlerzeile(meldung(fehler))); zurueck.disabled = false; }
|
||||
};
|
||||
oben.appendChild(zurueck);
|
||||
}
|
||||
@@ -375,6 +376,11 @@ function protokollbereich(): HTMLElement {
|
||||
anzeige.className = 'protokoll hidden';
|
||||
bereich.appendChild(anzeige);
|
||||
|
||||
// **Blättern statt alles auf einmal.** Die ersten 800 Zeilen beantworten
|
||||
// fast jede Frage; wer weiter zurück muss, verdoppelt mit diesem Knopf.
|
||||
const mehr = knopf('Mehr laden', 'btn hidden');
|
||||
bereich.appendChild(mehr);
|
||||
|
||||
void (async () => {
|
||||
let tage;
|
||||
try { tage = await protokolltage(); } catch { bereich.classList.add('hidden'); return; }
|
||||
@@ -416,25 +422,44 @@ function protokollbereich(): HTMLElement {
|
||||
});
|
||||
})();
|
||||
|
||||
async function zeigen(tag: string, nurFehler: boolean): Promise<void> {
|
||||
/** Wie viele Zeilen zuletzt geholt wurden — der Knopf „Mehr" verdoppelt. */
|
||||
let gezeigt = 0;
|
||||
|
||||
async function zeigen(tag: string, nurFehler: boolean, zeilen = 0): Promise<void> {
|
||||
// **Nur das Ende holen, und davon anfangs wenig.** Vorher kam die ganze
|
||||
// Datei, und davon wurden 95 % weggeworfen — bei zwei Megabyte über eine
|
||||
// wackelige Verbindung endete das in „Failed to fetch". Bei der
|
||||
// Fehlersuche mehr, weil davon die meisten weggefiltert werden.
|
||||
gezeigt = zeilen > 0 ? zeilen : (nurFehler ? 5000 : 800);
|
||||
|
||||
anzeige.classList.remove('hidden');
|
||||
anzeige.textContent = 'Wird geladen…';
|
||||
mehr.classList.add('hidden');
|
||||
|
||||
try {
|
||||
const antwort = await fetch(protokollAdresse(tag));
|
||||
if (!antwort.ok) throw new Error(`${antwort.status}`);
|
||||
const text = await antwort.text();
|
||||
const zeilen = text.split('\n');
|
||||
// **Das Ende ist interessanter als der Anfang.** Ein Protokolltag kann
|
||||
// zehntausend Zeilen haben; wer etwas sucht, sucht meist das Letzte.
|
||||
const antwort = await fetch(protokollAdresse(tag, gezeigt));
|
||||
if (!antwort.ok) throw new ApiFehler(antwort.status, `${antwort.status}`);
|
||||
|
||||
const roh = (await antwort.text()).split('\n');
|
||||
const gefiltert = nurFehler
|
||||
? zeilen.filter(z => z.includes('FEHLER') || z.includes('WARNUNG'))
|
||||
: zeilen;
|
||||
anzeige.textContent = gefiltert.length === 0
|
||||
? (nurFehler ? 'Keine Fehler an diesem Tag.' : 'Leer.')
|
||||
: gefiltert.slice(-800).join('\n');
|
||||
? roh.filter(z => z.includes('FEHLER') || z.includes('WARNUNG'))
|
||||
: roh;
|
||||
|
||||
anzeige.textContent = gefiltert.join('\n').trim().length === 0
|
||||
? (nurFehler ? 'Keine Fehler in den letzten Zeilen.' : 'Leer.')
|
||||
: gefiltert.join('\n');
|
||||
anzeige.scrollTop = anzeige.scrollHeight;
|
||||
|
||||
// **„Mehr" nur, solange es mehr geben kann.** Kamen weniger Zeilen
|
||||
// zurück als verlangt, ist die Datei zu Ende — ein Knopf, der dann
|
||||
// nichts tut, ist schlechter als keiner.
|
||||
if (roh.length >= gezeigt && gezeigt < 20000) {
|
||||
mehr.classList.remove('hidden');
|
||||
mehr.textContent = `Weitere ${Math.min(gezeigt, 20000 - gezeigt)} Zeilen laden`;
|
||||
mehr.onclick = () => void zeigen(tag, nurFehler, Math.min(gezeigt * 2, 20000));
|
||||
}
|
||||
} catch (fehler) {
|
||||
anzeige.textContent = `Konnte nicht gelesen werden: ${(fehler as Error).message}`;
|
||||
anzeige.textContent = `Konnte nicht gelesen werden: ${meldung(fehler)}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +525,7 @@ function vermisstenbereich(geaendert: () => void): HTMLElement {
|
||||
weg.onclick = async () => {
|
||||
weg.disabled = true;
|
||||
try { await vermisstesEntfernen(v.fileId); await laden(); geaendert(); }
|
||||
catch (fehler) { d.appendChild(fehlerzeile((fehler as Error).message)); weg.disabled = false; }
|
||||
catch (fehler) { d.appendChild(fehlerzeile(meldung(fehler))); weg.disabled = false; }
|
||||
};
|
||||
oben.appendChild(weg);
|
||||
d.appendChild(oben);
|
||||
@@ -592,7 +617,7 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
alle.forEach((b, i) => liste.appendChild(block(b, i)));
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile((fehler as Error).message));
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,7 +651,7 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
los.onclick = async () => {
|
||||
los.disabled = true;
|
||||
try { await scanStarten(b.id); await verfolgen(true); }
|
||||
catch (fehler) { stand.textContent = (fehler as Error).message; }
|
||||
catch (fehler) { stand.textContent = meldung(fehler); }
|
||||
los.disabled = false;
|
||||
};
|
||||
|
||||
@@ -654,6 +679,25 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
},
|
||||
});
|
||||
|
||||
// **Zurücksetzen steht neben Entfernen, weil es fast dasselbe tut** — nur
|
||||
// ohne Namen, Pfad und Einstellungen mitzunehmen. Wer eine Mediathek
|
||||
// löscht und neu anlegt, um sie sauber zu bekommen, tippt alles noch
|
||||
// einmal; genau das war bisher der einzige Weg.
|
||||
const zuruecksetzen = knopf('Zurücksetzen', 'btn');
|
||||
zuruecksetzen.onclick = () => nachfragen({
|
||||
titel: `„${b.name}" zurücksetzen?`,
|
||||
text: 'Alle erfassten Titel, Zuordnungen und der Sehfortschritt dieser '
|
||||
+ 'Mediathek werden verworfen; Name, Pfad und Einstellungen bleiben. '
|
||||
+ 'Die Mediendateien auf der Platte bleiben unangetastet. Danach einmal '
|
||||
+ 'scannen, und der Bestand entsteht neu.',
|
||||
bestaetigen: 'Zurücksetzen',
|
||||
tun: async () => {
|
||||
await mediathekZuruecksetzen(b.id);
|
||||
await laden();
|
||||
geaendert();
|
||||
},
|
||||
});
|
||||
|
||||
const entfernen = knopf('Entfernen', 'btn');
|
||||
entfernen.onclick = () => nachfragen({
|
||||
titel: `„${b.name}" entfernen?`,
|
||||
@@ -687,7 +731,9 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
if (melden && i > 0 && i % 5 === 0) await laden();
|
||||
else if (s.state === 'finished') {
|
||||
const teile = [`${s.filesFound} Dateien`, `${s.added} neu`, `${s.removed} entfernt`];
|
||||
if (s.unrecognised) teile.push(`${s.unrecognised} ohne Folgennummer`);
|
||||
// „ohne Folgennummer" stimmte nicht mehr: gezählt wird jetzt auch,
|
||||
// was eine Nummer trägt, aber zu keiner Serie gehört.
|
||||
if (s.unrecognised) teile.push(`${s.unrecognised} nicht zugeordnet`);
|
||||
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);
|
||||
@@ -709,7 +755,7 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
await pruefungSetzen(b.id, !b.reviewNewItems);
|
||||
await laden();
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile((fehler as Error).message));
|
||||
d.appendChild(fehlerzeile(meldung(fehler)));
|
||||
pruefung.disabled = false;
|
||||
}
|
||||
};
|
||||
@@ -736,7 +782,8 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
|
||||
const knoepfe = document.createElement('div');
|
||||
knoepfe.className = 'knopfreihe';
|
||||
knoepfe.append(hoch, runter, los, auffrischen, pruefung, umbenennen, entfernen);
|
||||
knoepfe.append(
|
||||
hoch, runter, los, auffrischen, pruefung, umbenennen, zuruecksetzen, entfernen);
|
||||
|
||||
// **Nur wenn hier wirklich etwas liegt.** Ein Knopf, der nichts zu tun
|
||||
// hat, ist schlechter als keiner — und die Zahl daneben sagt, worum es
|
||||
@@ -753,7 +800,7 @@ function bibliotheksbereich(geaendert: () => void, signal: AbortSignal): HTMLEle
|
||||
await laden();
|
||||
geaendert();
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile((fehler as Error).message));
|
||||
d.appendChild(fehlerzeile(meldung(fehler)));
|
||||
freigeben.disabled = false;
|
||||
}
|
||||
};
|
||||
@@ -814,7 +861,7 @@ function benutzerbereich(): HTMLElement {
|
||||
});
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile((fehler as Error).message));
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -823,9 +870,21 @@ function benutzerbereich(): HTMLElement {
|
||||
|
||||
/* ---------- Server ---------- */
|
||||
|
||||
function serverbereich(geaendert: () => void): HTMLElement {
|
||||
/** 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.
|
||||
*/
|
||||
function sprachbereich(): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.appendChild(abschnitt('Server'));
|
||||
bereich.appendChild(abschnitt('Sprache'));
|
||||
|
||||
const liste = document.createElement('div');
|
||||
liste.className = 'einstellungen';
|
||||
@@ -833,74 +892,9 @@ function serverbereich(geaendert: () => void): HTMLElement {
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const [auskunft, stand] = await Promise.all([systemauskunft(), metadatenEinstellung()]);
|
||||
const stand = await metadatenEinstellung();
|
||||
const eigeneSprache = meineSprache();
|
||||
liste.innerHTML = '';
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Name', auskunft.serverName ?? '—',
|
||||
'So nennen ihn die Apps und die Kopfzeile.',
|
||||
() => dialog({
|
||||
titel: 'Server umbenennen',
|
||||
felder: [{ name: 'name', beschriftung: 'Name', wert: auskunft.serverName ?? '' }],
|
||||
bestaetigen: 'Speichern',
|
||||
tun: async wert => { await servernameSetzen(wert('name')); location.reload(); },
|
||||
})));
|
||||
|
||||
// **Wartung, nicht Alltag.** Nach jedem Scan passiert das von selbst;
|
||||
// dieser Knopf ist für den Fall, dass doch wieder Dubletten auftauchen —
|
||||
// etwa nachdem eine Zuordnung von Hand geändert wurde.
|
||||
const wartung = zeile(
|
||||
'Doppelte Einträge', 'Zusammenlegen, was denselben Film meint',
|
||||
'Zwei Dateien mit leicht verschiedenen Namen konnten früher zwei '
|
||||
+ 'Einträge ergeben. Der Sehfortschritt bleibt dabei erhalten — es '
|
||||
+ 'gewinnt die weitere Stelle.');
|
||||
const legen = knopf('Zusammenlegen', 'btn');
|
||||
legen.onclick = async () => {
|
||||
legen.disabled = true;
|
||||
const vorher = legen.textContent;
|
||||
legen.textContent = 'Läuft…';
|
||||
try {
|
||||
const bericht = await doppelteZusammenlegen();
|
||||
legen.textContent = bericht.gesamt === 0
|
||||
? 'Nichts zu tun'
|
||||
: `${bericht.gesamt} zusammengelegt`;
|
||||
if (bericht.gesamt > 0) geaendert();
|
||||
} catch (fehler) {
|
||||
wartung.appendChild(fehlerzeile((fehler as Error).message));
|
||||
legen.textContent = vorher;
|
||||
legen.disabled = false;
|
||||
}
|
||||
};
|
||||
wartung.querySelector('.knopfreihe')?.appendChild(legen);
|
||||
liste.appendChild(wartung);
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Metadaten',
|
||||
// Ohne die Sprache: die steht eine Zeile tiefer und gehört dort hin.
|
||||
stand.usingProjectKey ? 'Mitgelieferter TMDB-Zugang'
|
||||
: stand.hasApiKey ? 'Eigener TMDB-Zugang'
|
||||
: 'Kein Zugang hinterlegt',
|
||||
stand.usingProjectKey
|
||||
? 'Ein eigener Zugang löst den mitgelieferten ab.'
|
||||
: 'Nach einer Sprachänderung die Metadaten auffrischen — '
|
||||
+ 'vorhandene Titel ändern sich sonst nicht.',
|
||||
() => dialog({
|
||||
titel: 'Metadaten',
|
||||
felder: [
|
||||
{
|
||||
name: 'schluessel', beschriftung: 'Eigener TMDB-Zugang', art: 'password',
|
||||
platzhalter: 'Read Access Token oder API Key',
|
||||
hinweis: 'Leer lassen, um den mitgelieferten zu behalten.',
|
||||
},
|
||||
],
|
||||
bestaetigen: 'Speichern',
|
||||
tun: async wert => {
|
||||
await metadatenSetzen({ apiKey: wert('schluessel') || undefined });
|
||||
location.reload();
|
||||
},
|
||||
})));
|
||||
|
||||
// **Die Sprache des Servers als eigene Zeile.**
|
||||
//
|
||||
// Sie steckte im Dialog „Metadaten" — zusammen mit dem TMDB-Schlüssel,
|
||||
@@ -973,11 +967,219 @@ function serverbereich(geaendert: () => void): HTMLElement {
|
||||
alert(`${bericht.gesamt} Übersetzungen geholt, ${bericht.fehlend} ohne Treffer.`);
|
||||
},
|
||||
})));
|
||||
|
||||
liste.appendChild(zeile('Fassung', auskunft.version, null));
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile((fehler as Error).message));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/** Speicher — wo die Dateien des Servers liegen.
|
||||
*
|
||||
* **Warum das in die Oberfläche gehört.** Die drei Pfade standen nur in der
|
||||
* Compose-Datei. Wer wissen wollte, wohin die Bilder wandern oder warum das
|
||||
* `cache`-Volume vollläuft, musste sich auf das NAS verbinden — für eine
|
||||
* Auskunft, die der Server längst hat.
|
||||
*
|
||||
* **Die Pfade bleiben Umgebungsvariablen.** Sie im Betrieb umzubiegen, während
|
||||
* Dateien darin liegen, hiesse: die Datenbank zeigt auf den alten Ort, der
|
||||
* Server schreibt in den neuen, und niemand merkt es, bis ein Bild fehlt.
|
||||
* Angezeigt wird deshalb der Variablenname — damit klar ist, wo man schraubt.
|
||||
*/
|
||||
function speicherbereich(): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.appendChild(abschnitt('Speicher'));
|
||||
|
||||
const liste = document.createElement('div');
|
||||
liste.className = 'einstellungen';
|
||||
liste.innerHTML = '<div class="spinner"></div>';
|
||||
bereich.appendChild(liste);
|
||||
|
||||
void laden();
|
||||
|
||||
async function laden(): Promise<void> {
|
||||
let orte;
|
||||
try { orte = await speicherorte(); }
|
||||
catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
return;
|
||||
}
|
||||
|
||||
liste.innerHTML = '';
|
||||
|
||||
orte.forEach(o => {
|
||||
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';
|
||||
|
||||
const frei = o.frei === null ? '' : ` · ${groesse(o.frei)} frei`;
|
||||
text.innerHTML = `<strong>${esc(o.name)}</strong>`
|
||||
+ `<span class="winzig">${esc(o.pfad)} — ${groesse(o.belegt)} belegt${esc(frei)}</span>`
|
||||
+ `<span class="winzig">Wird gesetzt über ${esc(o.variable)}.</span>`;
|
||||
oben.appendChild(text);
|
||||
|
||||
// Leeren gibt es nur dort, wo es gefahrlos ist.
|
||||
if (o.bilder !== null) {
|
||||
const leeren = knopf('Bildspeicher leeren', 'btn');
|
||||
leeren.onclick = async () => {
|
||||
leeren.disabled = true;
|
||||
try {
|
||||
const { freed } = await bildspeicherLeeren();
|
||||
await laden();
|
||||
// Erst nach dem Neuladen melden, sonst steht die alte Zahl daneben.
|
||||
const gemeldet = document.createElement('p');
|
||||
gemeldet.className = 'hinweis';
|
||||
gemeldet.textContent = `${groesse(freed)} freigegeben. `
|
||||
+ 'Die Bilder werden beim nächsten Aufruf neu geholt.';
|
||||
liste.prepend(gemeldet);
|
||||
} catch (fehler) {
|
||||
d.appendChild(fehlerzeile(meldung(fehler)));
|
||||
leeren.disabled = false;
|
||||
}
|
||||
};
|
||||
oben.appendChild(leeren);
|
||||
}
|
||||
|
||||
d.appendChild(oben);
|
||||
liste.appendChild(d);
|
||||
});
|
||||
}
|
||||
|
||||
return bereich;
|
||||
}
|
||||
|
||||
function serverbereich(geaendert: () => void): HTMLElement {
|
||||
const bereich = document.createElement('section');
|
||||
bereich.appendChild(abschnitt('Server'));
|
||||
|
||||
const liste = document.createElement('div');
|
||||
liste.className = 'einstellungen';
|
||||
bereich.appendChild(liste);
|
||||
|
||||
void (async () => {
|
||||
try {
|
||||
const [auskunft, stand] = await Promise.all([systemauskunft(), metadatenEinstellung()]);
|
||||
liste.innerHTML = '';
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Name', auskunft.serverName ?? '—',
|
||||
'So nennen ihn die Apps und die Kopfzeile.',
|
||||
() => dialog({
|
||||
titel: 'Server umbenennen',
|
||||
felder: [{ name: 'name', beschriftung: 'Name', wert: auskunft.serverName ?? '' }],
|
||||
bestaetigen: 'Speichern',
|
||||
tun: async wert => { await servernameSetzen(wert('name')); location.reload(); },
|
||||
})));
|
||||
|
||||
// **Wartung, nicht Alltag.** Nach jedem Scan passiert das von selbst;
|
||||
// dieser Knopf ist für den Fall, dass doch wieder Dubletten auftauchen —
|
||||
// etwa nachdem eine Zuordnung von Hand geändert wurde.
|
||||
const wartung = zeile(
|
||||
'Doppelte Einträge', 'Zusammenlegen, was denselben Film meint',
|
||||
'Zwei Dateien mit leicht verschiedenen Namen konnten früher zwei '
|
||||
+ 'Einträge ergeben. Der Sehfortschritt bleibt dabei erhalten — es '
|
||||
+ 'gewinnt die weitere Stelle.');
|
||||
const legen = knopf('Zusammenlegen', 'btn');
|
||||
legen.onclick = async () => {
|
||||
legen.disabled = true;
|
||||
const vorher = legen.textContent;
|
||||
legen.textContent = 'Läuft…';
|
||||
try {
|
||||
const bericht = await doppelteZusammenlegen();
|
||||
legen.textContent = bericht.gesamt === 0
|
||||
? 'Nichts zu tun'
|
||||
: `${bericht.gesamt} zusammengelegt`;
|
||||
if (bericht.gesamt > 0) geaendert();
|
||||
} catch (fehler) {
|
||||
wartung.appendChild(fehlerzeile(meldung(fehler)));
|
||||
legen.textContent = vorher;
|
||||
legen.disabled = false;
|
||||
}
|
||||
};
|
||||
wartung.querySelector('.knopfreihe')?.appendChild(legen);
|
||||
liste.appendChild(wartung);
|
||||
|
||||
liste.appendChild(zeile(
|
||||
'Metadaten',
|
||||
// Ohne die Sprache: die steht eine Zeile tiefer und gehört dort hin.
|
||||
stand.usingProjectKey ? 'Mitgelieferter TMDB-Zugang'
|
||||
: stand.hasApiKey ? 'Eigener TMDB-Zugang'
|
||||
: 'Kein Zugang hinterlegt',
|
||||
stand.usingProjectKey
|
||||
? 'Ein eigener Zugang löst den mitgelieferten ab.'
|
||||
: 'Nach einer Sprachänderung die Metadaten auffrischen — '
|
||||
+ 'vorhandene Titel ändern sich sonst nicht.',
|
||||
() => dialog({
|
||||
titel: 'Metadaten',
|
||||
felder: [
|
||||
{
|
||||
name: 'schluessel', beschriftung: 'Eigener TMDB-Zugang', art: 'password',
|
||||
platzhalter: 'Read Access Token oder API Key',
|
||||
hinweis: 'Leer lassen, um den mitgelieferten zu behalten.',
|
||||
},
|
||||
],
|
||||
bestaetigen: 'Speichern',
|
||||
tun: async wert => {
|
||||
await metadatenSetzen({ apiKey: wert('schluessel') || undefined });
|
||||
location.reload();
|
||||
},
|
||||
})));
|
||||
|
||||
// **Fassung und Build nebeneinander.**
|
||||
//
|
||||
// Die Fassung sagt, welcher Stand gemeint ist; der Build sagt, welches
|
||||
// Abbild wirklich läuft. Ohne den zweiten Wert war nicht feststellbar,
|
||||
// ob das NAS den Bau von eben zieht — und eine Fehlersuche, die diese
|
||||
// Frage nicht beantworten kann, dreht sich im Kreis.
|
||||
liste.appendChild(zeile(
|
||||
'Fassung',
|
||||
`${auskunft.version} · Build ${auskunft.build}`,
|
||||
auskunft.build === 'DEV'
|
||||
? 'Läuft aus dem Quellbaum, nicht aus einem Abbild.'
|
||||
: 'Diese Kennung druckt das Veröffentlichungsskript am Ende. '
|
||||
+ 'Stimmt sie nicht überein, läuft hier ein älteres Abbild.'));
|
||||
} catch (fehler) {
|
||||
liste.innerHTML = '';
|
||||
liste.appendChild(fehlerzeile(meldung(fehler)));
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user