Files
Flaro/web/app/src/util.ts
T
ScarriffleandClaude Opus 5 ac04901ccc 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>
2026-10-04 15:55:56 +02:00

113 lines
4.2 KiB
TypeScript

export function esc(wert: unknown): string {
return String(wert ?? '').replace(/[&<>"']/g, z =>
({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[z]!));
}
/** Sekunden als 1:02:03 oder 2:03. */
export function dauer(sekunden: number | null): string {
if (!sekunden || sekunden <= 0) return '';
const s = Math.round(sekunden);
const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
return h > 0 ? `${h}:${String(m).padStart(2, '0')}:${String(s % 60).padStart(2, '0')}`
: `${m}:${String(s % 60).padStart(2, '0')}`;
}
export function groesse(bytes: number): string {
if (bytes >= 1024 ** 3) return `${(bytes / 1024 ** 3).toFixed(1)} GB`;
if (bytes >= 1024 ** 2) return `${Math.round(bytes / 1024 ** 2)} MB`;
return `${Math.round(bytes / 1024)} KB`;
}
/** Wie weit ein Stand gediehen ist, als Anteil — oder nichts. */
export function anteil(stand: { positionSeconds: number; runtimeSeconds: number | null } | null | undefined)
: number | null {
if (!stand?.runtimeSeconds || stand.runtimeSeconds <= 0) return null;
return stand.positionSeconds / stand.runtimeSeconds;
}
/** „noch 42 Min." — was auf einer Kachel wirklich interessiert. */
export function verbleibend(stand: { positionSeconds: number; runtimeSeconds: number | null }): string {
if (!stand.runtimeSeconds) return '';
const rest = Math.max(0, stand.runtimeSeconds - stand.positionSeconds);
return rest < 60 ? 'fast durch' : `noch ${Math.round(rest / 60)} Min.`;
}
export function knopf(text: string, klasse = 'btn'): HTMLButtonElement {
const b = document.createElement('button');
b.className = klasse;
b.textContent = text;
return b;
}
export function feld(platzhalter: string, art = 'text'): HTMLInputElement {
const i = document.createElement('input');
i.className = 'feld';
i.type = art;
i.placeholder = platzhalter;
return i;
}
/** Zeigt einen Fehler, ohne die Seite zu verlassen. */
export function fehlerzeile(text: string): HTMLElement {
const d = document.createElement('div');
d.className = 'fehler';
d.textContent = text;
return d;
}
export function leer(text: string): HTMLElement {
const d = document.createElement('div');
d.className = 'leer';
d.textContent = text;
return d;
}
/** Was bei einem Fehler in der Oberfläche steht — immer deutsch.
*
* **Warum nicht einfach `fehler.message`.** Das stand überall, und überall
* kam dabei Englisch heraus: „Failed to fetch" vom Browser, „An error
* occurred while saving the entity changes" aus der Datenbankschicht. Ein
* Server, der auf Deutsch antwortet, darf das nicht durchreichen.
*
* **Der Server schreibt seine Sätze selbst.** Kommt die Meldung von dort,
* ist sie bereits deutsch und sagt, was zu tun ist — die wird übernommen.
* Alles andere entsteht hier, weil es im Browser passiert ist und der
* Server davon nichts weiss.
*/
export function meldung(fehler: unknown): string {
// Eine Antwort des Servers: sein Text gilt.
if (fehler instanceof Error && fehler.name === 'ApiFehler' && fehler.message) {
return fehler.message;
}
// **Die Verbindung kam nicht zustande.** `fetch` wirft dafür einen
// `TypeError` — und zwar in jedem Browser mit anderem Wortlaut: „Failed to
// fetch", „Load failed", „NetworkError when attempting to fetch resource".
if (fehler instanceof TypeError) {
return 'Der Server war nicht erreichbar. Läuft er noch, und steht die Verbindung?';
}
if (fehler instanceof DOMException && fehler.name === 'AbortError') {
return 'Der Vorgang wurde abgebrochen.';
}
const text = fehler instanceof Error ? fehler.message : String(fehler ?? '');
// Eine reine Statuszahl ist keine Meldung.
if (/^\d{3}$/.test(text.trim())) {
return `Der Server hat die Anfrage abgelehnt (${text.trim()}). `
+ 'Die Einzelheiten stehen im Protokoll von heute.';
}
// **Englisch bleibt draussen.** Was hier ankommt und keine deutschen
// Umlaute oder typischen Wörter trägt, stammt fast sicher aus einer
// Laufzeitbibliothek — das hilft niemandem weiter.
if (text.length === 0 || !/[äöüßÄÖÜ]|\b(der|die|das|ist|wurde|nicht|kein|keine)\b/i.test(text)) {
return 'Etwas ist schiefgegangen. Die Einzelheiten stehen im Protokoll von heute.';
}
return text;
}