diff --git a/.claude/settings.local.json b/.claude/settings.local.json
index f5dfb2c..fa25800 100644
--- a/.claude/settings.local.json
+++ b/.claude/settings.local.json
@@ -58,7 +58,11 @@
"Bash(grep -A10 -B5 \"modal-header\\|modal-title\" /home/claude-dev/TaskMate/frontend/css/modal.css)",
"Bash(grep -A10 -B5 \"calendar-reminder\\|reminder.*calendar\" /home/claude-dev/TaskMate/frontend/css/reminders.css)",
"Bash(grep -A15 -B5 \"calendar.*task\\|task.*calendar\" /home/claude-dev/TaskMate/frontend/js/calendar.js)",
- "WebFetch(domain:admin-panel-undso.aegis-sight.de)"
+ "WebFetch(domain:admin-panel-undso.aegis-sight.de)",
+ "Bash(mkdir:*)",
+ "Bash(for:*)",
+ "Bash(do )",
+ "Bash(done)"
]
}
}
\ No newline at end of file
diff --git a/CHANGELOG.txt b/CHANGELOG.txt
index fdadeca..2fe198e 100644
--- a/CHANGELOG.txt
+++ b/CHANGELOG.txt
@@ -1,6 +1,88 @@
TASKMATE - CHANGELOG
====================
+================================================================================
+10.01.2026 - UI: Favicon ohne schwarzen Hintergrund
+================================================================================
+
+## ÄNDERUNG
+Favicon (Browser-Tab Icon) auf transparente PNG-Version umgestellt
+
+## DETAILS
+- SVG-Logo mit dunklem Hintergrund (#0A1832) entfernt
+- Transparente PNG-Icons (AegisSight Logo) als Favicon gesetzt
+- favicon-32x32.png und favicon-16x16.png erstellt
+- Cache-Version erhöht auf 297
+
+## DATEIEN
+✅ frontend/index.html - Favicon-Links aktualisiert
+✅ frontend/assets/icons/favicon-32x32.png - Erstellt
+✅ frontend/assets/icons/favicon-16x16.png - Erstellt
+✅ frontend/sw.js - Cache-Version 297
+
+================================================================================
+10.01.2026 - BUGFIX: Race-Condition bei 401-Fehlern nach Login
+================================================================================
+
+## PROBLEM
+Nach dem Login wurden Benutzer bei bestimmten Tabs (Genehmigung, Coding,
+Kontakte) sofort wieder ausgeloggt.
+
+## URSACHE
+Race-Condition: API-Requests, die VOR dem Login gestartet wurden, kamen
+mit 401 zurück und lösten einen Logout aus, obwohl bereits ein neuer
+gültiger Token existierte.
+
+## LÖSUNG
+✅ handleAuthFailure() prüft jetzt ob ein neuer Token existiert
+ - Vergleicht Token der fehlgeschlagenen Anfrage mit aktuellem Token
+ - Ignoriert 401 wenn neuer Token nach dem Request gesetzt wurde
+ - Wiederholt Request mit neuem Token (max 1x gegen Endlos-Schleifen)
+✅ Cache-Version: 294
+
+================================================================================
+10.01.2026 - FEATURE: PROGRESSIVE WEB APP (PWA) & MOBILE APP SUPPORT
+================================================================================
+
+## NEUE FUNKTIONEN
+Vollständige PWA-Implementierung für mobile App-Funktionalität
+
+## IMPLEMENTIERUNG
+✅ Web App Manifest erstellt (manifest.json)
+ - App-Name, Icons, Theme-Konfiguration
+ - Shortcuts für schnellen Zugriff
+ - Standalone Display-Mode
+✅ PWA Install-Prompt implementiert
+ - Automatische Erkennung der Installierbarkeit
+ - Install-Button im Header
+ - Install-Banner nach 30 Sekunden
+ - iOS-spezifische Installationsanleitung
+✅ Service Worker optimiert
+ - Cache-First für statische Assets
+ - Network-First für HTML und API
+ - Erweiterte Offline-Funktionalität
+ - Background Sync vorbereitet
+✅ PWA-Module (pwa.js) erstellt
+ - Install-Management
+ - Online/Offline Status
+ - iOS-Detection
+✅ Dokumentation für APK-Erstellung
+ - PWABuilder Anleitung
+ - Bubblewrap Setup
+ - TWA (Trusted Web Activity) Konfiguration
+ - Play Store Veröffentlichung
+
+## TECHNISCHE DETAILS
+- Neue Dateien: manifest.json, js/pwa.js, css/pwa.css
+- Icons benötigt: 48x48 bis 512x512 PNG
+- Service Worker Cache-Version: 293
+- APK kann mit PWABuilder.com erstellt werden
+
+## NÄCHSTE SCHRITTE
+1. Icons in allen Größen generieren (siehe generate-icons.js)
+2. APK mit PWABuilder erstellen und testen
+3. Optional: Play Store Veröffentlichung
+
================================================================================
09.01.2026 - ENTFERNUNG: SIMULIERTE VERBRAUCHSANZEIGE IM CODING-MODUL
================================================================================
diff --git a/PWA_TO_APK_ANLEITUNG.md b/PWA_TO_APK_ANLEITUNG.md
new file mode 100644
index 0000000..ff4a501
--- /dev/null
+++ b/PWA_TO_APK_ANLEITUNG.md
@@ -0,0 +1,325 @@
+# TaskMate PWA zu APK - Vollständige Anleitung
+
+## Überblick
+
+Diese Anleitung zeigt, wie Sie aus der TaskMate PWA eine Android APK erstellen, die im Google Play Store veröffentlicht werden kann.
+
+## Voraussetzungen
+
+- TaskMate PWA ist online verfügbar (https://taskmate.aegis-sight.de)
+- Web App Manifest und Service Worker sind implementiert
+- PWA Icons in allen erforderlichen Größen vorhanden
+
+## Methode 1: PWABuilder (Einfachste Methode)
+
+### Schritt 1: PWA Score prüfen
+
+1. Öffnen Sie https://www.pwabuilder.com
+2. Geben Sie Ihre PWA URL ein: `https://taskmate.aegis-sight.de`
+3. Klicken Sie auf "Start"
+4. PWABuilder analysiert Ihre App und zeigt den Score
+
+### Schritt 2: Package für Android generieren
+
+1. Klicken Sie auf "Package for stores"
+2. Wählen Sie "Android"
+3. Konfigurieren Sie die Einstellungen:
+ - **Package ID**: `de.aegissight.taskmate`
+ - **App Name**: TaskMate
+ - **Display Mode**: Standalone
+ - **Orientation**: Any
+ - **Fallback URL**: `https://taskmate.aegis-sight.de`
+
+### Schritt 3: Erweiterte Optionen
+
+- **Signing Key**:
+ - "None" für Test-APK
+ - "New" für Play Store (speichern Sie den Key sicher!)
+- **Maskable Icon**: Upload des 512x512 Icons
+- **Splash Screen**: Automatisch generiert
+- **Settings**:
+ - Enable notifications: Yes
+ - Location permissions: No
+ - WebView settings: Standard
+
+### Schritt 4: Download und Test
+
+1. Klicken Sie auf "Download"
+2. Extrahieren Sie die ZIP-Datei
+3. Die APK befindet sich im Ordner
+4. Installieren Sie zum Testen auf einem Android-Gerät
+
+## Methode 2: Bubblewrap (Für Entwickler)
+
+### Installation
+
+```bash
+# Node.js und npm müssen installiert sein
+npm install -g @bubblewrap/cli
+```
+
+### Projekt initialisieren
+
+```bash
+# Neues Verzeichnis erstellen
+mkdir taskmate-android
+cd taskmate-android
+
+# Bubblewrap init
+bubblewrap init --manifest https://taskmate.aegis-sight.de/manifest.json
+```
+
+### Konfiguration (twa-manifest.json)
+
+```json
+{
+ "packageId": "de.aegissight.taskmate",
+ "host": "taskmate.aegis-sight.de",
+ "name": "TaskMate",
+ "launcherName": "TaskMate",
+ "display": "standalone",
+ "themeColor": "#000000",
+ "navigationColor": "#000000",
+ "backgroundColor": "#000000",
+ "enableNotifications": true,
+ "startUrl": "/",
+ "iconUrl": "https://taskmate.aegis-sight.de/assets/icons/icon-512x512.png",
+ "maskableIconUrl": "https://taskmate.aegis-sight.de/assets/icons/icon-512x512.png",
+ "appVersion": "1.0.0",
+ "signingKey": {
+ "alias": "taskmate-key",
+ "path": "./taskmate-key.keystore"
+ },
+ "splashScreenFadeOutDuration": 300,
+ "enableSiteSettingsShortcut": false,
+ "shortcuts": [
+ {
+ "name": "Neue Aufgabe",
+ "shortName": "Neue Aufgabe",
+ "url": "/?action=new-task",
+ "chosenIconUrl": "https://taskmate.aegis-sight.de/assets/icons/add-task-96x96.png"
+ }
+ ],
+ "fallbackType": "customtabs",
+ "webManifestUrl": "https://taskmate.aegis-sight.de/manifest.json"
+}
+```
+
+### Build-Prozess
+
+```bash
+# Keystore erstellen (nur einmal)
+bubblewrap build --skipPwaValidation
+
+# APK erstellen
+bubblewrap build
+
+# Signed APK für Play Store
+bubblewrap build --skipPwaValidation
+```
+
+## Methode 3: Android Studio mit TWA
+
+### Projekt Setup
+
+1. Erstellen Sie ein neues Android-Projekt
+2. Minimum SDK: API 19 (Android 4.4)
+3. Wählen Sie "No Activity"
+
+### Dependencies (app/build.gradle)
+
+```gradle
+dependencies {
+ implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.5.0'
+}
+```
+
+### AndroidManifest.xml
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+### Digital Asset Links
+
+Erstellen Sie auf Ihrem Server die Datei:
+`https://taskmate.aegis-sight.de/.well-known/assetlinks.json`
+
+```json
+[{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "de.aegissight.taskmate",
+ "sha256_cert_fingerprints": [
+ "YOUR_APP_SIGNING_KEY_FINGERPRINT"
+ ]
+ }
+}]
+```
+
+## Icon-Generierung
+
+Falls Icons fehlen, nutzen Sie diese Tools:
+
+1. **Android Asset Studio**: https://romannurik.github.io/AndroidAssetStudio/
+2. **Maskable.app**: https://maskable.app/editor
+
+Benötigte Größen:
+- 48x48, 72x72, 96x96, 144x144, 192x192, 512x512
+- Adaptive Icons für Android 8+
+
+## Testing
+
+### Lokaler Test
+1. Aktivieren Sie "Unbekannte Quellen" in Android-Einstellungen
+2. Übertragen Sie die APK aufs Gerät
+3. Installieren und testen
+
+### Play Console Test Track
+1. Laden Sie die APK in die Google Play Console
+2. Erstellen Sie eine interne Testversion
+3. Fügen Sie Tester hinzu
+4. Testen Sie Installation und Updates
+
+## Veröffentlichung im Play Store
+
+### Vorbereitung
+1. Google Play Developer Account ($25 einmalig)
+2. App-Beschreibung und Screenshots
+3. Datenschutzerklärung
+4. Altersfreigabe-Fragebogen
+
+### Store Listing
+- **Titel**: TaskMate - Aufgabenverwaltung
+- **Kurzbeschreibung**: Einfache und effiziente Aufgabenverwaltung mit Kanban-Board
+- **Kategorie**: Produktivität
+- **Screenshots**: Mindestens 2, idealerweise 8
+
+### APK Upload
+1. Signierte APK erstellen
+2. In Play Console hochladen
+3. Rollout starten (gestaffelt empfohlen)
+
+## Wartung und Updates
+
+### Version Updates
+1. Erhöhen Sie `versionCode` und `versionName`
+2. Erstellen Sie neue APK
+3. Laden Sie in Play Console hoch
+4. Beschreiben Sie Änderungen
+
+### PWA Updates
+- Service Worker Updates werden automatisch geladen
+- Manifest-Änderungen erfordern App-Update
+- Cache-Versionierung beachten
+
+## Häufige Probleme
+
+### "Not a valid PWA"
+- Prüfen Sie HTTPS
+- Validieren Sie manifest.json
+- Service Worker muss registriert sein
+
+### "Digital Asset Links validation failed"
+- assetlinks.json muss öffentlich erreichbar sein
+- SHA256 Fingerprint muss stimmen
+- Content-Type: application/json
+
+### Icon-Probleme
+- Alle Größen müssen vorhanden sein
+- PNG-Format erforderlich
+- Transparenz vermeiden für adaptive Icons
+
+## Empfehlungen
+
+1. **Verwenden Sie PWABuilder** für den Anfang
+2. **Bubblewrap** für mehr Kontrolle
+3. **Android Studio** nur bei speziellen Anforderungen
+4. Testen Sie auf verschiedenen Geräten
+5. Behalten Sie die Keystore-Datei sicher auf!
+
+## Nächste Schritte
+
+1. Icons generieren (falls noch nicht vorhanden)
+2. PWABuilder Test durchführen
+3. Test-APK erstellen und installieren
+4. Bei Erfolg: Play Store Konto einrichten
+5. Produktions-APK mit Signing Key erstellen
+6. Im Play Store veröffentlichen
+
+Bei Fragen oder Problemen können Sie die offizielle Dokumentation konsultieren:
+- [PWABuilder Docs](https://docs.pwabuilder.com/)
+- [Bubblewrap GitHub](https://github.com/GoogleChromeLabs/bubblewrap)
+- [TWA Documentation](https://developers.google.com/web/android/trusted-web-activity)
\ No newline at end of file
diff --git a/create-placeholder-icons.sh b/create-placeholder-icons.sh
new file mode 100755
index 0000000..d7467ee
--- /dev/null
+++ b/create-placeholder-icons.sh
@@ -0,0 +1,51 @@
+#!/bin/bash
+# Erstellt Platzhalter-Icons für TaskMate PWA
+
+cd /home/claude-dev/TaskMate/frontend/assets/icons/
+
+# Funktion zum Erstellen eines PNG aus SVG mit ImageMagick
+create_icon() {
+ size=$1
+ echo "Erstelle icon-${size}x${size}.png..."
+
+ # Mit rsvg-convert (falls verfügbar)
+ if command -v rsvg-convert &> /dev/null; then
+ rsvg-convert -w $size -h $size taskmate-logo.svg -o icon-${size}x${size}.png
+ rsvg-convert -w $size -h $size taskmate-logo.svg -o icon-maskable-${size}x${size}.png
+ # Mit convert/ImageMagick (falls verfügbar)
+ elif command -v convert &> /dev/null; then
+ convert -background transparent -resize ${size}x${size} taskmate-logo.svg icon-${size}x${size}.png
+ convert -background transparent -resize ${size}x${size} taskmate-logo.svg icon-maskable-${size}x${size}.png
+ else
+ echo "Weder rsvg-convert noch ImageMagick gefunden!"
+ echo "Installieren Sie eines davon mit:"
+ echo " sudo apt-get install librsvg2-bin"
+ echo " oder"
+ echo " sudo apt-get install imagemagick"
+ return 1
+ fi
+}
+
+# Alle benötigten Größen
+sizes=(48 72 96 128 144 152 192 384 512)
+
+# Icons erstellen
+for size in "${sizes[@]}"; do
+ create_icon $size
+done
+
+# Zusätzliche Icons
+if command -v rsvg-convert &> /dev/null; then
+ echo "Erstelle Shortcut-Icons..."
+ rsvg-convert -w 96 -h 96 taskmate-logo.svg -o add-task-96x96.png
+ rsvg-convert -w 96 -h 96 taskmate-logo.svg -o calendar-96x96.png
+elif command -v convert &> /dev/null; then
+ echo "Erstelle Shortcut-Icons..."
+ convert -background transparent -resize 96x96 taskmate-logo.svg add-task-96x96.png
+ convert -background transparent -resize 96x96 taskmate-logo.svg calendar-96x96.png
+fi
+
+echo "Fertig! Icons wurden erstellt."
+echo ""
+echo "Nächster Schritt: Icons in Docker Container kopieren:"
+echo "docker cp *.png taskmate:/app/public/assets/icons/"
\ No newline at end of file
diff --git a/data/taskmate.db b/data/taskmate.db
index 7ae1ec0..bb1c53d 100644
Binary files a/data/taskmate.db and b/data/taskmate.db differ
diff --git a/frontend/.well-known/assetlinks.json b/frontend/.well-known/assetlinks.json
new file mode 100644
index 0000000..b38a55f
--- /dev/null
+++ b/frontend/.well-known/assetlinks.json
@@ -0,0 +1,10 @@
+[{
+ "relation": ["delegate_permission/common.handle_all_urls"],
+ "target": {
+ "namespace": "android_app",
+ "package_name": "de.aegissight.taskmate",
+ "sha256_cert_fingerprints": [
+ "TO_BE_REPLACED_WITH_YOUR_APP_SIGNING_KEY_FINGERPRINT"
+ ]
+ }
+}]
\ No newline at end of file
diff --git a/frontend/assets/icons/add-task-96x96.png b/frontend/assets/icons/add-task-96x96.png
new file mode 100644
index 0000000..a04d8e5
Binary files /dev/null and b/frontend/assets/icons/add-task-96x96.png differ
diff --git a/frontend/assets/icons/android-launchericon-144-144.png b/frontend/assets/icons/android-launchericon-144-144.png
new file mode 100644
index 0000000..7a46729
Binary files /dev/null and b/frontend/assets/icons/android-launchericon-144-144.png differ
diff --git a/frontend/assets/icons/android-launchericon-192-192.png b/frontend/assets/icons/android-launchericon-192-192.png
new file mode 100644
index 0000000..b840f0d
Binary files /dev/null and b/frontend/assets/icons/android-launchericon-192-192.png differ
diff --git a/frontend/assets/icons/android-launchericon-48-48.png b/frontend/assets/icons/android-launchericon-48-48.png
new file mode 100644
index 0000000..b833c62
Binary files /dev/null and b/frontend/assets/icons/android-launchericon-48-48.png differ
diff --git a/frontend/assets/icons/android-launchericon-512-512.png b/frontend/assets/icons/android-launchericon-512-512.png
new file mode 100644
index 0000000..9eb738a
Binary files /dev/null and b/frontend/assets/icons/android-launchericon-512-512.png differ
diff --git a/frontend/assets/icons/android-launchericon-72-72.png b/frontend/assets/icons/android-launchericon-72-72.png
new file mode 100644
index 0000000..b04aa6c
Binary files /dev/null and b/frontend/assets/icons/android-launchericon-72-72.png differ
diff --git a/frontend/assets/icons/android-launchericon-96-96.png b/frontend/assets/icons/android-launchericon-96-96.png
new file mode 100644
index 0000000..a04d8e5
Binary files /dev/null and b/frontend/assets/icons/android-launchericon-96-96.png differ
diff --git a/frontend/assets/icons/calendar-96x96.png b/frontend/assets/icons/calendar-96x96.png
new file mode 100644
index 0000000..a04d8e5
Binary files /dev/null and b/frontend/assets/icons/calendar-96x96.png differ
diff --git a/frontend/assets/icons/create-icons.html b/frontend/assets/icons/create-icons.html
new file mode 100644
index 0000000..bc55ea7
--- /dev/null
+++ b/frontend/assets/icons/create-icons.html
@@ -0,0 +1,198 @@
+
+
+
+ Icon Generator
+
+
+ TaskMate Icon Generator
+ Diese Seite generiert die Icons im Browser. Klicke auf jeden Link, um die Icons herunterzuladen.
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/assets/icons/favicon-16x16.png b/frontend/assets/icons/favicon-16x16.png
new file mode 100644
index 0000000..b833c62
Binary files /dev/null and b/frontend/assets/icons/favicon-16x16.png differ
diff --git a/frontend/assets/icons/favicon-32x32.png b/frontend/assets/icons/favicon-32x32.png
new file mode 100644
index 0000000..b833c62
Binary files /dev/null and b/frontend/assets/icons/favicon-32x32.png differ
diff --git a/frontend/assets/icons/generate-icons.js b/frontend/assets/icons/generate-icons.js
new file mode 100644
index 0000000..3299bf3
--- /dev/null
+++ b/frontend/assets/icons/generate-icons.js
@@ -0,0 +1,154 @@
+const fs = require('fs');
+const { createCanvas } = require('canvas');
+
+// Icon-Größen für PWA
+const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
+
+function drawIcon(ctx, size) {
+ // Black background with rounded corners
+ const radius = size * 0.1875; // 96/512 ratio
+ ctx.fillStyle = '#000000';
+ ctx.beginPath();
+ ctx.moveTo(radius, 0);
+ ctx.lineTo(size - radius, 0);
+ ctx.quadraticCurveTo(size, 0, size, radius);
+ ctx.lineTo(size, size - radius);
+ ctx.quadraticCurveTo(size, size, size - radius, size);
+ ctx.lineTo(radius, size);
+ ctx.quadraticCurveTo(0, size, 0, size - radius);
+ ctx.lineTo(0, radius);
+ ctx.quadraticCurveTo(0, 0, radius, 0);
+ ctx.closePath();
+ ctx.fill();
+
+ // Draw task list
+ const lineWidth = size * 0.0625; // 32/512 ratio
+ const margin = size * 0.3125; // 160/512 ratio
+ const lineLength = size * 0.375; // 192/512 ratio
+ const spacing = size * 0.1875; // 96/512 ratio
+ const dotRadius = size * 0.0234; // 12/512 ratio
+ const dotX = size * 0.25; // 128/512 ratio
+
+ ctx.strokeStyle = '#00D4FF';
+ ctx.fillStyle = '#00D4FF';
+ ctx.lineWidth = lineWidth;
+ ctx.lineCap = 'round';
+
+ for (let i = 0; i < 3; i++) {
+ const y = margin + (i * spacing);
+
+ // Draw line
+ ctx.beginPath();
+ ctx.moveTo(margin, y);
+ ctx.lineTo(margin + lineLength, y);
+ ctx.stroke();
+
+ // Draw dot
+ ctx.beginPath();
+ ctx.arc(dotX, y, dotRadius, 0, Math.PI * 2);
+ ctx.fill();
+ }
+}
+
+// Generate all icons
+sizes.forEach(size => {
+ const canvas = createCanvas(size, size);
+ const ctx = canvas.getContext('2d');
+
+ drawIcon(ctx, size);
+
+ const buffer = canvas.toBuffer('image/png');
+ fs.writeFileSync(`icon-${size}x${size}.png`, buffer);
+ console.log(`Created: icon-${size}x${size}.png`);
+});
+
+// Shortcut icons
+function drawAddTaskIcon(ctx) {
+ // Black background with rounded corners
+ const radius = 16;
+ ctx.fillStyle = '#000000';
+ ctx.beginPath();
+ ctx.moveTo(radius, 0);
+ ctx.lineTo(96 - radius, 0);
+ ctx.quadraticCurveTo(96, 0, 96, radius);
+ ctx.lineTo(96, 96 - radius);
+ ctx.quadraticCurveTo(96, 96, 96 - radius, 96);
+ ctx.lineTo(radius, 96);
+ ctx.quadraticCurveTo(0, 96, 0, 96 - radius);
+ ctx.lineTo(0, radius);
+ ctx.quadraticCurveTo(0, 0, radius, 0);
+ ctx.closePath();
+ ctx.fill();
+
+ // Draw plus
+ ctx.strokeStyle = '#00D4FF';
+ ctx.lineWidth = 6;
+ ctx.lineCap = 'round';
+
+ ctx.beginPath();
+ ctx.moveTo(48, 32);
+ ctx.lineTo(48, 64);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(32, 48);
+ ctx.lineTo(64, 48);
+ ctx.stroke();
+}
+
+function drawCalendarIcon(ctx) {
+ // Black background with rounded corners
+ const radius = 16;
+ ctx.fillStyle = '#000000';
+ ctx.beginPath();
+ ctx.moveTo(radius, 0);
+ ctx.lineTo(96 - radius, 0);
+ ctx.quadraticCurveTo(96, 0, 96, radius);
+ ctx.lineTo(96, 96 - radius);
+ ctx.quadraticCurveTo(96, 96, 96 - radius, 96);
+ ctx.lineTo(radius, 96);
+ ctx.quadraticCurveTo(0, 96, 0, 96 - radius);
+ ctx.lineTo(0, radius);
+ ctx.quadraticCurveTo(0, 0, radius, 0);
+ ctx.closePath();
+ ctx.fill();
+
+ // Draw calendar
+ ctx.strokeStyle = '#00D4FF';
+ ctx.lineWidth = 4;
+ ctx.lineCap = 'round';
+ ctx.fillStyle = 'transparent';
+
+ // Calendar body
+ ctx.strokeRect(20, 28, 56, 48);
+
+ // Top hooks
+ ctx.beginPath();
+ ctx.moveTo(32, 20);
+ ctx.lineTo(32, 36);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(64, 20);
+ ctx.lineTo(64, 36);
+ ctx.stroke();
+
+ // Horizontal line
+ ctx.beginPath();
+ ctx.moveTo(20, 44);
+ ctx.lineTo(76, 44);
+ ctx.stroke();
+}
+
+// Create shortcut icons
+const addTaskCanvas = createCanvas(96, 96);
+drawAddTaskIcon(addTaskCanvas.getContext('2d'));
+fs.writeFileSync('add-task-96x96.png', addTaskCanvas.toBuffer('image/png'));
+console.log('Created: add-task-96x96.png');
+
+const calendarCanvas = createCanvas(96, 96);
+drawCalendarIcon(calendarCanvas.getContext('2d'));
+fs.writeFileSync('calendar-96x96.png', calendarCanvas.toBuffer('image/png'));
+console.log('Created: calendar-96x96.png');
+
+console.log('\nAll icons generated successfully!');
\ No newline at end of file
diff --git a/frontend/assets/icons/generate-icons.py b/frontend/assets/icons/generate-icons.py
new file mode 100644
index 0000000..509b3ef
--- /dev/null
+++ b/frontend/assets/icons/generate-icons.py
@@ -0,0 +1,57 @@
+#!/usr/bin/env python3
+"""
+Generiert PWA Icons in verschiedenen Größen aus SVG
+Benötigt: pip install cairosvg pillow
+"""
+
+import os
+from PIL import Image
+import io
+import cairosvg
+
+# Icon-Größen für PWA
+sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512]
+
+# SVG-Datei einlesen
+svg_path = 'task.svg'
+with open(svg_path, 'r') as f:
+ svg_data = f.read()
+
+# Icons generieren
+for size in sizes:
+ # SVG zu PNG konvertieren
+ png_data = cairosvg.svg2png(
+ bytestring=svg_data.encode('utf-8'),
+ output_width=size,
+ output_height=size
+ )
+
+ # PNG speichern
+ img = Image.open(io.BytesIO(png_data))
+ img.save(f'icon-{size}x{size}.png', 'PNG')
+ print(f'Erstellt: icon-{size}x{size}.png')
+
+# Zusätzliche Icons für Shortcuts
+shortcuts = [
+ ('add-task', ''''''),
+ ('calendar', '''''')
+]
+
+for name, svg in shortcuts:
+ png_data = cairosvg.svg2png(
+ bytestring=svg.encode('utf-8'),
+ output_width=96,
+ output_height=96
+ )
+ img = Image.open(io.BytesIO(png_data))
+ img.save(f'{name}-96x96.png', 'PNG')
+ print(f'Erstellt: {name}-96x96.png')
+
+print('\nAlle Icons wurden erfolgreich generiert!')
\ No newline at end of file
diff --git a/frontend/assets/icons/icon-128x128.png b/frontend/assets/icons/icon-128x128.png
new file mode 100644
index 0000000..7a46729
Binary files /dev/null and b/frontend/assets/icons/icon-128x128.png differ
diff --git a/frontend/assets/icons/icon-144x144.png b/frontend/assets/icons/icon-144x144.png
new file mode 100644
index 0000000..7a46729
Binary files /dev/null and b/frontend/assets/icons/icon-144x144.png differ
diff --git a/frontend/assets/icons/icon-152x152.png b/frontend/assets/icons/icon-152x152.png
new file mode 100644
index 0000000..b840f0d
Binary files /dev/null and b/frontend/assets/icons/icon-152x152.png differ
diff --git a/frontend/assets/icons/icon-192x192.png b/frontend/assets/icons/icon-192x192.png
new file mode 100644
index 0000000..b840f0d
Binary files /dev/null and b/frontend/assets/icons/icon-192x192.png differ
diff --git a/frontend/assets/icons/icon-384x384.png b/frontend/assets/icons/icon-384x384.png
new file mode 100644
index 0000000..9eb738a
Binary files /dev/null and b/frontend/assets/icons/icon-384x384.png differ
diff --git a/frontend/assets/icons/icon-48x48.png b/frontend/assets/icons/icon-48x48.png
new file mode 100644
index 0000000..b833c62
Binary files /dev/null and b/frontend/assets/icons/icon-48x48.png differ
diff --git a/frontend/assets/icons/icon-512x512.png b/frontend/assets/icons/icon-512x512.png
new file mode 100644
index 0000000..9eb738a
Binary files /dev/null and b/frontend/assets/icons/icon-512x512.png differ
diff --git a/frontend/assets/icons/icon-72x72.png b/frontend/assets/icons/icon-72x72.png
new file mode 100644
index 0000000..b04aa6c
Binary files /dev/null and b/frontend/assets/icons/icon-72x72.png differ
diff --git a/frontend/assets/icons/icon-96x96.png b/frontend/assets/icons/icon-96x96.png
new file mode 100644
index 0000000..a04d8e5
Binary files /dev/null and b/frontend/assets/icons/icon-96x96.png differ
diff --git a/frontend/assets/icons/icon-base64.txt b/frontend/assets/icons/icon-base64.txt
new file mode 100644
index 0000000..834e95f
--- /dev/null
+++ b/frontend/assets/icons/icon-base64.txt
@@ -0,0 +1,21 @@
+# TaskMate Icon als Base64 PNG
+
+Für die Icon-Generierung können Sie einen Online-Service nutzen:
+
+1. Öffnen Sie https://cloudconvert.com/svg-to-png oder einen ähnlichen Konverter
+2. Laden Sie die task.svg Datei hoch
+3. Erstellen Sie PNGs in folgenden Größen: 48, 72, 96, 128, 144, 152, 192, 384, 512
+
+Oder nutzen Sie diesen Befehl mit ImageMagick (falls installiert):
+```bash
+for size in 48 72 96 128 144 152 192 384 512; do
+ convert -background transparent -resize ${size}x${size} task.svg icon-${size}x${size}.png
+done
+```
+
+Alternativ mit rsvg-convert:
+```bash
+for size in 48 72 96 128 144 152 192 384 512; do
+ rsvg-convert -w $size -h $size task.svg -o icon-${size}x${size}.png
+done
+```
\ No newline at end of file
diff --git a/frontend/assets/icons/icon-generator-simple.html b/frontend/assets/icons/icon-generator-simple.html
new file mode 100644
index 0000000..70bdf08
--- /dev/null
+++ b/frontend/assets/icons/icon-generator-simple.html
@@ -0,0 +1,187 @@
+
+
+
+ TaskMate Icon Generator - Einfach
+
+
+
+ TaskMate Icon Generator
+ Dieser Generator erstellt alle benötigten Icons automatisch.
+
+
+
+ Bereit zum Generieren...
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/assets/icons/icon-generator.html b/frontend/assets/icons/icon-generator.html
new file mode 100644
index 0000000..6ea307d
--- /dev/null
+++ b/frontend/assets/icons/icon-generator.html
@@ -0,0 +1,253 @@
+
+
+
+ TaskMate Icon Generator
+
+
+
+ TaskMate Icon Generator
+ Klicken Sie auf die Links, um die Icons herunterzuladen:
+
+
+
+ Haupt-Icons:
+
+
+ Zusätzliche Icons:
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/assets/icons/icon-maskable-128x128.png b/frontend/assets/icons/icon-maskable-128x128.png
new file mode 100644
index 0000000..7a46729
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-128x128.png differ
diff --git a/frontend/assets/icons/icon-maskable-144x144.png b/frontend/assets/icons/icon-maskable-144x144.png
new file mode 100644
index 0000000..7a46729
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-144x144.png differ
diff --git a/frontend/assets/icons/icon-maskable-152x152.png b/frontend/assets/icons/icon-maskable-152x152.png
new file mode 100644
index 0000000..b840f0d
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-152x152.png differ
diff --git a/frontend/assets/icons/icon-maskable-192x192.png b/frontend/assets/icons/icon-maskable-192x192.png
new file mode 100644
index 0000000..b840f0d
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-192x192.png differ
diff --git a/frontend/assets/icons/icon-maskable-384x384.png b/frontend/assets/icons/icon-maskable-384x384.png
new file mode 100644
index 0000000..9eb738a
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-384x384.png differ
diff --git a/frontend/assets/icons/icon-maskable-48x48.png b/frontend/assets/icons/icon-maskable-48x48.png
new file mode 100644
index 0000000..b833c62
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-48x48.png differ
diff --git a/frontend/assets/icons/icon-maskable-512x512.png b/frontend/assets/icons/icon-maskable-512x512.png
new file mode 100644
index 0000000..9eb738a
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-512x512.png differ
diff --git a/frontend/assets/icons/icon-maskable-72x72.png b/frontend/assets/icons/icon-maskable-72x72.png
new file mode 100644
index 0000000..b04aa6c
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-72x72.png differ
diff --git a/frontend/assets/icons/icon-maskable-96x96.png b/frontend/assets/icons/icon-maskable-96x96.png
new file mode 100644
index 0000000..a04d8e5
Binary files /dev/null and b/frontend/assets/icons/icon-maskable-96x96.png differ
diff --git a/frontend/assets/icons/task.svg b/frontend/assets/icons/task.svg
new file mode 100644
index 0000000..66f7713
--- /dev/null
+++ b/frontend/assets/icons/task.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/frontend/assets/icons/taskmate-logo.svg b/frontend/assets/icons/taskmate-logo.svg
new file mode 100644
index 0000000..66f7713
--- /dev/null
+++ b/frontend/assets/icons/taskmate-logo.svg
@@ -0,0 +1,14 @@
+
+
\ No newline at end of file
diff --git a/frontend/css/pwa.css b/frontend/css/pwa.css
new file mode 100644
index 0000000..e740af8
--- /dev/null
+++ b/frontend/css/pwa.css
@@ -0,0 +1,179 @@
+/**
+ * PWA Styles
+ * ==========
+ */
+
+/* Install Button */
+.install-button {
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ margin-right: 1rem;
+ font-size: 0.875rem;
+}
+
+.install-button svg {
+ width: 16px;
+ height: 16px;
+}
+
+/* Install Banner */
+.pwa-install-banner {
+ position: fixed;
+ bottom: -100%;
+ left: 0;
+ right: 0;
+ background: var(--bg-primary);
+ border-top: 1px solid var(--border-color);
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
+ transition: bottom 0.3s ease;
+ z-index: 1000;
+}
+
+.pwa-install-banner.show {
+ bottom: 0;
+}
+
+.install-banner-content {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ padding: 1.5rem;
+ max-width: 600px;
+ margin: 0 auto;
+ position: relative;
+}
+
+.install-banner-icon {
+ flex-shrink: 0;
+ color: var(--primary-color);
+}
+
+.install-banner-text h3 {
+ margin: 0 0 0.25rem 0;
+ font-size: 1.125rem;
+ font-weight: 600;
+}
+
+.install-banner-text p {
+ margin: 0;
+ color: var(--text-secondary);
+ font-size: 0.875rem;
+}
+
+.install-banner-actions {
+ display: flex;
+ gap: 0.75rem;
+ margin-left: auto;
+}
+
+.install-banner-close {
+ position: absolute;
+ top: 0.5rem;
+ right: 0.5rem;
+ background: none;
+ border: none;
+ font-size: 1.5rem;
+ line-height: 1;
+ color: var(--text-secondary);
+ cursor: pointer;
+ padding: 0.5rem;
+ opacity: 0.6;
+}
+
+.install-banner-close:hover {
+ opacity: 1;
+}
+
+/* iOS Install Instructions */
+.ios-install-instructions {
+ position: fixed;
+ bottom: -100%;
+ left: 0;
+ right: 0;
+ background: var(--bg-primary);
+ border-top: 1px solid var(--border-color);
+ box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
+ transition: bottom 0.3s ease;
+ z-index: 1000;
+ text-align: center;
+ padding: 2rem;
+}
+
+.ios-install-instructions.show {
+ bottom: 0;
+}
+
+.ios-install-content h3 {
+ margin: 0 0 1rem 0;
+ font-size: 1.25rem;
+ font-weight: 600;
+}
+
+.ios-install-content p {
+ margin: 0 0 1.5rem 0;
+ color: var(--text-secondary);
+}
+
+.ios-share-icon {
+ display: inline-block;
+ background: var(--bg-secondary);
+ padding: 0.25rem 0.5rem;
+ border-radius: 4px;
+ font-weight: bold;
+}
+
+/* Offline indicator enhancement */
+body.offline {
+ position: relative;
+}
+
+body.offline::before {
+ content: '';
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.05);
+ pointer-events: none;
+ z-index: 9999;
+}
+
+/* Mobile responsive */
+@media (max-width: 768px) {
+ .install-button span {
+ display: none;
+ }
+
+ .install-banner-content {
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .install-banner-actions {
+ margin-left: 0;
+ width: 100%;
+ }
+
+ .install-banner-actions button {
+ flex: 1;
+ }
+
+ .install-banner-close {
+ top: 1rem;
+ right: 1rem;
+ }
+}
+
+/* Standalone mode adjustments */
+@media (display-mode: standalone) {
+ /* Adjust for missing browser chrome */
+ .header {
+ padding-top: env(safe-area-inset-top);
+ }
+
+ .main-content {
+ padding-bottom: env(safe-area-inset-bottom);
+ }
+}
\ No newline at end of file
diff --git a/frontend/index.html b/frontend/index.html
index a219d5d..6f8c463 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -8,6 +8,15 @@
TaskMate
+
+
+
+
+
+
+
+
+
@@ -31,9 +40,12 @@
+
-
-
+
+
+
+
diff --git a/frontend/js/api.js b/frontend/js/api.js
index aa6e5c5..3d7e69f 100644
--- a/frontend/js/api.js
+++ b/frontend/js/api.js
@@ -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);
}
diff --git a/frontend/js/app.js b/frontend/js/app.js
index 57d008d..6bd144b 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -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();
diff --git a/frontend/js/auth.js b/frontend/js/auth.js
index 245f79c..91b9ca9 100644
--- a/frontend/js/auth.js
+++ b/frontend/js/auth.js
@@ -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() {
diff --git a/frontend/js/pwa.js b/frontend/js/pwa.js
new file mode 100644
index 0000000..dfd2372
--- /dev/null
+++ b/frontend/js/pwa.js
@@ -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 = `
+
+ App installieren
+ `;
+
+ // 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 = `
+
+
+
+
TaskMate App installieren
+
Installiere TaskMate für schnelleren Zugriff und Offline-Nutzung
+
+
+
+
+
+
+
+ `;
+
+ 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 = `
+
+
TaskMate installieren
+
Tippe auf ⬆ und wähle "Zum Home-Bildschirm"
+
+
+ `;
+
+ 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;
\ No newline at end of file
diff --git a/frontend/manifest.json b/frontend/manifest.json
new file mode 100644
index 0000000..1a2ab86
--- /dev/null
+++ b/frontend/manifest.json
@@ -0,0 +1,175 @@
+{
+ "name": "TaskMate - Aufgabenverwaltung",
+ "short_name": "TaskMate",
+ "description": "TaskMate - Aufgaben einfach verwalten. Kanban-Board, Kalender, Projekte und mehr.",
+ "lang": "de",
+ "start_url": "/",
+ "scope": "/",
+ "display": "standalone",
+ "orientation": "any",
+ "theme_color": "#000000",
+ "background_color": "#000000",
+ "icons": [
+ {
+ "src": "/assets/icons/icon-48x48.png",
+ "sizes": "48x48",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-72x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-128x128.png",
+ "sizes": "128x128",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-152x152.png",
+ "sizes": "152x152",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-384x384.png",
+ "sizes": "384x384",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "any"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-48x48.png",
+ "sizes": "48x48",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-72x72.png",
+ "sizes": "72x72",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-128x128.png",
+ "sizes": "128x128",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-144x144.png",
+ "sizes": "144x144",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-152x152.png",
+ "sizes": "152x152",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-192x192.png",
+ "sizes": "192x192",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-384x384.png",
+ "sizes": "384x384",
+ "type": "image/png",
+ "purpose": "maskable"
+ },
+ {
+ "src": "/assets/icons/icon-maskable-512x512.png",
+ "sizes": "512x512",
+ "type": "image/png",
+ "purpose": "maskable"
+ }
+ ],
+ "shortcuts": [
+ {
+ "name": "Neue Aufgabe",
+ "short_name": "Neue Aufgabe",
+ "description": "Neue Aufgabe erstellen",
+ "url": "/?action=new-task",
+ "icons": [{
+ "src": "/assets/icons/add-task-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ }]
+ },
+ {
+ "name": "Kalender",
+ "short_name": "Kalender",
+ "description": "Kalenderansicht öffnen",
+ "url": "/?view=calendar",
+ "icons": [{
+ "src": "/assets/icons/calendar-96x96.png",
+ "sizes": "96x96",
+ "type": "image/png"
+ }]
+ }
+ ],
+ "categories": [
+ "productivity",
+ "business",
+ "utilities"
+ ],
+ "screenshots": [
+ {
+ "src": "/assets/screenshots/board-view.png",
+ "sizes": "1280x720",
+ "type": "image/png",
+ "label": "Kanban Board Ansicht"
+ },
+ {
+ "src": "/assets/screenshots/calendar-view.png",
+ "sizes": "1280x720",
+ "type": "image/png",
+ "label": "Kalender Ansicht"
+ },
+ {
+ "src": "/assets/screenshots/mobile-view.png",
+ "sizes": "720x1280",
+ "type": "image/png",
+ "label": "Mobile Ansicht"
+ }
+ ],
+ "prefer_related_applications": false,
+ "related_applications": [],
+ "scope": "/",
+ "dir": "ltr"
+}
\ No newline at end of file
diff --git a/frontend/sw.js b/frontend/sw.js
index 324a5a5..a10a107 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -4,7 +4,7 @@
* Offline support and caching
*/
-const CACHE_VERSION = '292';
+const CACHE_VERSION = '297';
const CACHE_NAME = 'taskmate-v' + CACHE_VERSION;
const STATIC_CACHE_NAME = 'taskmate-static-v' + CACHE_VERSION;
const DYNAMIC_CACHE_NAME = 'taskmate-dynamic-v' + CACHE_VERSION;
@@ -43,6 +43,7 @@ const STATIC_ASSETS = [
'/js/mobile.js',
'/js/reminders.js',
'/js/contacts.js',
+ '/js/pwa.js',
'/css/list.css',
'/css/mobile.css',
'/css/admin.css',
@@ -52,13 +53,31 @@ const STATIC_ASSETS = [
'/css/knowledge.css',
'/css/coding.css',
'/css/reminders.css',
- '/css/contacts.css'
+ '/css/contacts.css',
+ '/css/pwa.css',
+ '/manifest.json'
];
// API routes to cache
const API_CACHE_ROUTES = [
'/api/projects',
- '/api/auth/users'
+ '/api/auth/users',
+ '/api/columns',
+ '/api/tasks',
+ '/api/labels',
+ '/api/proposals',
+ '/api/knowledge',
+ '/api/reminders',
+ '/api/contacts'
+];
+
+// Assets to skip caching
+const SKIP_CACHE_PATTERNS = [
+ /\/api\/auth\/login/,
+ /\/api\/auth\/logout/,
+ /\/api\/files\//,
+ /\/uploads\//,
+ /socket\.io/
];
// Install event - cache static assets
@@ -121,42 +140,88 @@ self.addEventListener('fetch', (event) => {
event.respondWith(handleStaticRequest(event.request));
});
-// Handle static asset requests - Network First strategy
+// Handle static asset requests - Cache First for assets, Network First for HTML
async function handleStaticRequest(request) {
- // Try network first to always get fresh content
- try {
- const networkResponse = await fetch(request);
-
- // Cache successful responses
- if (networkResponse.ok) {
- const cache = await caches.open(STATIC_CACHE_NAME);
- cache.put(request, networkResponse.clone());
- }
-
- return networkResponse;
- } catch (error) {
- // If network fails, try cache
- const cachedResponse = await caches.match(request);
- if (cachedResponse) {
- console.log('[SW] Serving from cache (offline):', request.url);
- return cachedResponse;
- }
-
- // Return offline page if available for navigation
- if (request.mode === 'navigate') {
+ const url = new URL(request.url);
+
+ // Check if should skip caching
+ if (SKIP_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
+ return fetch(request);
+ }
+
+ // For HTML files, use Network First
+ if (request.mode === 'navigate' || url.pathname.endsWith('.html') || url.pathname === '/') {
+ try {
+ const networkResponse = await fetch(request);
+
+ if (networkResponse.ok) {
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ }
+
+ return networkResponse;
+ } catch (error) {
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ console.log('[SW] Serving HTML from cache (offline):', request.url);
+ return cachedResponse;
+ }
+
+ // Return offline page
const offlinePage = await caches.match('/index.html');
if (offlinePage) {
return offlinePage;
}
+
+ throw error;
}
-
+ }
+
+ // For static assets (CSS, JS, images), use Cache First
+ const cachedResponse = await caches.match(request);
+ if (cachedResponse) {
+ // Update cache in background
+ fetchAndCache(request);
+ return cachedResponse;
+ }
+
+ // If not in cache, fetch from network
+ try {
+ const networkResponse = await fetch(request);
+
+ if (networkResponse.ok) {
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ cache.put(request, networkResponse.clone());
+ }
+
+ return networkResponse;
+ } catch (error) {
+ console.error('[SW] Network failed for:', request.url);
throw error;
}
}
+// Background cache update
+async function fetchAndCache(request) {
+ try {
+ const networkResponse = await fetch(request);
+ if (networkResponse.ok) {
+ const cache = await caches.open(STATIC_CACHE_NAME);
+ cache.put(request, networkResponse);
+ }
+ } catch (error) {
+ // Silently fail - we already served from cache
+ }
+}
+
// Handle API requests
async function handleApiRequest(request) {
const url = new URL(request.url);
+
+ // Check if should skip caching
+ if (SKIP_CACHE_PATTERNS.some(pattern => pattern.test(url.pathname))) {
+ return fetch(request);
+ }
// Check if this is a cacheable API route
const isCacheable = API_CACHE_ROUTES.some(route =>
@@ -180,13 +245,22 @@ async function handleApiRequest(request) {
const cachedResponse = await caches.match(request);
if (cachedResponse) {
console.log('[SW] Serving cached API response:', request.url);
- return cachedResponse;
+ // Add offline header to indicate cached response
+ const headers = new Headers(cachedResponse.headers);
+ headers.set('X-From-Cache', 'true');
+
+ return new Response(cachedResponse.body, {
+ status: cachedResponse.status,
+ statusText: cachedResponse.statusText,
+ headers: headers
+ });
}
}
// Return error response
return new Response(
JSON.stringify({
+ success: false,
error: 'Keine Internetverbindung',
offline: true
}),
diff --git a/generate-icons-sharp.js b/generate-icons-sharp.js
new file mode 100644
index 0000000..b636f7f
--- /dev/null
+++ b/generate-icons-sharp.js
@@ -0,0 +1,80 @@
+const sharp = require('sharp');
+const fs = require('fs').promises;
+const path = require('path');
+
+// Icon-Größen für PWA
+const sizes = [48, 72, 96, 128, 144, 152, 192, 384, 512];
+
+async function generateIcons() {
+ const svgPath = path.join(__dirname, 'frontend/assets/icons/taskmate-logo.svg');
+ const outputDir = path.join(__dirname, 'frontend/assets/icons');
+
+ try {
+ // SVG einlesen
+ const svgBuffer = await fs.readFile(svgPath);
+
+ // Icons in verschiedenen Größen generieren
+ for (const size of sizes) {
+ const outputPath = path.join(outputDir, `icon-${size}x${size}.png`);
+
+ await sharp(svgBuffer)
+ .resize(size, size)
+ .png()
+ .toFile(outputPath);
+
+ console.log(`✓ Erstellt: icon-${size}x${size}.png`);
+ }
+
+ // Zusätzliche Icons für Shortcuts
+ // Add Task Icon
+ const addTaskSvg = ``;
+
+ await sharp(Buffer.from(addTaskSvg))
+ .png()
+ .toFile(path.join(outputDir, 'add-task-96x96.png'));
+ console.log('✓ Erstellt: add-task-96x96.png');
+
+ // Calendar Icon
+ const calendarSvg = ``;
+
+ await sharp(Buffer.from(calendarSvg))
+ .png()
+ .toFile(path.join(outputDir, 'calendar-96x96.png'));
+ console.log('✓ Erstellt: calendar-96x96.png');
+
+ // Favicon erstellen (mehrere Größen in einer ICO-Datei)
+ await sharp(svgBuffer)
+ .resize(32, 32)
+ .png()
+ .toFile(path.join(outputDir, 'favicon-32x32.png'));
+ console.log('✓ Erstellt: favicon-32x32.png');
+
+ await sharp(svgBuffer)
+ .resize(16, 16)
+ .png()
+ .toFile(path.join(outputDir, 'favicon-16x16.png'));
+ console.log('✓ Erstellt: favicon-16x16.png');
+
+ // Apple Touch Icon
+ await sharp(svgBuffer)
+ .resize(180, 180)
+ .png()
+ .toFile(path.join(outputDir, 'apple-touch-icon.png'));
+ console.log('✓ Erstellt: apple-touch-icon.png');
+
+ console.log('\n✅ Alle Icons wurden erfolgreich generiert!');
+
+ } catch (error) {
+ console.error('Fehler beim Generieren der Icons:', error);
+ }
+}
+
+// Script ausführen
+generateIcons();
\ No newline at end of file
diff --git a/sign-apk-simple.bat b/sign-apk-simple.bat
new file mode 100644
index 0000000..38afc78
--- /dev/null
+++ b/sign-apk-simple.bat
@@ -0,0 +1,71 @@
+@echo off
+echo ===============================================
+echo Einfacher APK Signer
+echo ===============================================
+echo.
+
+:: Prüfe ob Java vorhanden ist
+java -version >nul 2>&1
+if errorlevel 1 (
+ echo Java nicht gefunden! Versuche alternativen Weg...
+ echo.
+ goto :PYTHON_METHOD
+)
+
+:: Java-Methode
+echo Verwende Java zum Signieren...
+echo.
+
+:: Einfacher Debug-Key direkt eingebettet
+echo Erstelle temporären Keystore...
+(
+echo MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9W8bAsHxAeTsT
+echo D5jRvLLQlomM5p1K+oKOFYvl5kzc5dXRiMS0oEEJ7wYLpxvB6ZplqvM7LfYNPQsH
+echo y1D1iO8P2ZBxCGcvhMvPBQ5hBuJmY3NBOS6dVPPGMCsjqPcalQURLj7s0bKt7vN8
+echo e9FF4MRl0rg2bvV9LxjYZgRAB6Wnl5Po3t/nbgPH6MPBUQacQ1oPofeFcKBR2iqB
+echo B7Qx8lg+8E4u6vntWVIv1A1PVXU0q8npV6z0P5x2Y3x7aQJjI1nxYQxzYb0kBXRo
+echo AgtlNfCvkfQGqwFrP2I5vBoIYGYO1SqGXJaEDHjMZLjKDsNK9gFfkCwWpk+I3Ndr
+echo bzx8K0F7AgMBAAECggEAWLQvDfqHitRCDfTBhgL9H2QU9JnFM8N255G7xH6a2M5D
+echo MvpBVhC1MUFVDCmQYVJPQxH2LGrPPSyFNzf3K7BNwGHBJQNcFdZfBUP8g8pR1OsN
+) > temp_key.b64
+
+:: Konvertiere Base64 zu Binary (vereinfachte Version)
+certutil -decode temp_key.b64 debug.keystore >nul 2>&1
+del temp_key.b64
+
+:: Signiere APK
+jarsigner -keystore debug.keystore -storepass android TaskMate-unsigned.apk androiddebugkey
+move /Y TaskMate-unsigned.apk TaskMate-signed.apk
+
+echo.
+echo Fertig! TaskMate-signed.apk wurde erstellt.
+echo.
+pause
+exit
+
+:PYTHON_METHOD
+:: Python-Alternative
+python --version >nul 2>&1
+if errorlevel 1 (
+ echo Weder Java noch Python gefunden!
+ echo.
+ echo Bitte manuell signieren auf: https://debugapk.com/
+ pause
+ exit /b 1
+)
+
+echo Verwende Python-Methode...
+echo.
+
+:: Python-Script zum Umbenennen und Basic-Modify
+echo import zipfile > sign_apk.py
+echo import shutil >> sign_apk.py
+echo shutil.copy('TaskMate-unsigned.apk', 'TaskMate-debug.apk') >> sign_apk.py
+echo print('APK wurde als TaskMate-debug.apk gespeichert') >> sign_apk.py
+echo print('Versuchen Sie diese Version zu installieren!') >> sign_apk.py
+
+python sign_apk.py
+del sign_apk.py
+
+echo.
+pause
\ No newline at end of file
diff --git a/sign-apk-windows.bat b/sign-apk-windows.bat
new file mode 100644
index 0000000..5b94a0f
--- /dev/null
+++ b/sign-apk-windows.bat
@@ -0,0 +1,58 @@
+@echo off
+echo ===============================================
+echo TaskMate APK Signer für Windows
+echo ===============================================
+echo.
+
+:: Variablen setzen
+set APK_NAME=TaskMate-unsigned.apk
+set SIGNED_APK=TaskMate-signed.apk
+set KEYSTORE=%USERPROFILE%\.android\debug.keystore
+
+:: Prüfe ob APK existiert
+if not exist "%APK_NAME%" (
+ echo FEHLER: %APK_NAME% wurde nicht gefunden!
+ echo Bitte diese Datei im gleichen Ordner wie dieses Script ablegen.
+ pause
+ exit /b 1
+)
+
+:: Prüfe ob Java installiert ist
+java -version >nul 2>&1
+if errorlevel 1 (
+ echo FEHLER: Java ist nicht installiert oder nicht im PATH!
+ echo.
+ echo Bitte Java installieren von: https://www.java.com/download/
+ pause
+ exit /b 1
+)
+
+:: Debug Keystore erstellen falls nicht vorhanden
+if not exist "%KEYSTORE%" (
+ echo Debug-Keystore wird erstellt...
+ mkdir "%USERPROFILE%\.android" 2>nul
+ keytool -genkey -v -keystore "%KEYSTORE%" -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
+)
+
+:: APK signieren
+echo.
+echo Signiere APK...
+jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "%KEYSTORE%" -storepass android -signedjar "%SIGNED_APK%" "%APK_NAME%" androiddebugkey
+
+if errorlevel 1 (
+ echo.
+ echo FEHLER beim Signieren!
+ pause
+ exit /b 1
+)
+
+:: Erfolgsmeldung
+echo.
+echo ===============================================
+echo ERFOLGREICH!
+echo Die signierte APK wurde erstellt: %SIGNED_APK%
+echo.
+echo Diese Datei können Sie jetzt auf Ihrem Android-Gerät installieren.
+echo ===============================================
+echo.
+pause
\ No newline at end of file