Kundenansicht neu P1: h(), api() mit ApiFehler und Quelltext-Regel

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-16 15:50:12 +00:00
1 parent 46edb7c418
commit 6f0808647f
4 files changed
+140

No files matched your search

+54
View File
@@ -0,0 +1,54 @@
/* Kundenansicht neu: ein einziger Zugang zur Schnittstelle des Portals.
Fehler kommen immer als ApiFehler (mit Status, Code, Text, Feld und dem ganzen Body), Netzfehler mit Status 0. */
const TEXT_NETZ = 'Keine Verbindung. Bitte versuch es gleich noch einmal.'
const TEXT_502 = 'Server nicht erreichbar. Bitte kurz warten und noch einmal versuchen.'
// Sitzung beendet (Passwort auf einem anderen Gerät geändert): nur einmal zur Anmeldung wechseln
let sitzungBeendetGemeldet = false
export class ApiFehler extends Error {
constructor(message, { status = 0, code = '', field = '', body = {} } = {}) {
super(message)
this.name = 'ApiFehler'
this.status = status
this.code = code
this.field = field
this.body = body
}
}
/** Aufruf der Schnittstelle. `roh` ist ein Rohdaten-Body (Blob, ArrayBuffer …), der unverändert als application/octet-stream geht. */
export async function api(pfad, { method = 'GET', body, roh } = {}) {
const headers = {}
let inhalt
if (roh) {
headers['Content-Type'] = 'application/octet-stream'
inhalt = roh
} else if (body !== undefined && body !== null) {
headers['Content-Type'] = 'application/json'
inhalt = JSON.stringify(body)
}
let antwort
try {
antwort = await fetch(pfad, { method, credentials: 'same-origin', headers, body: inhalt })
} catch {
// Netzfehler (offline, Server nicht erreichbar): deutscher Text statt „Failed to fetch“; Status 0 = nie eine Antwort bekommen
throw new ApiFehler(TEXT_NETZ, { status: 0 })
}
const daten = await antwort.json().catch(() => ({}))
if (antwort.ok) return daten
const antwortBody = daten !== null && typeof daten === 'object' && !Array.isArray(daten) ? daten : {}
const message = typeof antwortBody.error === 'string' && antwortBody.error ? antwortBody.error : (antwort.status === 502 ? TEXT_502 : `Fehler ${antwort.status}`)
const fehler = new ApiFehler(message, {
status: antwort.status,
code: typeof antwortBody.code === 'string' ? antwortBody.code : '',
field: typeof antwortBody.field === 'string' ? antwortBody.field : '',
body: antwortBody,
})
if (fehler.code === 'session_revoked' && !sitzungBeendetGemeldet) {
sitzungBeendetGemeldet = true
window.location.href = `/login.html?error=${encodeURIComponent(message)}`
}
throw fehler
}
+28
View File
@@ -0,0 +1,28 @@
/* Kundenansicht neu: Elemente bauen, ohne dass Kundentext je als HTML gelesen wird.
Texte werden Textknoten, nie innerHTML. Das Modul liest `document` erst beim Aufruf und ist in Node importierbar. */
/** Element bauen. attrs.class setzt die Klasse, attrs.on = { click: fn } hängt Ereignisse an, true setzt ein Attribut ohne Wert,
* false/null/undefined lassen es weg. Kinder werden flach gelegt, Strings und Zahlen werden Textknoten. */
export function h(tag, attrs = {}, ...kinder) {
const el = document.createElement(tag)
for (const [name, wert] of Object.entries(attrs || {})) {
if (wert === undefined || wert === null || wert === false) continue
if (name === 'class') el.className = wert
else if (name === 'on') for (const [typ, fn] of Object.entries(wert)) el.addEventListener(typ, fn)
else el.setAttribute(name, wert === true ? '' : String(wert))
}
for (const kind of kinder.flat(Infinity)) if (kind !== undefined && kind !== null && kind !== false) el.append(typeof kind === 'number' ? String(kind) : kind)
return el
}
/** Alle Kinder entfernen. */
export function leeren(el) {
el.replaceChildren()
return el
}
/** Text setzen (nie HTML); null und undefined werden zum leeren Text. */
export function text(el, wert) {
el.textContent = String(wert ?? '')
return el
}