This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Dustin f886b0a42e v2.51.1 — Technik ausgelagert: Erweitert-Sektion (Logs, Diagnose, Entwickler, Scanner)
## Erweitert
- Neuer ErweitertScreen (lib/screens/erweitert_screen.dart): Logs, Diagnosedaten-Toggle, Entwickler, Musik scannen
- Neuer LogViewerScreen: In-Memory-Logbuch mit Kategorie-Filter (MeloLogger bekommt Anzeige-Puffer, der beim Senden nicht geleert wird)
- Neuer EntwicklerScreen: Version, Session-ID, DB-Songzahl, Server-URLs, Test-Log senden
- Einstellungen-Screen: 'Erweitert'-Sektion eingebaut, Diagnose-Toggle dorthin verschoben, Version auf 2.51

## Einstellungen
- 'Server verbinden' → 'Musikserver' umbenannt
- Abmelden/Info/Cloud Sync unverändert
2026-08-05 07:54:22 +02:00

145 lines
4.2 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
import '../config/app_config.dart';
/// Umfassendes Log-System für die Melo App.
/// Zeichnet ALLES auf: Aktionen, Fehler, Netzwerk, Performance.
class MeloLogger {
static final MeloLogger _instanz = MeloLogger._();
factory MeloLogger() => _instanz;
MeloLogger._();
static const _maxEintraege = 500;
bool _initialisiert = false;
String _sessionId = '';
String _version = '2.7';
final List<Map<String, dynamic>> _eintraege = [];
// Anzeige-Puffer: bleibt erhalten, auch wenn Einträge gesendet wurden
// (für den Log-Viewer unter „Mehr → Erweitert → Logs“).
final List<Map<String, dynamic>> _anzeigeEintraege = [];
int _logId = 0;
Timer? _flushTimer;
String get _serverUrl => '${AppConfig.logUrl}/api/log';
/// Alle bisher aufgezeichneten Log-Einträge (nur Anzeige, nie geleert).
List<Map<String, dynamic>> get anzeigeEintraege =>
List.unmodifiable(_anzeigeEintraege);
/// Aktuelle Session-ID (für den Entwickler-Screen).
String get sessionId => _sessionId;
void init(String version) {
if (_initialisiert) return;
_initialisiert = true;
_version = version;
_sessionId = DateTime.now().millisecondsSinceEpoch.toString();
_flushTimer = Timer.periodic(const Duration(seconds: 30), (_) => _senden());
_addEintrag('app', 'start', {'version': version, 'session': _sessionId});
FlutterError.onError = (details) {
FlutterError.presentError(details);
_addEintrag('crash', 'flutter_error', {
'fehler': details.exceptionAsString(),
'stack': details.stack.toString(),
});
};
PlatformDispatcher.instance.onError = (error, stack) {
_addEintrag('crash', 'dart_error', {
'fehler': error.toString(),
'stack': stack.toString(),
});
return true;
};
debugPrint('📋 MeloLogger ready (Session: $_sessionId)');
}
void _addEintrag(String kategorie, String aktion, [Map<String, dynamic>? details]) {
// Cap 500: älteste Einträge verwerfen
if (_eintraege.length >= _maxEintraege) {
_eintraege.removeAt(0);
}
_eintraege.add({
'id': _logId++,
'zeit': DateTime.now().toIso8601String(),
'kategorie': kategorie,
'aktion': aktion,
'details': details ?? {},
});
// Anzeige-Puffer (nie leeren — nur älteste verwerfen)
if (_anzeigeEintraege.length >= _maxEintraege) {
_anzeigeEintraege.removeAt(0);
}
_anzeigeEintraege.add(_eintraege.last);
}
void aktion(String name, [Map<String, dynamic>? details]) {
_addEintrag('aktion', name, details);
}
void netzwerk(String methode, String url, {int? statusCode, String? fehler, int? dauerMs}) {
_addEintrag('netzwerk', '$methode $url', {
if (statusCode != null) 'status': statusCode,
if (fehler != null) 'fehler': fehler,
if (dauerMs != null) 'dauer_ms': dauerMs,
});
}
void fehler(String kontext, Object? error, [StackTrace? stack]) {
_addEintrag('fehler', kontext, {
'error': error.toString(),
'stack': stack?.toString() ?? '',
});
_senden();
}
void performance(String name, int dauerMs) {
_addEintrag('performance', name, {'dauer_ms': dauerMs});
}
void zustand(String name, Map<String, dynamic> daten) {
_addEintrag('zustand', name, daten);
}
void manuell(String nachricht) {
_addEintrag('user', nachricht);
_senden();
}
Future<void> _senden() async {
if (_eintraege.isEmpty) return;
if (!AppConfig.sendeDiagnosedaten) return;
final batch = _eintraege.toList();
_eintraege.clear();
try {
await http.post(
Uri.parse(_serverUrl),
headers: {'Content-Type': 'application/json'},
body: jsonEncode({
'typ': 'log_batch',
'session': _sessionId,
'version': _version,
'device': '${Platform.operatingSystem} ${Platform.operatingSystemVersion}',
'eintraege': batch,
}),
).timeout(const Duration(seconds: 5));
} catch (_) {
_eintraege.insertAll(0, batch);
}
}
void dispose() {
_flushTimer?.cancel();
_senden();
}
}