Chore: Sprint32-36 Zwischenstand — alle Änderungen aus dieser Session committen
This commit is contained in:
parent
f4052fbb7d
commit
747c353444
20 changed files with 3115 additions and 63 deletions
|
|
@ -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}`); },
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue