v2.55.1 — Code-Review-Fixes: Singleton, Race-Conditions, Fallback-Timer, Log-Security

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)
This commit is contained in:
Dustin
2026-08-06 18:04:52 +02:00
parent 3f169fa1d9
commit b4d63509d1
2 changed files with 35 additions and 5 deletions
+5 -3
View File
@@ -109,19 +109,21 @@ class _SettingsScreenState extends State<SettingsScreen> {
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;
+30 -2
View File
@@ -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).