Logo für Webseiten-Tab implementiert

Dieser Commit ist enthalten in:
hendrik_gebhardt@gmx.de
2026-01-10 16:47:02 +00:00
committet von Server Deploy
Ursprung ef153789cc
Commit 5b1f8b1cfe
53 geänderte Dateien mit 2377 neuen und 46 gelöschten Zeilen

Datei anzeigen

@ -133,12 +133,23 @@ class ApiClient {
}
// Handle authentication failure
handleAuthFailure() {
// requestToken: Der Token, der bei der fehlgeschlagenen Anfrage verwendet wurde
handleAuthFailure(requestToken = null) {
// Race-Condition Check: Wenn ein neuer Token existiert, der NACH dem
// fehlgeschlagenen Request gesetzt wurde, nicht ausloggen
const currentToken = localStorage.getItem('auth_token');
if (requestToken && currentToken && requestToken !== currentToken) {
console.log('[API] 401 ignored - new token exists (login occurred after request)');
return false; // Nicht ausloggen
}
console.log('[API] Authentication failed - clearing tokens');
this.setToken(null);
this.setRefreshToken(null);
this.setCsrfToken(null);
window.dispatchEvent(new CustomEvent('auth:logout'));
return true;
}
// Proaktiver Token-Refresh Timer
@ -224,8 +235,12 @@ class ApiClient {
// Handle 401 Unauthorized
if (response.status === 401) {
const currentTokenNow = localStorage.getItem('auth_token');
console.log('[API] 401 received for:', endpoint);
console.log('[API] Token used in request:', token ? token.substring(0, 20) + '...' : 'NULL');
console.log('[API] Current token in storage:', currentTokenNow ? currentTokenNow.substring(0, 20) + '...' : 'NULL');
console.log('[API] Tokens match:', token === currentTokenNow);
// Versuche Token mit Refresh-Token zu erneuern
if (this.refreshToken && !this.refreshingToken && !options._tokenRefreshAttempted) {
console.log('[API] Attempting token refresh...');
@ -235,14 +250,27 @@ class ApiClient {
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true });
} catch (refreshError) {
console.log('[API] Token refresh failed:', refreshError.message);
// Fallback zum Logout
this.handleAuthFailure();
// Fallback zum Logout - aber nur wenn kein neuer Login stattfand
if (this.handleAuthFailure(token)) {
throw new ApiError('Sitzung abgelaufen', 401);
}
// Neuer Token existiert - Request mit neuem Token wiederholen (max 1x)
if (!options._newTokenRetried) {
return this.request(endpoint, { ...options, _tokenRefreshAttempted: true, _newTokenRetried: true });
}
throw new ApiError('Sitzung abgelaufen', 401);
}
}
// Kein Refresh-Token oder Refresh bereits versucht
this.handleAuthFailure();
// Nur ausloggen wenn kein neuer Token existiert
if (this.handleAuthFailure(token)) {
throw new ApiError('Sitzung abgelaufen', 401);
}
// Neuer Token existiert - Request mit neuem Token wiederholen (max 1x)
if (!options._newTokenRetried) {
return this.request(endpoint, { ...options, _newTokenRetried: true });
}
throw new ApiError('Sitzung abgelaufen', 401);
}

Datei anzeigen

