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

  1. Schnelle Entwicklung: Eine Codebasis fΓΌr alle Plattformen
  2. Offline-First: Bewertung funktioniert ohne Internet
  3. Einfache Distribution: URL teilen statt App Store
  4. Automatische Updates: Neue Features sofort verfΓΌgbar
  5. Niedrige Kosten: Kein App Store, keine Device-Testing-Farm

Negativ

  1. iOS-Limitierungen: Safari hat eingeschrΓ€nkte PWA-Features
  2. Kein App-Store-Listing: Discoverability schlechter (aber irrelevant fΓΌr geschlossene Liga)
  3. Browser-AbhΓ€ngigkeit: Safari/Chrome Updates kΓΆnnen App brechen
  4. 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