Chore: Sprint32-36 Zwischenstand — alle Änderungen aus dieser Session committen

This commit is contained in:
rene 2026-05-03 11:09:39 +02:00
parent f4052fbb7d
commit 747c353444
20 changed files with 3115 additions and 63 deletions

View file

@ -6,69 +6,84 @@
const API = (() => {
// ----------------------------------------------------------
// Request-Deduplication: gleiche GET-URL nur einmal in-flight
// ----------------------------------------------------------
const _inflight = new Map();
// ----------------------------------------------------------
// Interner HTTP-Kern
// ----------------------------------------------------------
async function _request(method, path, body = null, options = {}) {
async function _doRequest(method, path, body, attempt) {
const config = {
method,
headers: { 'Content-Type': 'application/json' },
credentials: 'include', // HttpOnly Cookie wird automatisch mitgesendet
credentials: 'include',
};
if (body && !(body instanceof FormData)) {
config.body = JSON.stringify(body);
} else if (body instanceof FormData) {
delete config.headers['Content-Type']; // Browser setzt multipart/form-data
delete config.headers['Content-Type'];
config.body = body;
}
// JWT aus localStorage als Bearer (für API-Calls die das brauchen)
const token = localStorage.getItem('by_token');
if (token) {
config.headers['Authorization'] = `Bearer ${token}`;
}
if (token) config.headers['Authorization'] = `Bearer ${token}`;
let response;
try {
response = await fetch(`/api${path}`, config);
} catch (err) {
const offlineMsg = 'Kein Internet — du bist offline.';
if (window.UI && UI.toast) UI.toast.warning(offlineMsg, 4000);
throw new APIError(offlineMsg, 0, 'network');
} catch {
// Netzwerkfehler: bei GET bis zu 2 Retry-Versuche
if (method === 'GET' && attempt < 2) {
await new Promise(r => setTimeout(r, 200 * Math.pow(3, attempt)));
return _doRequest(method, path, body, attempt + 1);
}
const msg = 'Kein Internet — du bist offline.';
if (window.UI?.toast) UI.toast.warning(msg, 4000);
throw new APIError(msg, 0, 'network');
}
// 204 No Content
if (response.status === 204) return null;
let data;
try {
data = await response.json();
} catch {
data = null;
}
try { data = await response.json(); } catch { data = null; }
if (!response.ok) {
const message = data?.detail || data?.message || `Fehler ${response.status}`;
// SW gibt bei Offline-Anfragen 503 + 'Offline — keine Verbindung.' zurück
const isOffline = response.status === 503 && message.startsWith('Offline');
if (isOffline && window.UI && UI.toast) {
UI.toast.warning('Kein Internet — du bist offline.', 4000);
const isSwOffline = response.status === 503 && message.startsWith('Offline');
// Retry: GET auf echte 5xx (nicht SW-generierte Offline-503)
if (method === 'GET' && response.status >= 500 && !isSwOffline && attempt < 2) {
await new Promise(r => setTimeout(r, 200 * Math.pow(3, attempt)));
return _doRequest(method, path, body, attempt + 1);
}
throw new APIError(message, response.status, isOffline ? 'network' : data?.code);
if (isSwOffline && window.UI?.toast) UI.toast.warning('Kein Internet — du bist offline.', 4000);
throw new APIError(message, response.status, isSwOffline ? 'network' : data?.code);
}
// SW hat die Anfrage in die Offline-Queue eingereiht
if (data?._queued) {
if (typeof UI !== 'undefined' && UI.toast) {
if (typeof UI !== 'undefined' && UI.toast)
UI.toast.info('Offline gespeichert — wird automatisch synchronisiert');
}
return data;
}
return data;
}
async function _request(method, path, body = null) {
// GET-Deduplication: laufende identische Anfragen zusammenfassen
if (method === 'GET') {
if (_inflight.has(path)) return _inflight.get(path);
const promise = _doRequest('GET', path, null, 0).finally(() => _inflight.delete(path));
_inflight.set(path, promise);
return promise;
}
return _doRequest(method, path, body, 0);
}
// ----------------------------------------------------------
// Öffentliche HTTP-Methoden
// ----------------------------------------------------------
@ -426,8 +441,9 @@ const API = (() => {
// WETTER
// ----------------------------------------------------------
const weather = {
alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); },
get(lat, lon) { return get(`/weather?lat=${lat}&lon=${lon}`); },
alerts(lat, lon) { return get(`/weather/alerts?lat=${lat}&lon=${lon}`); },
get(lat, lon) { return get(`/weather?lat=${lat}&lon=${lon}`); },
forecast(lat, lon) { return get(`/weather/forecast?lat=${lat}&lon=${lon}`); },
};
// ----------------------------------------------------------