Add files via upload

This commit is contained in:
Serverlele30 authored and GitHub committed 2026-08-31 16:04:38 +02:00
1 parent 05bae700ec
commit c6e2fed82c
7 files changed
+235 -3

No files matched your search

+25
View File
@@ -34,6 +34,14 @@ const API = (() => {
});
}
function deleteJson(path, body) {
return request(path, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
}
// Baut einen Query-String und lässt dabei undefined/null-Werte weg.
function qs(params) {
const parts = [];
@@ -146,5 +154,22 @@ const API = (() => {
getLineDelayStats(line, days = 14) {
return request(`/stats/delays/${encodeURIComponent(line)}?${qs({ days })}`);
},
// Web-Push (siehe js/push.js + backend/src/routes/webPush.js)
getPushPublicKey() {
return request('/webpush/public-key');
},
subscribePush(subscription, favorites) {
return postJson('/webpush/subscribe', { subscription, favorites });
},
unsubscribePush(endpoint) {
return postJson('/webpush/unsubscribe', { endpoint });
},
setPushTripAlarm(endpoint, alarm) {
return postJson('/webpush/trip-alarm', { endpoint, ...alarm });
},
clearPushTripAlarm(endpoint) {
return deleteJson('/webpush/trip-alarm', { endpoint });
},
};
})();
+17 -2
View File
@@ -134,6 +134,7 @@ const App = (() => {
renderFavoriteChips();
renderRecentSearches();
updateFavoriteButton();
if (window.Push) Push.syncFavorites(favorites);
}
function renderFavoriteChips() {
@@ -339,9 +340,22 @@ const App = (() => {
if (!btn || !window.Notification) return;
btn.addEventListener('click', async () => {
try {
await Notification.requestPermission();
// Volles Web-Push-Abo statt nur lokaler Notification-Erlaubnis -
// funktioniert dadurch auch, wenn der Tab/Browser geschlossen ist.
// Fällt automatisch auf den bisherigen, rein Tab-lokalen Push zurück,
// falls der Browser Web-Push nicht unterstützt oder der Server es
// nicht konfiguriert hat (Push.enable() wirft dann einen Fehler, den
// wir hier abfangen - die Tab-lokalen Benachrichtigungen laufen so
// oder so über den normalen Notification.requestPermission()-Pfad
// weiter, den Push.enable() intern mit aufruft).
if (window.Push && Push.isSupported()) {
await Push.enable(loadFavorites());
} else {
await Notification.requestPermission();
}
} catch (err) {
console.error('Notification-Permission-Fehler:', err.message);
console.error('Benachrichtigungen aktivieren fehlgeschlagen:', err.message);
showToast(err.message, 'warning');
}
updateNotifyToggleVisibility(loadFavorites().length > 0);
});
@@ -1262,6 +1276,7 @@ const App = (() => {
function init() {
I18N.init();
if (window.Push && Push.isSupported()) Push.registerServiceWorker();
I18N.setOnLangChange(() => {
// [data-i18n]-Elemente übernimmt I18N.applyStaticTranslations() bereits
// selbst - hier nur die dynamisch (per JS-Template) erzeugten Bereiche
+127
View File
@@ -0,0 +1,127 @@
// Web-Push: Service-Worker-Registrierung, Subscription-Verwaltung und
// Synchronisierung der Favoriten/des Fahrtalarms mit dem Server, damit
// Benachrichtigungen auch bei geschlossenem Tab/Browser ankommen.
//
// Bewusst getrennt vom bisherigen, rein Tab-basierten Störungs-Push (siehe
// app.js showToast()/Notification-Aufrufe) - der bleibt als sofortiges,
// tab-lokales Feedback bestehen. Dieses Modul ist die zusätzliche,
// "funktioniert auch wenn die App zu ist"-Schicht.
const Push = (() => {
const STORAGE_KEY = 'oepnv-navi:db-push-enabled';
let registration = null;
function isSupported() {
return 'serviceWorker' in navigator && 'PushManager' in window;
}
async function registerServiceWorker() {
if (!isSupported()) return null;
try {
registration = await navigator.serviceWorker.register('/sw.js');
return registration;
} catch (err) {
console.error('Service-Worker-Registrierung fehlgeschlagen:', err.message);
return null;
}
}
// Wandelt den base64-kodierten VAPID-Public-Key in das Uint8Array-Format
// um, das PushManager.subscribe() erwartet.
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - (base64String.length % 4)) % 4);
const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/');
const rawData = window.atob(base64);
return Uint8Array.from([...rawData].map((c) => c.charCodeAt(0)));
}
function isEnabled() {
return localStorage.getItem(STORAGE_KEY) === 'true';
}
async function getExistingSubscription() {
if (!registration) return null;
try {
return await registration.pushManager.getSubscription();
} catch (_) {
return null;
}
}
// Aktiviert Web-Push: fragt Benachrichtigungs-Erlaubnis an, abonniert beim
// Push-Dienst des Browsers und meldet die Subscription + die aktuellen
// Favoriten beim eigenen Backend an.
async function enable(favorites) {
if (!isSupported()) throw new Error('Push wird von diesem Browser nicht unterstützt.');
if (!registration) await registerServiceWorker();
if (!registration) throw new Error('Service Worker konnte nicht registriert werden.');
const permission = await Notification.requestPermission();
if (permission !== 'granted') throw new Error('Berechtigung für Benachrichtigungen wurde nicht erteilt.');
const { publicKey } = await API.getPushPublicKey();
let subscription = await getExistingSubscription();
if (!subscription) {
subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array(publicKey),
});
}
await API.subscribePush(subscription.toJSON(), favorites);
localStorage.setItem(STORAGE_KEY, 'true');
}
async function disable() {
localStorage.setItem(STORAGE_KEY, 'false');
const subscription = await getExistingSubscription();
if (!subscription) return;
try {
await API.unsubscribePush(subscription.endpoint);
await subscription.unsubscribe();
} catch (err) {
console.error('Push-Abmeldung fehlgeschlagen:', err.message);
}
}
// Wird aufgerufen, wenn sich die Favoriten ändern, während Push aktiv ist -
// hält den Server auf dem gleichen Stand wie den Browser.
async function syncFavorites(favorites) {
if (!isEnabled()) return;
const subscription = await getExistingSubscription();
if (!subscription) return;
try {
await API.subscribePush(subscription.toJSON(), favorites);
} catch (err) {
console.error('Push-Favoriten-Sync fehlgeschlagen:', err.message);
}
}
// Spiegelt einen Fahrtalarm serverseitig - "best effort": schlägt das fehl
// (z.B. weil Push gar nicht aktiviert ist), bleibt der normale, Tab-lokale
// Fahrtalarm trotzdem voll funktionsfähig.
async function syncTripAlarm(alarm) {
if (!isEnabled()) return;
const subscription = await getExistingSubscription();
if (!subscription) return;
try {
await API.setPushTripAlarm(subscription.endpoint, alarm);
} catch (err) {
console.error('Fahrtalarm-Push-Sync fehlgeschlagen:', err.message);
}
}
async function clearTripAlarm() {
if (!isEnabled()) return;
const subscription = await getExistingSubscription();
if (!subscription) return;
try {
await API.clearPushTripAlarm(subscription.endpoint);
} catch (err) {
console.error('Fahrtalarm-Push-Löschen fehlgeschlagen:', err.message);
}
}
return { isSupported, registerServiceWorker, isEnabled, enable, disable, syncFavorites, syncTripAlarm, clearTripAlarm };
})();
+7
View File
@@ -30,6 +30,12 @@ const TripAlarm = (() => {
renderBanner();
restartPolling();
check(); // sofort einmal prüfen statt erst nach POLL_MS zu warten
// Best effort: Alarm zusätzlich serverseitig spiegeln, damit er auch bei
// geschlossenem Tab/Browser noch auslöst. Schlägt das fehl (Push nicht
// aktiviert/unterstützt), läuft der normale Tab-lokale Alarm unverändert
// weiter - das hier ist rein additiv.
if (window.Push) Push.syncTripAlarm({ tripId, source: 'vbb', line, targetName, targetIndex, stopsBefore: alarm.stopsBefore });
}
function clear() {
@@ -39,6 +45,7 @@ const TripAlarm = (() => {
stopPolling();
hideBanner();
hideOverlay();
if (window.Push) Push.clearTripAlarm();
}
function getActive() {