Status: Accepted Datum: 2025-12-18 Entscheider: Entwicklungsteam Bezieht sich auf: ADR-013 React Frontend, ADR-015 Turso Database
Kontext
Die DurchfΓΌhrungs-App muss im Schwimmbad auf Tablets funktionieren:
Herausforderungen:
- π SchwimmbΓ€der haben oft schlechte/keine Internetverbindung
- π± Verschiedene GerΓ€te: iPads, Android-Tablets, evtl. Laptops
- β‘ Live-Bewertung darf nicht durch Netzwerkprobleme unterbrochen werden
- π₯ Ehrenamtliche Helfer mΓΌssen App ohne Installation nutzen kΓΆnnen
Anforderungen:
- Offline-FΓ€higkeit fΓΌr kritische Funktionen (Bewertung erfassen)
- App-Γ€hnliches Erlebnis (Home-Screen-Icon, Fullscreen)
- Kein App-Store nΓΆtig (keine Kosten, keine Wartezeit)
- Automatische Updates
- Schnelle Ladezeiten trotz mobilem Netz
Entscheidung
Wir entwickeln die DurchfΓΌhrungs-App als Progressive Web App (PWA) mit:
- Service Worker fΓΌr Offline-FunktionalitΓ€t
- Workbox fΓΌr Caching-Strategien
- Web App Manifest fΓΌr installierbare App
- Embedded Turso Replica fΓΌr lokale Daten
PWA-Architektur
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β Browser (Safari/Chrome) β
β β
β βββββββββββββββββββββββββββββββββββββββββββββ β
β β React App (UI Layer) β β
β β - Bewertungs-Formulare β β
β β - Durchgangs-Γbersicht β β
β β - Offline-Status-Anzeige β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββΌβββββββββββββββββββββββββββββ β
β β Service Worker (Workbox) β β
β β β β
β β ββββββββββββββ ββββββββββββββββββββββ β β
β β β Cache API β β Background Sync β β β
β β β (Assets) β β (Pending Writes) β β β
β β ββββββββββββββ ββββββββββββββββββββββ β β
β ββββββββββββββββ¬βββββββββββββββββββββββββββββ β
β β β
β ββββββββββββββββΌβββββββββββββββββββββββββββββ β
β β IndexedDB / libSQL Replica β β
β β - Lokale Kopie der Wettkampf-Daten β β
β β - Offline Writes Queue β β
β βββββββββββββββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β
β Sync when online
βΌ
βββββββββββββββββββββββββ
β Backend API β
β + Turso Cloud β
βββββββββββββββββββββββββ
BegrΓΌndung
Pro PWA
Vorteile:
- β Keine Installation nΓΆtig: URL ΓΆffnen, βZum Home-Bildschirmβ
- β Plattform-unabhΓ€ngig: iOS, Android, Windows, macOS
- β Automatische Updates: Neue Version bei nΓ€chstem Laden
- β Offline-FΓ€higkeit: Service Worker cacht App + Daten
- β Schneller Start: Assets aus Cache, keine Downloads
- β App-Γ€hnlich: Fullscreen, eigenes Icon, keine Browser-UI
- β Eine Codebasis: Kein nativer Code pro Plattform
- β Keine App-Store-GebΓΌhren: 0β¬ statt 99β¬/Jahr (Apple)
FΓΌr Aquarius:
- β Kleine Liga (20 Kinder) β App Store lohnt sich nicht
- β Ehrenamtliche Helfer β Einfache Nutzung ohne Installation
- β Schwimmbad-Internet β Offline-FΓ€higkeit kritisch
Alternative: Native App (Swift/Kotlin)
Pro:
- β Beste Performance
- β Voller Zugriff auf GerΓ€te-APIs
Contra:
- β 2 Codebasen: iOS (Swift) + Android (Kotlin/Java)
- β App-Store-Prozess: Review-Zeit, GebΓΌhren
- β Entwicklungsaufwand: 2-3x lΓ€nger
- β Updates: User mΓΌssen manuell aktualisieren
Entscheidung gegen Native: Zu hoher Aufwand fΓΌr kleine Liga
Alternative: React Native / Flutter
Pro:
- β Eine Codebasis fΓΌr iOS + Android
- β Gute Performance
Contra:
- β Trotzdem App-Store: Installation + Review nΓΆtig
- β Build-KomplexitΓ€t: Xcode, Android Studio
- β Native-AbhΓ€ngigkeiten: Platform-spezifische Bugs
- β Keine Desktop-Version: Planungs-App wΓ€re separate Codebasis
Entscheidung gegen React Native: PWA reicht aus, weniger KomplexitΓ€t
Alternative: Electron App
Pro:
- β Desktop-App mit Web-Technologie
Contra:
- β Keine Mobile-UnterstΓΌtzung: Tablets ausgeschlossen
- β Installation nΓΆtig: Download + Setup
- β GroΓe Bundle-Size: Chromium mitgeliefert
Entscheidung gegen Electron: Mobile ist Hauptfokus
Konsequenzen
Positiv
- Schnelle Entwicklung: Eine Codebasis fΓΌr alle Plattformen
- Offline-First: Bewertung funktioniert ohne Internet
- Einfache Distribution: URL teilen statt App Store
- Automatische Updates: Neue Features sofort verfΓΌgbar
- Niedrige Kosten: Kein App Store, keine Device-Testing-Farm
Negativ
- iOS-Limitierungen: Safari hat eingeschrΓ€nkte PWA-Features
- Kein App-Store-Listing: Discoverability schlechter (aber irrelevant fΓΌr geschlossene Liga)
- Browser-AbhΓ€ngigkeit: Safari/Chrome Updates kΓΆnnen App brechen
- Storage-Limits: IndexedDB hat GrΓΆΓenbeschrΓ€nkungen (aber ausreichend)
iOS-spezifische EinschrΓ€nkungen
| Feature | iOS Safari | Android Chrome |
|---|---|---|
| Installierbar | β (seit iOS 11.3) | β |
| Service Worker | β (seit iOS 11.3) | β |
| Background Sync | β | β |
| Push Notifications | β (Stand 2024) | β |
| Fullscreen | β οΈ (Partial) | β |
| Offline Storage | β (50 MB Limit) | β (Quota-based) |
Mitigation: Background Sync nicht kritisch, da Sync manuell getriggert werden kann
Risiken
| Risiko | Wahrscheinlichkeit | Impact | Mitigation |
|---|---|---|---|
| iOS lΓΆscht Cache zu aggressiv | Mittel | Hoch | Embedded Turso Replica statt nur Cache |
| Storage-Quota ΓΌberschritten | Niedrig | Mittel | Alte Daten periodisch lΓΆschen |
| Service Worker Bugs | Niedrig | Hoch | GrΓΌndliches Testing, Fallback auf Online-Modus |
Implementierung
1. Web App Manifest
// apps/execution/public/manifest.json
{
"name": "Aquarius DurchfΓΌhrung",
"short_name": "Aquarius",
"description": "Wettkampf-DurchfΓΌhrung und Live-Bewertung",
"start_url": "/",
"display": "standalone",
"background_color": "#0ea5e9",
"theme_color": "#0ea5e9",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "any maskable"
},
{
"src": "/icons/icon-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"categories": ["sports", "utilities"],
"screenshots": [
{
"src": "/screenshots/bewertung.png",
"sizes": "1170x2532",
"type": "image/png"
}
]
}
2. Service Worker (Workbox)
// apps/execution/src/service-worker.ts
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
import { ExpirationPlugin } from 'workbox-expiration';
// Precache all build assets
precacheAndRoute(self.__WB_MANIFEST);
// API Requests: Network First (mit Cache-Fallback)
registerRoute(
({ url }) => url.pathname.startsWith('/api/'),
new NetworkFirst({
cacheName: 'api-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 50,
maxAgeSeconds: 5 * 60, // 5 Minuten
}),
],
})
);
// Bilder: Cache First
registerRoute(
({ request }) => request.destination === 'image',
new CacheFirst({
cacheName: 'images-cache',
plugins: [
new ExpirationPlugin({
maxEntries: 60,
maxAgeSeconds: 30 * 24 * 60 * 60, // 30 Tage
}),
],
})
);
// HTML: Stale While Revalidate
registerRoute(
({ request }) => request.mode === 'navigate',
new StaleWhileRevalidate({
cacheName: 'pages-cache',
})
);
3. Offline-Status-Komponente
// apps/execution/src/components/OfflineIndicator.tsx
import { useEffect, useState } from 'react';
export function OfflineIndicator() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
useEffect(() => {
const handleOnline = () => setIsOnline(true);
const handleOffline = () => setIsOnline(false);
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
}, []);
if (isOnline) return null;
return (
<div className="fixed top-0 left-0 right-0 bg-yellow-500 text-white px-4 py-2 text-center">
β οΈ Offline-Modus: Daten werden lokal gespeichert und spΓ€ter synchronisiert
</div>
);
}
4. Installation-Prompt
// apps/execution/src/hooks/useInstallPrompt.ts
import { useState, useEffect } from 'react';
export function useInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null);
const [isInstallable, setIsInstallable] = useState(false);
useEffect(() => {
const handler = (e: Event) => {
e.preventDefault();
setDeferredPrompt(e);
setIsInstallable(true);
};
window.addEventListener('beforeinstallprompt', handler);
return () => window.removeEventListener('beforeinstallprompt', handler);
}, []);
const promptInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const { outcome } = await deferredPrompt.userChoice;
if (outcome === 'accepted') {
setIsInstallable(false);
}
setDeferredPrompt(null);
};
return { isInstallable, promptInstall };
}
5. Vite PWA Plugin
// apps/execution/vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { VitePWA } from 'vite-plugin-pwa';
export default defineConfig({
plugins: [
react(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['favicon.ico', 'robots.txt', 'icons/*.png'],
manifest: {
name: 'Aquarius DurchfΓΌhrung',
short_name: 'Aquarius',
theme_color: '#0ea5e9',
icons: [
{ src: 'icons/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: 'icons/icon-512.png', sizes: '512x512', type: 'image/png' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.aquarius\..*/i,
handler: 'NetworkFirst',
options: {
cacheName: 'api-cache',
expiration: { maxEntries: 50, maxAgeSeconds: 300 },
},
},
],
},
}),
],
});
Validierung
Success Criteria
- β Lighthouse PWA Score > 90
- β Installierbar auf iOS Safari und Chrome
- β Offline-FunktionalitΓ€t: Bewertung ohne Internet mΓΆglich
- β Service Worker: Registriert und aktiv
- β Manifest: Valide, alle erforderlichen Felder
- β HTTPS: Deployment auf HTTPS (erforderlich fΓΌr PWA)
Testing-Checkliste
# Lighthouse PWA Audit
lighthouse https://aquarius.app/execution --view
# Service Worker registriert?
# Chrome DevTools β Application β Service Workers
# Offline-Test
# Chrome DevTools β Network β Offline
# App sollte weiterhin funktionieren
# iOS Installation
# Safari β Share β Add to Home Screen
# Android Installation
# Chrome β Menu β Install App
Metriken
| Metrik | Zielwert | Aktuell |
|---|---|---|
| Lighthouse PWA Score | > 90 | TBD |
| Offline FunktionalitΓ€t | 100% kritische Features | TBD |
| Service Worker Cache Hit Rate | > 80% | TBD |
| Time to Interactive (3G) | < 5s | TBD |
Referenzen
Historie
| Datum | Γnderung | Autor |
|---|---|---|
| 2025-12-18 | Initiale Version | Team |