From b4d63509d186767314e7018f42a37f2949c108e0 Mon Sep 17 00:00:00 2001 From: Dustin Date: Thu, 6 Aug 2026 18:04:52 +0200 Subject: [PATCH] =?UTF-8?q?v2.55.1=20=E2=80=94=20Code-Review-Fixes:=20Sing?= =?UTF-8?q?leton,=20Race-Conditions,=20Fallback-Timer,=20Log-Security?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL (C1): RealtimeSyncService jetzt Singleton (Factory-Pattern) - Verhindert doppelte SSE-Verbindungen durch home_screen + settings - UI-Notifier (istVerbundenNotifier) app-weit konsistent HIGH (H1): _reconnecteOderFallback + starteWennAktiviert setzen cloud_interval vor starteAutoSyncTimer() — sonst bleibt Timer bei Fallback auf 0 (kein Sync) HIGH (H2): Race-Condition in _verbinde() nach await request.close() — _pausiert-Flag re-check; bei true → clean exit mit _laeuft=false HIGH (H3): fortsetzen() re-checkt _pausiert nach starteWennAktiviert() — verhindert Leak bei schnellem App-Umschalten MEDIUM (M1): _syncModusSetzen stoppt SSE bei Wechsel auf manual/interval (vorher lief die Verbindung weiter) MEDIUM (M2): Event-Logging loggt nur noch Event-Typ, nicht rohe Daten (Privacy: Song-IDs/Titel/Künstler nicht im Log) LOW (L1): _verbinde() setzt _laeuft=false bei Token-Fehler (kein Deadlock) --- lib/screens/settings_screen.dart | 8 ++++--- lib/services/realtime_sync_service.dart | 32 +++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart index 5eb7f1b..72e7392 100644 --- a/lib/screens/settings_screen.dart +++ b/lib/screens/settings_screen.dart @@ -109,19 +109,21 @@ class _SettingsScreenState extends State { await p.setString('sync_modus', modus); // Realtime-Service entsprechend starten/stoppen + final rs = RealtimeSyncService(); switch (modus) { case 'realtime': // SSE sofort starten - final rs = RealtimeSyncService(); await rs.starteWennAktiviert(); break; case 'manual': - // Kein Auto-Sync — Timer stoppen + // Kein Auto-Sync — SSE stoppen, Timer deaktivieren + rs.stoppe(); await p.setInt('cloud_interval', 0); // alten Timer deaktivieren await SyncService.starteAutoSyncTimer(); break; case 'interval': - // Intervall-Timer mit cloud_interval_stunden starten + // Intervall-Timer — SSE stoppen, Timer mit cloud_interval_stunden starten + rs.stoppe(); await p.setInt('cloud_interval', _cloudIntervallStunden); await SyncService.starteAutoSyncTimer(); break; diff --git a/lib/services/realtime_sync_service.dart b/lib/services/realtime_sync_service.dart index 98f68bf..97b2639 100644 --- a/lib/services/realtime_sync_service.dart +++ b/lib/services/realtime_sync_service.dart @@ -33,8 +33,13 @@ enum SyncModus { /// **Lifecycle** (via WidgetsBindingObserver in main.dart): /// - resumed → fortsetzen() (SSE neu verbinden) /// - paused/hidden → pausiere() (Batterie sparen) +/// +/// **Singleton**: Nur EINE Instanz pro App-Lebenszyklus, damit +/// home_screen.dart und settings_screen.dart dieselbe Verbindung teilen. class RealtimeSyncService { - RealtimeSyncService() + static final RealtimeSyncService _instanz = RealtimeSyncService._(); + factory RealtimeSyncService() => _instanz; + RealtimeSyncService._() : _db = DbHelper(), _auth = AuthService(), _favoriten = FavoritenService(); @@ -123,6 +128,9 @@ class RealtimeSyncService { // Fallback: sync_modus auf interval setzen + Auto-Sync-Timer starten final p = await SharedPreferences.getInstance(); await p.setString('sync_modus', 'interval'); + // cloud_interval setzen, damit starteAutoSyncTimer() den Timer startet + final intervallStunden = await cloudIntervallStunden(); + await p.setInt('cloud_interval', intervallStunden); await SyncService.starteAutoSyncTimer(); stoppe(); return; @@ -164,6 +172,7 @@ class RealtimeSyncService { final token = _auth.token; if (token == null || token.isEmpty) { _fehlerBericht('Kein Auth-Token'); + _laeuft = false; return; } @@ -179,6 +188,14 @@ class RealtimeSyncService { _sseResponse = await request.close(); + // Nach await prüfen: pausiere() könnte während des Connects + // aufgerufen worden sein (WidgetsBindingObserver-Race). + if (_pausiert) { + _sseResponse = null; + _laeuft = false; + return; + } + if (_sseResponse!.statusCode != 200) { _fehlerBericht('HTTP ${_sseResponse!.statusCode}'); await _reconnecteOderFallback(); @@ -249,7 +266,8 @@ class RealtimeSyncService { MeloLogger().aktion('realtime_event', { 'type': eventTyp, - 'data': daten, + // Daten nicht loggen — könnten sensitive User-Informationen enthalten + // (Song-IDs, Titel, Künstler). Nur Event-Typ wird geloggt. }); switch (eventTyp) { @@ -391,6 +409,10 @@ class RealtimeSyncService { MeloLogger().aktion('realtime_fallback_to_interval', {}); final p = await SharedPreferences.getInstance(); await p.setString('sync_modus', 'interval'); + // cloud_interval (alter Key) auf Default 48h setzen, damit + // SyncService.starteAutoSyncTimer() den Timer auch wirklich startet. + final intervallStunden = await cloudIntervallStunden(); + await p.setInt('cloud_interval', intervallStunden); await SyncService.starteAutoSyncTimer(); } } @@ -435,6 +457,12 @@ class RealtimeSyncService { if (!_pausiert) return; _pausiert = false; await starteWennAktiviert(); + // Nach await prüfen: pausiere() könnte während starteWennAktiviert() + // aufgerufen worden sein (z.B. schnelles App-Umschalten). + // Falls ja, Verbindung sofort wieder trennen. + if (_pausiert) { + stoppe(); + } } /// Stoppt die SSE-Verbindung endgültig (z. B. Logout, Modus-Wechsel).