// Fejlesztési log service — fájlba írás terepi teszteléshez. // Az app külső tárhelyére ír, onnan könnyen elérhető. // // Használat: // AppLogger.i('startRecording', 'Track indítva: $name'); // AppLogger.w('_onPosition', 'Ugrás kiszűrve: ${dist}m'); // AppLogger.e('_onPosition', 'GPS hiba', error: e); // // Log fájl helye: /sdcard/Android/data/hu.app_dev.terepi_seged/files/logs/ // Elérhető: Android Studio Device Explorer, adb pull, vagy fájlkezelő app import 'dart:async'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:terepi_seged/services/device_identity_service.dart'; enum _Level { info, warning, error } class AppLogger extends GetxService { static AppLogger get to => Get.find(); // ── Konfiguráció ────────────────────────────────────────────────── /// Csak fejlesztési módban logol (release build-ben kikapcsol) static const bool _enableInRelease = true; /// Max log fájl méret MB-ban — felette rotál static const int _maxSizeMb = 5; /// Max megőrzött log fájlok száma static const int _maxFiles = 5; // ── Belső állapot ───────────────────────────────────────────────── File? _logFile; IOSink? _sink; String? _logDir; static final _timeFmt = DateFormat('HH:mm:ss.SSS'); static final _dateFmt = DateFormat('yyyy-MM-dd'); static final _fileFmt = DateFormat('yyyy-MM-dd_HH-mm-ss'); final isEnabled = false.obs; final currentLogPath = ''.obs; // ── Lifecycle ───────────────────────────────────────────────────── @override Future onReady() async { super.onReady(); if (!kDebugMode && !_enableInRelease) return; try { await _initLogFile(); isEnabled.value = true; i('AppLogger', 'Log indítva — ${currentLogPath.value}'); } catch (e) { debugPrint('AppLogger init hiba: $e'); } } @override Future onClose() async { await _sink?.flush(); await _sink?.close(); super.onClose(); } // ── Publikus API ────────────────────────────────────────────────── /// Info szintű log static void i(String tag, String message) { _write(_Level.info, tag, message); _remoteLog("INFO", tag, message, null, null); } /// Figyelmeztetés static void w(String tag, String message, {Object? error}) { _write(_Level.warning, tag, message, error: error); _remoteLog("WARN", tag, message, error, null); } /// Hiba static void e(String tag, String message, {Object? error, StackTrace? stack}) { _write(_Level.error, tag, message, error: error, stack: stack); _remoteLog("ERROR", tag, message, error, stack); } /// Szeparátor — jól látható elválasztó a logban static void separator(String label) { _write(_Level.info, '─────', '─── $label ───────────────────────'); } // ── Log fájl megosztása ─────────────────────────────────────────── Future shareLogs() async { await _sink?.flush(); final dir = _logDir; if (dir == null) return; final files = Directory(dir) .listSync() .whereType() .where((f) => f.path.endsWith('.log')) .toList() ..sort((a, b) => b.path.compareTo(a.path)); if (files.isEmpty) { Get.snackbar('Log', 'Nincs log fájl', snackPosition: SnackPosition.BOTTOM); return; } // Legutóbbi fájl megosztása await SharePlus.instance.share(ShareParams( files: [XFile(files.first.path, mimeType: 'text/plain')], subject: 'Terepi Segéd log — ${_dateFmt.format(DateTime.now())}', )); } /// Összes log fájl törlése Future clearLogs() async { await _sink?.flush(); await _sink?.close(); _sink = null; final dir = _logDir; if (dir == null) return; for (final f in Directory(dir).listSync().whereType()) { await f.delete().catchError((_) {}); } await _initLogFile(); i('AppLogger', 'Logok törölve, új fájl nyitva'); } // ── Belső ───────────────────────────────────────────────────────── static void _write( _Level level, String tag, String message, { Object? error, StackTrace? stack, }) { if (!kDebugMode && !_enableInRelease) return; // Konzolon is megjelenik final prefix = switch (level) { _Level.info => '📋', _Level.warning => '⚠️', _Level.error => '🔴', }; debugPrint('$prefix [$tag] $message' '${error != null ? ' | $error' : ''}'); // Fájlba írás try { AppLogger.to._writeLine(level, tag, message, error: error, stack: stack); } catch (_) {} } void _writeLine( _Level level, String tag, String message, { Object? error, StackTrace? stack, }) { final sink = _sink; if (sink == null) return; final time = _timeFmt.format(DateTime.now()); final lvl = switch (level) { _Level.info => 'I', _Level.warning => 'W', _Level.error => 'E', }; sink.writeln('$time $lvl [$tag] $message'); if (error != null) sink.writeln(' ↳ $error'); if (stack != null) { // Csak az első 5 sor a stack trace-ből final lines = stack.toString().split('\n').take(5); for (final l in lines) sink.writeln(' $l'); } // Méret ellenőrzés — ha szükséges, rotál _checkRotation(); } Future _initLogFile() async { final ext = await getExternalStorageDirectory(); final dir = Directory(p.join(ext!.path, 'logs')); await dir.create(recursive: true); _logDir = dir.path; final name = 'terepi_${_fileFmt.format(DateTime.now())}.log'; _logFile = File(p.join(dir.path, name)); _sink = _logFile!.openWrite(mode: FileMode.append); currentLogPath.value = _logFile!.path; // Fejléc _sink!.writeln('═' * 60); _sink!.writeln('Terepi Segéd — Log'); _sink!.writeln('Dátum: ${DateTime.now().toIso8601String()}'); _sink!.writeln('═' * 60); // Régi fájlok törlése await _pruneOldFiles(dir); } void _checkRotation() { final file = _logFile; if (file == null) return; if (!file.existsSync()) return; final sizeMb = file.lengthSync() / (1024 * 1024); if (sizeMb > _maxSizeMb) { _sink?.flush(); _sink?.close(); _initLogFile(); // új fájl } } Future _pruneOldFiles(Directory dir) async { final files = dir .listSync() .whereType() .where((f) => f.path.endsWith('.log')) .toList() ..sort((a, b) => a.path.compareTo(b.path)); while (files.length > _maxFiles) { await files.removeAt(0).delete(); } } // ── Távoli (Supabase) hiba-napló ────────────────────────────────── static final Map _remoteLogDedup = {}; static const _remoteLogDedupWindow = Duration(seconds: 60); static void _remoteLog( String type, String tag, String message, Object? error, StackTrace? stack, [String? info, Map? params]) { final key = '$tag|$message'; final last = _remoteLogDedup[key]; final now = DateTime.now(); if (last != null && now.difference(last) < _remoteLogDedupWindow) { return; // ugyanaz a hiba nemrég már felment — ne floodoljuk } _remoteLogDedup[key] = now; unawaited(() async { try { String? deviceId; String? appVersion; String? model; String? appInstanceId; String? platform; if (Get.isRegistered()) { final d = DeviceIdentityService.to; deviceId = d.isReady ? d.deviceId : null; appVersion = d.isReady ? d.appInfo : null; model = d.isReady ? d.model : null; appInstanceId = d.isReady ? d.appInstanceId : null; platform = d.isReady ? d.info.platform : null; } final userId = Supabase.instance.client.auth.currentUser?.id; await Supabase.instance.client.from('terepi_seged_logs').insert({ 'tag': tag, 'message': message, 'error_text': error?.toString(), 'stack_text': stack != null ? stack.toString().split('\n').take(8).join('\n') : null, 'app_version': appVersion, 'device_id': deviceId, 'user_id': userId, 'app_id': appInstanceId, 'model': model, 'platform': platform, 'info': info, 'params': params }).timeout(const Duration(seconds: 5)); } catch (_) { // Szándékosan néma — a távoli naplózás hibája NEM okozhat // újabb naplóbejegyzést (végtelen ciklus elkerülése). } }()); } // ── Egyszerű, "Firebase Analytics"-stílusú esemény-napló ────────── // NEM a Firebase-be megy — ugyanabba a Supabase-be, mint a hiba-napló, // hogy a hiba ÉS a használati esemény is egy helyen, azonnal // lekérdezhető legyen (a valódi Firebase Analytics konzolja akár // 24 órás késleltetéssel jelenít meg adatokat). static void event(String name, [String? info, Map? params]) { unawaited(() async { try { String? deviceId; String? appVersion; String? model; String? appInstanceId; String? platform; if (Get.isRegistered()) { final d = DeviceIdentityService.to; deviceId = d.isReady ? d.deviceId : null; appVersion = d.isReady ? d.appInfo : null; model = d.isReady ? d.model : null; appInstanceId = d.isReady ? d.appInstanceId : null; platform = d.isReady ? d.info.platform : null; } final userId = Supabase.instance.client.auth.currentUser?.id; await Supabase.instance.client.from('terepi_seged_events').insert({ 'event_name': name, 'params': params, 'app_version': appVersion, 'device_id': deviceId, 'user_id': userId, 'app_id': appInstanceId, 'model': model, 'platform': platform, 'info': info }).timeout(const Duration(seconds: 5)); } catch (_) { // Néma — az esemény-naplózás hibája nem árthat a funkciónak. } }()); } }