@ -24,6 +24,7 @@ import knowledgeManager from './knowledge.js';
import codingManager from './coding.js';
import mobileManager from './mobile.js';
import reminderManager from './reminders.js';
import pwaManager from './pwa.js';
import { $, $$, debounce, getFromStorage, setToStorage } from './utils.js';
class App {
@ -91,6 +92,9 @@ class App {
// Initialize mobile features
mobileManager.init();
// Initialize PWA features
pwaManager.init();
// Update UI
this.updateUserMenu();

Datei anzeigen

@ -532,19 +532,24 @@ class SessionTimerHandler {
return false;
}
// Session refreshen um neues Token zu bekommen
this.isActive = true; // Aktivieren damit refreshSession funktioniert
await this.refreshSession();
// Timer mit aktuellem Token starten
this.expiresAt = expiresAt;
this.isActive = true;
this.start();
// Timer mit neuem Token starten
this.updateFromToken();
if (this.expiresAt) {
this.start();
return true;
// Token-Refresh VERZÖGERN um Race-Condition zu vermeiden:
// Andere Module machen beim Start Requests mit dem aktuellen Token.
// Ein sofortiger Refresh würde den Token ändern, während Requests noch laufen.
const remainingTime = expiresAt - Date.now();
const refreshThreshold = 5 * 60 * 1000; // 5 Minuten
if (remainingTime < refreshThreshold) {
// Token läuft bald ab - nach kurzer Verzögerung refreshen
setTimeout(() => this.refreshSession(), 2000);
}
// Sonst: Token ist noch frisch genug, Refresh passiert später durch Interaktion
this.isActive = false;
return false;
return true;
}
start() {

275
frontend/js/pwa.js Normale Datei
Datei anzeigen

@ -0,0 +1,275 @@
/**
* TASKMATE - PWA Module
* =====================
* Progressive Web App Features
*/
class PWAManager {
constructor() {
this.deferredPrompt = null;
this.installButton = null;
this.isInstalled = false;
}
/**
* Initialize PWA features
*/
init() {
// Check if already installed
this.checkInstallStatus();
// Listen for install prompt
window.addEventListener('beforeinstallprompt', (e) => {
console.log('[PWA] Install prompt available');
e.preventDefault();
this.deferredPrompt = e;
this.showInstallButton();
});
// Listen for app installed
window.addEventListener('appinstalled', () => {
console.log('[PWA] App installed');
this.isInstalled = true;
this.hideInstallButton();
this.showInstallSuccess();
});
// Check for iOS
if (this.isIOS() && !this.isInStandaloneMode()) {
this.showIOSInstallInstructions();
}
// Update online/offline status
this.updateOnlineStatus();
window.addEventListener('online', () => this.updateOnlineStatus());
window.addEventListener('offline', () => this.updateOnlineStatus());
}
/**
* Check if app is already installed
*/
checkInstallStatus() {
// Check for display-mode: standalone
if (window.matchMedia('(display-mode: standalone)').matches) {
this.isInstalled = true;
console.log('[PWA] Already running in standalone mode');
return;
}
// Check for iOS standalone
if (window.navigator.standalone) {
this.isInstalled = true;
console.log('[PWA] Already running in iOS standalone mode');
return;
}
// Check URL parameters (for TWA)
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get('mode') === 'twa') {
this.isInstalled = true;
console.log('[PWA] Running as TWA');
return;
}
}
/**
* Show install button
*/
showInstallButton() {
if (this.isInstalled) return;
// Create install button if not exists
if (!this.installButton) {
this.createInstallButton();
}
this.installButton.classList.remove('hidden');
// Show install banner after delay
setTimeout(() => {
if (!this.isInstalled && this.deferredPrompt) {
this.showInstallBanner();
}
}, 30000); // 30 seconds
}
/**
* Hide install button
*/
hideInstallButton() {
if (this.installButton) {
this.installButton.classList.add('hidden');
}
}
/**
* Create install button in header
*/
createInstallButton() {
this.installButton = document.createElement('button');
this.installButton.className = 'btn btn-primary install-button hidden';
this.installButton.innerHTML = `
<svg viewBox="0 0 24 24" width="16" height="16">
<path d="M12 2v10m0 0l-4-4m4 4l4-4M3 12v7a2 2 0 002 2h14a2 2 0 002-2v-7"
stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
<span>App installieren</span>
`;
// Insert before user menu
const headerActions = document.querySelector('.header-actions');
if (headerActions) {
headerActions.insertBefore(this.installButton, headerActions.firstChild);
}
// Handle click
this.installButton.addEventListener('click', () => this.handleInstallClick());
}
/**
* Handle install button click
*/
async handleInstallClick() {
if (!this.deferredPrompt) return;
// Show the install prompt
this.deferredPrompt.prompt();
// Wait for the user to respond
const { outcome } = await this.deferredPrompt.userChoice;
console.log(`[PWA] User response: ${outcome}`);
// Clear the deferred prompt
this.deferredPrompt = null;
// Hide button if accepted
if (outcome === 'accepted') {
this.hideInstallButton();
}
}
/**
* Show install banner
*/
showInstallBanner() {
const banner = document.createElement('div');
banner.className = 'pwa-install-banner';
banner.innerHTML = `
<div class="install-banner-content">
<div class="install-banner-icon">
<svg viewBox="0 0 24 24" width="32" height="32">
<path d="M12 2v10m0 0l-4-4m4 4l4-4M3 12v7a2 2 0 002 2h14a2 2 0 002-2v-7"
stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round"/>
</svg>
</div>
<div class="install-banner-text">
<h3>TaskMate App installieren</h3>
<p>Installiere TaskMate für schnelleren Zugriff und Offline-Nutzung</p>
</div>
<div class="install-banner-actions">
<button class="btn btn-secondary" data-dismiss>Später</button>
<button class="btn btn-primary" data-install>Installieren</button>
</div>
<button class="install-banner-close" data-dismiss>&times;</button>
</div>
`;
document.body.appendChild(banner);
// Animate in
setTimeout(() => banner.classList.add('show'), 100);
// Handle buttons
banner.querySelector('[data-install]').addEventListener('click', () => {
this.handleInstallClick();
this.dismissBanner(banner);
});
banner.querySelectorAll('[data-dismiss]').forEach(btn => {
btn.addEventListener('click', () => this.dismissBanner(banner));
});
}
/**
* Dismiss install banner
*/
dismissBanner(banner) {
banner.classList.remove('show');
setTimeout(() => banner.remove(), 300);
}
/**
* Show install success message
*/
showInstallSuccess() {
window.dispatchEvent(new CustomEvent('toast:show', {
detail: {
message: 'TaskMate wurde erfolgreich installiert!',
type: 'success'
}
}));
}
/**
* Check if iOS
*/
isIOS() {
return /iPhone|iPad|iPod/.test(navigator.userAgent);
}
/**
* Check if in standalone mode
*/
isInStandaloneMode() {
return window.navigator.standalone ||
window.matchMedia('(display-mode: standalone)').matches;
}
/**
* Show iOS install instructions
*/
showIOSInstallInstructions() {
// Only show once per session
if (sessionStorage.getItem('ios-install-shown')) return;
const instructions = document.createElement('div');
instructions.className = 'ios-install-instructions';
instructions.innerHTML = `
<div class="ios-install-content">
<h3>TaskMate installieren</h3>
<p>Tippe auf <span class="ios-share-icon">⬆</span> und wähle "Zum Home-Bildschirm"</p>
<button class="btn btn-primary" data-dismiss>Verstanden</button>
</div>
`;
document.body.appendChild(instructions);
// Animate in
setTimeout(() => instructions.classList.add('show'), 100);
// Handle dismiss
instructions.querySelector('[data-dismiss]').addEventListener('click', () => {
instructions.classList.remove('show');
setTimeout(() => instructions.remove(), 300);
sessionStorage.setItem('ios-install-shown', 'true');
});
}
/**
* Update online/offline status
*/
updateOnlineStatus() {
const isOnline = navigator.onLine;
document.body.classList.toggle('offline', !isOnline);
// Update offline banner
const offlineBanner = document.getElementById('offline-banner');
if (offlineBanner) {
offlineBanner.classList.toggle('hidden', isOnline);
}
}
}
// Export
const pwaManager = new PWAManager();
export default pwaManager;