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