Add files via upload
This commit is contained in:
1 parent
35d5816588
commit
cbc9fd2510
3 files changed
+114
-16
No files matched your search
@@ -5,6 +5,16 @@ Alle wichtigen Änderungen an diesem Projekt werden in dieser Datei dokumentiert
|
||||
Das Format basiert auf [Keep a Changelog](https://keepachangelog.com/de/1.0.0/),
|
||||
und dieses Projekt folgt [Semantic Versioning](https://semver.org/lang/de/).
|
||||
|
||||
## [33.1.0] - 2026-07-03
|
||||
|
||||
### Automatischer API-Failover
|
||||
|
||||
#### Added
|
||||
- **Failover auf v6.bvg.transport.rest**: Wenn die primäre VBB-API nicht antwortet (Netzwerkfehler, Timeout, HTTP 5xx), schaltet die App automatisch auf die baugleiche BVG-API um - gleicher Betreiber, identische Routen und Stop-IDs, deckt ebenfalls ganz Berlin/Brandenburg ab, nutzt aber eine andere Upstream-Quelle. Nach 10 Minuten wird die primäre API automatisch wieder probiert.
|
||||
- **12s-Timeout pro Request** (AbortController): hängende Verbindungen blockieren die App nicht mehr und lösen den Failover aus.
|
||||
- HTTP-4xx-Fehler lösen bewusst KEINEN Failover aus (fehlerhafte Anfrage, kein API-Ausfall).
|
||||
- Sind beide APIs down, werden notfalls abgelaufene Cache-Daten geliefert.
|
||||
|
||||
## [33.0.0] - 2026-07-03
|
||||
|
||||
### Favoriten, Persistenz, echte Filter & Modul-Architektur
|
||||
|
||||
@@ -4,7 +4,15 @@
|
||||
// VBB NETZ STATUS - Haupt-Script
|
||||
// ==========================================
|
||||
|
||||
const API_BASE = 'https://v6.vbb.transport.rest';
|
||||
// Primäre API + baugleicher Fallback (gleicher Betreiber, gleiche
|
||||
// Routen/Stop-IDs, andere Upstream-Quelle - siehe apiFetch)
|
||||
const API_BASES = [
|
||||
'https://v6.vbb.transport.rest',
|
||||
'https://v6.bvg.transport.rest'
|
||||
];
|
||||
const API_BASE = API_BASES[0]; // Call-Sites bauen URLs immer mit der primären Basis
|
||||
let activeApiBaseIndex = 0; // 0 = VBB, 1 = BVG-Fallback
|
||||
let apiFallbackSince = 0; // Zeitpunkt des Failovers (für Recovery)
|
||||
let currentStationId = null;
|
||||
let currentStationName = null;
|
||||
let searchTimeout = null;
|
||||
@@ -61,6 +69,24 @@
|
||||
* @returns {Promise<any>} JSON-Daten
|
||||
* Wirft Error('RATE_LIMIT') wenn Budget aufgebraucht (Cache wird trotzdem genutzt).
|
||||
*/
|
||||
/**
|
||||
* Zentraler API-Aufruf mit automatischem Failover.
|
||||
*
|
||||
* Primär: v6.vbb.transport.rest
|
||||
* Fallback: v6.bvg.transport.rest (gleicher Betreiber, identische
|
||||
* Routen & Stop-IDs, deckt ebenfalls ganz Berlin/Brandenburg
|
||||
* ab - nutzt aber eine ANDERE Upstream-Quelle. Fällt VBB
|
||||
* aus, läuft die App über BVG weiter.)
|
||||
*
|
||||
* - Timeout 12s pro Versuch (hängende Requests blockieren nichts)
|
||||
* - Failover bei Netzwerkfehler, Timeout oder HTTP 5xx
|
||||
* (NICHT bei 4xx - das ist ein Anfragefehler, kein API-Ausfall)
|
||||
* - Nach 10 Minuten auf dem Fallback wird die primäre API erneut probiert
|
||||
* - Cache-Keys basieren auf der primären URL -> Cache übersteht Failover
|
||||
*
|
||||
* @param {string} url - vollständige URL (mit API_BASE gebaut)
|
||||
* @param {object} opts - { ttl: Cache-Dauer in ms (0 = kein Cache) }
|
||||
*/
|
||||
async function apiFetch(url, { ttl = 0 } = {}) {
|
||||
// 1) Frischer Cache-Treffer? -> kein Request nötig
|
||||
const cached = apiCache.get(url);
|
||||
@@ -82,28 +108,90 @@
|
||||
throw new Error('RATE_LIMIT');
|
||||
}
|
||||
|
||||
apiCallTimestamps.push(Date.now());
|
||||
const promise = (async () => {
|
||||
// Recovery: Nach 10 min auf dem Fallback wieder primär versuchen
|
||||
if (activeApiBaseIndex !== 0 &&
|
||||
Date.now() - apiFallbackSince > 10 * 60 * 1000) {
|
||||
activeApiBaseIndex = 0;
|
||||
console.warn('API-Failover: Versuche wieder die primäre VBB-API');
|
||||
}
|
||||
|
||||
const promise = fetch(url)
|
||||
.then(async (response) => {
|
||||
if (!response.ok) throw new Error('HTTP ' + response.status);
|
||||
const data = await response.json();
|
||||
if (ttl > 0) {
|
||||
// Cache-Größe begrenzen (ältesten Eintrag entfernen)
|
||||
if (apiCache.size >= API_CACHE_MAX_ENTRIES) {
|
||||
const oldestKey = apiCache.keys().next().value;
|
||||
apiCache.delete(oldestKey);
|
||||
const tryOrder = activeApiBaseIndex === 0 ? [0, 1] : [1, 0];
|
||||
let lastError = null;
|
||||
|
||||
for (const idx of tryOrder) {
|
||||
const realUrl = url.replace(API_BASES[0], API_BASES[idx]);
|
||||
apiCallTimestamps.push(Date.now()); // jeder echte Versuch zählt
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(realUrl, 12000);
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status >= 500) {
|
||||
// Serverfehler -> Failover lohnt sich
|
||||
throw new Error('HTTP ' + response.status);
|
||||
}
|
||||
// 4xx: Anfrage ist falsch - andere API würde genauso antworten
|
||||
const err = new Error('HTTP ' + response.status);
|
||||
err.noFailover = true;
|
||||
throw err;
|
||||
}
|
||||
apiCache.set(url, { data, time: Date.now(), ttl });
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Basis-Wechsel dokumentieren
|
||||
if (idx !== activeApiBaseIndex) {
|
||||
activeApiBaseIndex = idx;
|
||||
if (idx !== 0) {
|
||||
apiFallbackSince = Date.now();
|
||||
console.warn(`API-Failover aktiv: ${API_BASES[idx]} (VBB-API antwortet nicht)`);
|
||||
} else {
|
||||
console.warn('API-Failover beendet: primäre VBB-API antwortet wieder');
|
||||
}
|
||||
}
|
||||
|
||||
if (ttl > 0) {
|
||||
// Cache-Größe begrenzen (ältesten Eintrag entfernen)
|
||||
if (apiCache.size >= API_CACHE_MAX_ENTRIES) {
|
||||
const oldestKey = apiCache.keys().next().value;
|
||||
apiCache.delete(oldestKey);
|
||||
}
|
||||
apiCache.set(url, { data, time: Date.now(), ttl });
|
||||
}
|
||||
return data;
|
||||
|
||||
} catch (e) {
|
||||
if (e.noFailover) throw e;
|
||||
lastError = e;
|
||||
// Budget für den zweiten Versuch noch da?
|
||||
if (apiBudgetLeft() <= 0) break;
|
||||
// -> nächste Basis probieren
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.finally(() => apiInflight.delete(url));
|
||||
}
|
||||
|
||||
// Beide APIs down: notfalls abgelaufenen Cache liefern
|
||||
if (cached) {
|
||||
console.warn('Beide APIs nicht erreichbar – liefere veraltete Cache-Daten');
|
||||
return cached.data;
|
||||
}
|
||||
throw lastError || new Error('API nicht erreichbar');
|
||||
})().finally(() => apiInflight.delete(url));
|
||||
|
||||
apiInflight.set(url, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
// fetch mit hartem Timeout (hängende Verbindungen brechen sauber ab)
|
||||
async function fetchWithTimeout(realUrl, ms) {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), ms);
|
||||
try {
|
||||
return await fetch(realUrl, { signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
|
||||
// HTML-Escaping für alle Strings aus der API (XSS-Schutz)
|
||||
function escapeHtml(str) {
|
||||
if (str === null || str === undefined) return '';
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "vbb-netz-status",
|
||||
"version": "33.0.0",
|
||||
"version": "33.1.0",
|
||||
"description": "Live-Nahverkehr für Berlin & Brandenburg - Progressive Web App (PWA) für Echtzeit-Abfahrten, Routenplanung und Live-Fahrzeugverfolgung im VBB-Gebiet",
|
||||
"main": "server.js",
|
||||
"scripts": {
|
||||
|
||||
Reference in new issue
Block a user