2026-06-23 15:55:33 +02:00
|
|
|
// 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
|
|
|
|
|
|
2026-07-11 23:32:20 +02:00
|
|
|
import 'dart:async';
|
2026-06-23 15:55:33 +02:00
|
|
|
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';
|
2026-07-11 23:32:20 +02:00
|
|
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
|
|
|
|
import 'package:terepi_seged/services/device_identity_service.dart';
|
2026-06-23 15:55:33 +02:00
|
|
|
|
|
|
|
|
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)
|
2026-07-02 15:49:23 +02:00
|
|
|
static const bool _enableInRelease = true;
|
2026-06-23 15:55:33 +02:00
|
|
|
|
|
|
|
|
/// 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<void> 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<void> onClose() async {
|
|
|
|
|
await _sink?.flush();
|
|
|
|
|
await _sink?.close();
|
|
|
|
|
super.onClose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Publikus API ──────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/// Info szintű log
|
2026-07-11 23:32:20 +02:00
|
|
|
static void i(String tag, String message) {
|
|
|
|
|
_write(_Level.info, tag, message);
|
|
|
|
|
_remoteLog("INFO", tag, message, null, null);
|
|
|
|
|
}
|
2026-06-23 15:55:33 +02:00
|
|
|
|
|
|
|
|
/// Figyelmeztetés
|
2026-07-11 23:32:20 +02:00
|
|
|
static void w(String tag, String message, {Object? error}) {
|
|
|
|
|
_write(_Level.warning, tag, message, error: error);
|
|
|
|
|
_remoteLog("WARN", tag, message, error, null);
|
|
|
|
|
}
|
2026-06-23 15:55:33 +02:00
|
|
|
|
|
|
|
|
/// Hiba
|
|
|
|
|
static void e(String tag, String message,
|
2026-07-11 23:32:20 +02:00
|
|
|
{Object? error, StackTrace? stack}) {
|
|
|
|
|
_write(_Level.error, tag, message, error: error, stack: stack);
|
|
|
|
|
_remoteLog("ERROR", tag, message, error, stack);
|
|
|
|
|
}
|
2026-06-23 15:55:33 +02:00
|
|
|
|
|
|
|
|
/// 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<void> shareLogs() async {
|
|
|
|
|
await _sink?.flush();
|
|
|
|
|
|
|
|
|
|
final dir = _logDir;
|
|
|
|
|
if (dir == null) return;
|
|
|
|
|
|
|
|
|
|
final files = Directory(dir)
|
|
|
|
|
.listSync()
|
|
|
|
|
.whereType<File>()
|
|
|
|
|
.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<void> 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<File>()) {
|
|
|
|
|
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<void> _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<void> _pruneOldFiles(Directory dir) async {
|
|
|
|
|
final files = dir
|
|
|
|
|
.listSync()
|
|
|
|
|
.whereType<File>()
|
|
|
|
|
.where((f) => f.path.endsWith('.log'))
|
|
|
|
|
.toList()
|
|
|
|
|
..sort((a, b) => a.path.compareTo(b.path));
|
|
|
|
|
|
|
|
|
|
while (files.length > _maxFiles) {
|
|
|
|
|
await files.removeAt(0).delete();
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-11 23:32:20 +02:00
|
|
|
|
|
|
|
|
// ── Távoli (Supabase) hiba-napló ──────────────────────────────────
|
|
|
|
|
|
|
|
|
|
static final Map<String, DateTime> _remoteLogDedup = {};
|
|
|
|
|
static const _remoteLogDedupWindow = Duration(seconds: 60);
|
|
|
|
|
|
|
|
|
|
static void _remoteLog(
|
|
|
|
|
String type, String tag, String message, Object? error, StackTrace? stack,
|
|
|
|
|
[String? info, Map<String, dynamic>? 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<DeviceIdentityService>()) {
|
|
|
|
|
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<String, dynamic>? params]) {
|
|
|
|
|
unawaited(() async {
|
|
|
|
|
try {
|
|
|
|
|
String? deviceId;
|
|
|
|
|
String? appVersion;
|
|
|
|
|
String? model;
|
|
|
|
|
String? appInstanceId;
|
|
|
|
|
String? platform;
|
|
|
|
|
if (Get.isRegistered<DeviceIdentityService>()) {
|
|
|
|
|
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.
|
|
|
|
|
}
|
|
|
|
|
}());
|
|
|
|
|
}
|
2026-06-23 15:55:33 +02:00
|
|
|
}
|