From 3e5fc217b87e7830a0b44d0b5d353c5af5b29fd7 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Wed, 7 Jan 2026 12:58:29 -0600 Subject: [PATCH 01/25] feat: add logger service with memory buffer and Riverpod provider --- lib/features/logs/logs_provider.dart | 79 +++++++ lib/services/logger_service.dart | 311 +++++++++++++++++++++++++++ 2 files changed, 390 insertions(+) create mode 100644 lib/features/logs/logs_provider.dart create mode 100644 lib/services/logger_service.dart diff --git a/lib/features/logs/logs_provider.dart b/lib/features/logs/logs_provider.dart new file mode 100644 index 000000000..9aeb0e8ea --- /dev/null +++ b/lib/features/logs/logs_provider.dart @@ -0,0 +1,79 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; + +final logsProvider = StateNotifierProvider>((ref) { + return LogsNotifier(); +}); + +class LogsNotifier extends StateNotifier> { + LogsNotifier() : super([]) { + MemoryLogOutput.instance.addListener(_onLogsChanged); + _onLogsChanged(); + } + + void _onLogsChanged() { + state = MemoryLogOutput.instance.getAllLogs(); + } + + void clearLogs() { + MemoryLogOutput.instance.clear(); + } + + @override + void dispose() { + MemoryLogOutput.instance.removeListener(_onLogsChanged); + super.dispose(); + } +} + +final filteredLogsProvider = Provider.family, LogsFilter>((ref, filter) { + final logs = ref.watch(logsProvider); + + var filteredLogs = logs; + + if (filter.levelFilter != null && filter.levelFilter != 'all') { + final level = _stringToLevel(filter.levelFilter!); + filteredLogs = filteredLogs.where((log) => log.level == level).toList(); + } + + if (filter.searchQuery.isNotEmpty) { + final query = filter.searchQuery.toLowerCase(); + filteredLogs = filteredLogs.where((log) { + return log.message.toLowerCase().contains(query) || + log.service.toLowerCase().contains(query); + }).toList(); + } + + return filteredLogs; +}); + +Level _stringToLevel(String levelStr) { + switch (levelStr) { + case 'error': return Level.error; + case 'warning': return Level.warning; + case 'info': return Level.info; + case 'debug': return Level.debug; + default: return Level.debug; + } +} + +class LogsFilter { + final String? levelFilter; + final String searchQuery; + + const LogsFilter({ + this.levelFilter, + this.searchQuery = '', + }); + + LogsFilter copyWith({ + String? levelFilter, + String? searchQuery, + }) { + return LogsFilter( + levelFilter: levelFilter ?? this.levelFilter, + searchQuery: searchQuery ?? this.searchQuery, + ); + } +} diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart new file mode 100644 index 000000000..6ac821032 --- /dev/null +++ b/lib/services/logger_service.dart @@ -0,0 +1,311 @@ +import 'dart:isolate'; +import 'package:flutter/foundation.dart'; +import 'package:logger/logger.dart'; +import 'package:mostro_mobile/core/config.dart'; + +// Isolate log communication +ReceivePort? _isolateLogReceiver; +SendPort? _isolateLogSender; + +/// Initialize receiver to collect logs from background isolates +void initIsolateLogReceiver() { + if (_isolateLogReceiver != null) return; + + _isolateLogReceiver = ReceivePort(); + _isolateLogSender = _isolateLogReceiver!.sendPort; + + _isolateLogReceiver!.listen((message) { + if (message is Map) { + try { + addLogFromIsolate(message); + } catch (e, stack) { + // ignore: avoid_print + print('ERROR: Failed to process isolate log message: $e'); + // ignore: avoid_print + print('Stack trace: $stack'); + } + } + }); +} + +SendPort? get isolateLogSenderPort => _isolateLogSender; + +String cleanMessage(String message) { + var cleaned = message; + cleaned = cleaned + .replaceAll(RegExp(r'\x1B\[[0-9;]*[a-zA-Z]'), '') + .replaceAll(RegExp(r'\[\d+m'), '') + .replaceAll(RegExp(r'\[38;5;\d+m'), '') + .replaceAll(RegExp(r'\[39m'), '') + .replaceAll(RegExp(r'\[2m'), '') + .replaceAll(RegExp(r'\[22m'), '') + .replaceAll(RegExp(r'[┌┐└┘├┤─│┬┴┼╭╮╰╯╔╗╚╝╠╣═║╦╩╬━┃┄├]'), '') + .replaceAll(RegExp(r'[\u{1F300}-\u{1F9FF}]', unicode: true), '') + .replaceAll(RegExp(r'nsec[0-9a-z]+'), '[PRIVATE_KEY]') + .replaceAll(RegExp(r'"privateKey"\s*:\s*"[^"]*"'), '"privateKey":"[REDACTED]"') + .replaceAll(RegExp(r'"mnemonic"\s*:\s*"[^"]*"'), '"mnemonic":"[REDACTED]"') + .replaceAll(RegExp(r'[^A-Za-z0-9\s.:,!?\-_/\[\]]'), ' ') + .replaceAll(RegExp(r'\s+'), ' '); + return cleaned.trim(); +} + +void addLogFromIsolate(Map logData) { + if (!MemoryLogOutput.isLoggingEnabled) return; + + DateTime timestamp; + try { + final timestampStr = logData['timestamp']; + if (timestampStr == null) { + timestamp = DateTime.now(); + } else { + timestamp = DateTime.parse(timestampStr.toString()); + } + } catch (e) { + timestamp = DateTime.now(); + } + + final levelStr = logData['level']?.toString() ?? 'debug'; + final level = _levelFromString(levelStr); + final rawMessage = logData['message']?.toString() ?? ''; + final message = cleanMessage(rawMessage); + final service = logData['service']?.toString() ?? 'Background'; + final line = logData['line']?.toString() ?? '0'; + + MemoryLogOutput.instance.addEntry(LogEntry( + timestamp: timestamp, + level: level, + message: message, + service: service, + line: line, + )); +} + +Level _levelFromString(String level) { + switch (level) { + case 'error': return Level.error; + case 'warning': return Level.warning; + case 'info': return Level.info; + case 'debug': return Level.debug; + case 'trace': return Level.trace; + default: return Level.debug; + } +} + +class LogEntry { + final DateTime timestamp; + final Level level; + final String message; + final String service; + final String line; + + LogEntry({ + required this.timestamp, + required this.level, + required this.message, + required this.service, + required this.line, + }); + + String format() { + final time = timestamp.toString().substring(0, 19); + final levelStr = level.toString().split('.').last.toUpperCase(); + return '[$levelStr]($service:$line) $time - $message'; + } +} + +class MemoryLogOutput extends LogOutput with ChangeNotifier { + static final MemoryLogOutput instance = MemoryLogOutput._(); + + MemoryLogOutput._(); + + final List _buffer = []; + final SimplePrinter _printer = SimplePrinter(); + + static bool isLoggingEnabled = false; + + @override + void output(OutputEvent event) { + if (!isLoggingEnabled) return; + + final stackTrace = event.origin.stackTrace ?? StackTrace.current; + final serviceAndLine = _printer.extractFromStackTrace(stackTrace); + + addEntry(LogEntry( + timestamp: event.origin.time, + level: event.level, + message: cleanMessage(event.origin.message.toString()), + service: serviceAndLine['service'] ?? 'Unknown', + line: serviceAndLine['line'] ?? '0', + )); + } + + List getAllLogs() => List.unmodifiable(_buffer); + + void clear() { + _buffer.clear(); + notifyListeners(); + } + + void addEntry(LogEntry entry) { + _buffer.add(entry); + _maintainBufferSize(); + notifyListeners(); + } + + void _maintainBufferSize() { + if (_buffer.length > Config.logMaxEntries) { + final deleteCount = _buffer.length < Config.logBatchDeleteSize + ? _buffer.length - Config.logMaxEntries + : Config.logBatchDeleteSize; + if (deleteCount > 0) { + _buffer.removeRange(0, deleteCount); + } + } + } + + int get logCount => _buffer.length; +} + +class _MultiOutput extends LogOutput { + final MemoryLogOutput memoryOutput; + final LogOutput? consoleOutput; + + _MultiOutput(this.memoryOutput, this.consoleOutput); + + @override + void output(OutputEvent event) { + memoryOutput.output(event); + consoleOutput?.output(event); + } +} + +class SimplePrinter extends LogPrinter { + @override + List log(LogEvent event) { + final level = _formatLevel(event.level); + final message = event.message.toString(); + final timestamp = event.time.toString().substring(0, 19); + final stackTrace = event.stackTrace ?? StackTrace.current; + final serviceAndLine = extractFromStackTrace(stackTrace); + final service = serviceAndLine['service'] ?? 'Unknown'; + final line = serviceAndLine['line'] ?? '0'; + + return [ + '[$level]($service:$line) $timestamp - $message', + ]; + } + + String _formatLevel(Level level) { + switch (level) { + case Level.error: + return 'ERROR'; + case Level.warning: + return 'WARN'; + case Level.info: + return 'INFO'; + case Level.debug: + return 'DEBUG'; + case Level.trace: + return 'TRACE'; + default: + return 'LOG'; + } + } + + Map extractFromStackTrace(StackTrace? stackTrace) { + if (stackTrace == null) return {'service': 'Unknown', 'line': '0'}; + + final lines = stackTrace.toString().split('\n'); + + for (final line in lines) { + if (line.contains('logger_service.dart') || + line.contains('logger.dart') || + line.contains(' (dart:') || + line.contains('') || + line.trim().isEmpty) { + continue; + } + + var match = RegExp(r'#\d+\s+\S+\s+\((?:package:[\w_]+/)?(?:.*/)(\w+)\.dart:(\d+)').firstMatch(line); + if (match != null) { + return { + 'service': match.group(1) ?? 'Unknown', + 'line': match.group(2) ?? '0' + }; + } + + match = RegExp(r'package:[\w_]+/(?:.*/)(\w+)\.dart:(\d+)').firstMatch(line); + if (match != null) { + return { + 'service': match.group(1) ?? 'Unknown', + 'line': match.group(2) ?? '0' + }; + } + } + + return {'service': 'Unknown', 'line': '0'}; + } +} + +class _ProductionOptimizedFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) { + if (Config.isDebug) { + return true; + } + + return MemoryLogOutput.isLoggingEnabled; + } +} + +Logger? _cachedLogger; + +Logger get logger { + _cachedLogger ??= Logger( + printer: PrettyPrinter( + methodCount: 2, + errorMethodCount: 8, + lineLength: 120, + colors: true, + printEmojis: true, + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + ), + output: _MultiOutput( + MemoryLogOutput.instance, + Config.isDebug ? ConsoleOutput() : null, + ), + level: Config.isDebug ? Level.debug : Level.warning, + filter: _ProductionOptimizedFilter(), + ); + return _cachedLogger!; +} + +class IsolateLogOutput extends LogOutput { + final SendPort? sendPort; + + IsolateLogOutput(this.sendPort); + + @override + void output(OutputEvent event) { + for (final line in event.lines) { + // ignore: avoid_print + print(line); + } + + if (sendPort != null) { + final printer = SimplePrinter(); + final serviceAndLine = printer.extractFromStackTrace(event.origin.stackTrace); + + final rawMessage = event.origin.message.toString(); + final sanitizedMessage = cleanMessage(rawMessage); + + sendPort!.send({ + 'timestamp': event.origin.time.toIso8601String(), + 'level': event.level.name, + 'message': sanitizedMessage, + 'service': serviceAndLine['service'] ?? 'Background', + 'line': serviceAndLine['line'] ?? '0', + }); + } + } +} From 96bf7b1611916de248d0d8c8b00bdc3e5087502b Mon Sep 17 00:00:00 2001 From: bracr10 Date: Wed, 7 Jan 2026 12:58:46 -0600 Subject: [PATCH 02/25] fix : update config with logger constants --- lib/core/config.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/core/config.dart b/lib/core/config.dart index 1e6a7b19e..62c68def9 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -46,5 +46,4 @@ class Config { // Logger configuration static const int logMaxEntries = 1000; static const int logBatchDeleteSize = 100; - static bool fullLogsInfo = true; // false = simple logs, true = PrettyPrinter } From f31cdb6cb3f4a22d8854aa23120c73f9eb2f1962 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Wed, 7 Jan 2026 12:58:59 -0600 Subject: [PATCH 03/25] feat : connect logs screen to logger service with real-time display --- lib/features/logs/screens/logs_screen.dart | 191 +++++++++++++++++++-- 1 file changed, 173 insertions(+), 18 deletions(-) diff --git a/lib/features/logs/screens/logs_screen.dart b/lib/features/logs/screens/logs_screen.dart index f9cabbf76..cfbf48516 100644 --- a/lib/features/logs/screens/logs_screen.dart +++ b/lib/features/logs/screens/logs_screen.dart @@ -1,11 +1,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/core/config.dart'; +import 'package:mostro_mobile/features/logs/logs_provider.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/generated/l10n.dart'; - -enum LogLevel { error, warning, info, debug } +import 'package:mostro_mobile/services/logger_service.dart'; class LogsScreen extends ConsumerStatefulWidget { const LogsScreen({super.key}); @@ -15,7 +16,7 @@ class LogsScreen extends ConsumerStatefulWidget { } class _LogsScreenState extends ConsumerState { - LogLevel? _selectedLevel; + String? _selectedLevel; String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); final ScrollController _scrollController = ScrollController(); @@ -30,7 +31,7 @@ class _LogsScreenState extends ConsumerState { void _onScroll() { if (_scrollController.hasClients) { final maxScroll = _scrollController.position.maxScrollExtent; - final showButton = _scrollController.offset > 200 && maxScroll > 0; + final showButton = _scrollController.offset > 200 && maxScroll > 200; if (showButton != _showScrollToTop) { setState(() => _showScrollToTop = showButton); } @@ -60,6 +61,16 @@ class _LogsScreenState extends ConsumerState { } } + void _enableLogging() { + MemoryLogOutput.isLoggingEnabled = true; + ref.read(settingsProvider.notifier).updateLoggingEnabled(true); + } + + void _disableLogging() { + MemoryLogOutput.isLoggingEnabled = false; + ref.read(settingsProvider.notifier).updateLoggingEnabled(false); + } + Future _showPerformanceWarning() async { final confirmed = await showDialog( context: context, @@ -87,13 +98,13 @@ class _LogsScreenState extends ConsumerState { ); if (confirmed == true && mounted) { - await ref.read(settingsProvider.notifier).updateLoggingEnabled(true); + _enableLogging(); } } Future _disableLoggingAndSave() async { if (mounted) { - await ref.read(settingsProvider.notifier).updateLoggingEnabled(false); + _disableLogging(); } } @@ -127,6 +138,7 @@ class _LogsScreenState extends ConsumerState { ); if (confirmed == true && mounted) { + ref.read(logsProvider.notifier).clearLogs(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -141,7 +153,12 @@ class _LogsScreenState extends ConsumerState { Widget build(BuildContext context) { final settings = ref.watch(settingsProvider); final isLoggingEnabled = settings.isLoggingEnabled; - final logs = []; + final filter = LogsFilter( + levelFilter: _selectedLevel, + searchQuery: _searchQuery, + ); + final allLogs = ref.watch(logsProvider); + final logs = ref.watch(filteredLogsProvider(filter)); return Stack( children: [ @@ -169,11 +186,13 @@ class _LogsScreenState extends ConsumerState { ), body: Column( children: [ - _buildStatsHeader(logs.length, logs.length, isLoggingEnabled), + _buildStatsHeader(allLogs.length, logs.length, isLoggingEnabled), _buildSearchBar(), _buildFilterChips(), Expanded( - child: _buildEmptyState(), + child: logs.isEmpty + ? _buildEmptyState() + : _buildLogsList(logs), ), ], ), @@ -293,7 +312,13 @@ class _LogsScreenState extends ConsumerState { icon: const Icon(Icons.clear, color: AppTheme.textSecondary), onPressed: () { _searchController.clear(); - setState(() => _searchQuery = ''); + setState(() { + _searchQuery = ''; + _showScrollToTop = false; + if (_scrollController.hasClients) { + _scrollController.jumpTo(0); + } + }); }, ) : null, @@ -306,7 +331,13 @@ class _LogsScreenState extends ConsumerState { contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), ), onChanged: (value) { - setState(() => _searchQuery = value); + setState(() { + _searchQuery = value; + _showScrollToTop = false; + if (_scrollController.hasClients) { + _scrollController.jumpTo(0); + } + }); }, ), ); @@ -322,26 +353,32 @@ class _LogsScreenState extends ConsumerState { children: [ _buildFilterChip(S.of(context)!.allLevels, null), const SizedBox(width: 8), - _buildFilterChip(S.of(context)!.errors, LogLevel.error), + _buildFilterChip(S.of(context)!.errors, 'error'), const SizedBox(width: 8), - _buildFilterChip(S.of(context)!.warnings, LogLevel.warning), + _buildFilterChip(S.of(context)!.warnings, 'warning'), const SizedBox(width: 8), - _buildFilterChip(S.of(context)!.info, LogLevel.info), + _buildFilterChip(S.of(context)!.info, 'info'), const SizedBox(width: 8), - _buildFilterChip(S.of(context)!.debug, LogLevel.debug), + _buildFilterChip(S.of(context)!.debug, 'debug'), ], ), ), ); } - Widget _buildFilterChip(String label, LogLevel? level) { - final isSelected = _selectedLevel == level; + Widget _buildFilterChip(String label, String? levelFilter) { + final isSelected = _selectedLevel == levelFilter; return FilterChip( label: Text(label), selected: isSelected, onSelected: (selected) { - setState(() => _selectedLevel = selected ? level : null); + setState(() { + _selectedLevel = selected ? levelFilter : null; + _showScrollToTop = false; + if (_scrollController.hasClients) { + _scrollController.jumpTo(0); + } + }); }, backgroundColor: AppTheme.backgroundInput, selectedColor: AppTheme.statusInfo.withValues(alpha: 0.2), @@ -390,4 +427,122 @@ class _LogsScreenState extends ConsumerState { ), ); } + + Widget _buildLogsList(List logs) { + return ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.all(8), + itemCount: logs.length, + itemBuilder: (context, index) { + final log = logs[logs.length - 1 - index]; + return _buildLogItem(log); + }, + ); + } + + Widget _buildLogItem(LogEntry log) { + final color = _getLogLevelColor(log.level); + final icon = _getLogLevelIcon(log.level); + + return Container( + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppTheme.backgroundCard, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: color.withValues(alpha: 0.3), + width: 1, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(icon, size: 16, color: color), + const SizedBox(width: 8), + Text( + log.level.toString().split('.').last.toUpperCase(), + style: TextStyle( + color: color, + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(width: 8), + Text( + '${log.service}:${log.line}', + style: const TextStyle( + color: AppTheme.textSecondary, + fontSize: 12, + ), + ), + const Spacer(), + Text( + _formatTime(log.timestamp), + style: const TextStyle( + color: AppTheme.textInactive, + fontSize: 11, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + log.message, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 13, + ), + ), + ], + ), + ); + } + + Color _getLogLevelColor(Level level) { + switch (level) { + case Level.error: + return AppTheme.statusError; + case Level.warning: + return AppTheme.statusWarning; + case Level.info: + return AppTheme.statusInfo; + case Level.debug: + return AppTheme.textSecondary; + default: + return AppTheme.textSecondary; + } + } + + IconData _getLogLevelIcon(Level level) { + switch (level) { + case Level.error: + return Icons.error; + case Level.warning: + return Icons.warning; + case Level.info: + return Icons.info; + case Level.debug: + return Icons.bug_report; + default: + return Icons.circle; + } + } + + String _formatTime(DateTime timestamp) { + final now = DateTime.now(); + final difference = now.difference(timestamp); + + if (difference.inSeconds < 60) { + return '${difference.inSeconds}s ago'; + } else if (difference.inMinutes < 60) { + return '${difference.inMinutes}m ago'; + } else if (difference.inHours < 24) { + return '${difference.inHours}h ago'; + } else { + return '${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}'; + } + } } From 6b59aee6fea63f3818b81efaddaeedbf5652e7a9 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Wed, 7 Jan 2026 12:59:08 -0600 Subject: [PATCH 04/25] feat: sync logging toggle with MemoryLogOutput --- lib/features/settings/settings_notifier.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/features/settings/settings_notifier.dart b/lib/features/settings/settings_notifier.dart index 3ebfb478b..3e1a0de75 100644 --- a/lib/features/settings/settings_notifier.dart +++ b/lib/features/settings/settings_notifier.dart @@ -4,6 +4,7 @@ import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/enums/storage_keys.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; class SettingsNotifier extends StateNotifier { @@ -151,6 +152,7 @@ class SettingsNotifier extends StateNotifier { Future updateLoggingEnabled(bool newValue) async { state = state.copyWith(isLoggingEnabled: newValue); + MemoryLogOutput.isLoggingEnabled = newValue; } Settings get settings => state.copyWith(); From 062eef6a1e2f6bfe0e8bb982a31cbe6e54cb6c70 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Wed, 7 Jan 2026 12:59:17 -0600 Subject: [PATCH 05/25] refactor: migrate files to use logger singleton --- lib/features/relays/relays_notifier.dart | 95 +++++++++---------- .../subscriptions/subscription_manager.dart | 32 +++---- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/lib/features/relays/relays_notifier.dart b/lib/features/relays/relays_notifier.dart index 6a52e16bb..6d08c3b40 100644 --- a/lib/features/relays/relays_notifier.dart +++ b/lib/features/relays/relays_notifier.dart @@ -3,10 +3,10 @@ import 'dart:io'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:dart_nostr/nostr/model/ease.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/models/relay_list_event.dart'; import 'package:mostro_mobile/features/settings/settings_notifier.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'relay.dart'; @@ -27,11 +27,10 @@ class RelayValidationResult { class RelaysNotifier extends StateNotifier> { final SettingsNotifier settings; final Ref ref; - final _logger = Logger(); SubscriptionManager? _subscriptionManager; StreamSubscription? _relayListSubscription; Timer? _settingsWatchTimer; - Timer? _retryTimer; // Store retry timer to prevent leaks + Timer? _retryTimer; // Hash-based deduplication to prevent processing identical relay lists String? _lastRelayListHash; @@ -50,9 +49,9 @@ class RelaysNotifier extends StateNotifier> { void _loadRelays() { final saved = settings.state; - - _logger.i('Loading relays from settings: ${saved.relays}'); - _logger.i('Loading user relays from settings: ${saved.userRelays}'); + + logger.i('Loading relays from settings: ${saved.relays}'); + logger.i('Loading user relays from settings: ${saved.userRelays}'); final loadedRelays = []; @@ -75,7 +74,7 @@ class RelaysNotifier extends StateNotifier> { loadedRelays.addAll(userRelaysFromSettings); state = loadedRelays; - _logger.i('Loaded ${state.length} relays: ${state.map((r) => '${r.url} (${r.source})').toList()}'); + logger.i('Loaded ${state.length} relays: ${state.map((r) => '${r.url} (${r.source})').toList()}'); } Future _saveRelays() async { @@ -91,7 +90,7 @@ class RelaysNotifier extends StateNotifier> { // Separate user relays for metadata preservation final userRelays = state.where((r) => r.source == RelaySource.user).toList(); - _logger.i('Saving ${allActiveRelayUrls.length} active relays (excluding ${blacklistedUrls.length} blacklisted) and ${userRelays.length} user relays metadata'); + logger.i('Saving ${allActiveRelayUrls.length} active relays (excluding ${blacklistedUrls.length} blacklisted) and ${userRelays.length} user relays metadata'); // Save ALL active relays to settings.relays (NostrService will use these) await settings.updateRelays(allActiveRelayUrls); @@ -100,7 +99,7 @@ class RelaysNotifier extends StateNotifier> { final userRelaysJson = userRelays.map((r) => r.toJson()).toList(); await settings.updateUserRelays(userRelaysJson); - _logger.i('Relays saved successfully'); + logger.i('Relays saved successfully'); } Future addRelay(Relay relay) async { @@ -380,7 +379,7 @@ class RelaysNotifier extends StateNotifier> { // Step 5: Remove from blacklist if present (user wants to manually add it) if (settings.state.blacklistedRelays.contains(normalizedUrl)) { await settings.removeFromBlacklist(normalizedUrl); - _logger.i('Removed $normalizedUrl from blacklist - user manually added it'); + logger.i('Removed $normalizedUrl from blacklist - user manually added it'); } // Step 6: Add relay as user relay @@ -424,15 +423,15 @@ class RelaysNotifier extends StateNotifier> { _handleMostroRelayListUpdate(relayListEvent); }, onError: (error, stackTrace) { - _logger.e('Error handling relay list event', + logger.e('Error handling relay list event', error: error, stackTrace: stackTrace); }, ); // Don't call syncWithMostroInstance() here - it's handled by Future.microtask() in constructor - _logger.i('Mostro relay sync initialized - sync will start after provider initialization'); + logger.i('Mostro relay sync initialized - sync will start after provider initialization'); } catch (e, stackTrace) { - _logger.e('Failed to initialize Mostro relay sync', + logger.e('Failed to initialize Mostro relay sync', error: e, stackTrace: stackTrace); } } @@ -442,11 +441,11 @@ class RelaysNotifier extends StateNotifier> { try { final mostroPubkey = settings.state.mostroPublicKey; if (mostroPubkey.isEmpty) { - _logger.w('No Mostro pubkey configured, skipping relay sync'); + logger.w('No Mostro pubkey configured, skipping relay sync'); return; } - _logger.i('Syncing relays with Mostro instance: $mostroPubkey'); + logger.i('Syncing relays with Mostro instance: $mostroPubkey'); // Cancel any existing relay list subscription before creating new one _subscriptionManager?.unsubscribeFromMostroRelayList(); @@ -460,18 +459,18 @@ class RelaysNotifier extends StateNotifier> { // Subscribe to the new Mostro instance _subscriptionManager?.subscribeToMostroRelayList(mostroPubkey); - _logger.i('Successfully subscribed to relay list events for Mostro: $mostroPubkey'); + logger.i('Successfully subscribed to relay list events for Mostro: $mostroPubkey'); // Schedule a retry in case the subscription doesn't work immediately _scheduleRetrySync(mostroPubkey); } catch (e) { - _logger.w('Failed to subscribe immediately, will retry later: $e'); + logger.w('Failed to subscribe immediately, will retry later: $e'); // Schedule a retry even if initial subscription fails _scheduleRetrySync(mostroPubkey); } } catch (e, stackTrace) { - _logger.e('Failed to sync with Mostro instance', + logger.e('Failed to sync with Mostro instance', error: e, stackTrace: stackTrace); } } @@ -484,11 +483,11 @@ class RelaysNotifier extends StateNotifier> { _retryTimer = Timer(const Duration(seconds: 10), () async { try { if (settings.state.mostroPublicKey == mostroPubkey) { - _logger.i('Retrying relay sync for Mostro: $mostroPubkey'); + logger.i('Retrying relay sync for Mostro: $mostroPubkey'); _subscriptionManager?.subscribeToMostroRelayList(mostroPubkey); } } catch (e) { - _logger.w('Retry sync failed: $e'); + logger.w('Retry sync failed: $e'); } finally { _retryTimer = null; // Clear reference after execution } @@ -505,11 +504,11 @@ class RelaysNotifier extends StateNotifier> { final nostrService = ref.read(nostrServiceProvider); // Check if NostrService is actually initialized if (nostrService.isInitialized) { - _logger.i('NostrService is ready for relay subscriptions'); + logger.i('NostrService is ready for relay subscriptions'); return; } } catch (e) { - _logger.w('NostrService not accessible yet, attempt ${attempt + 1}/$maxAttempts: $e'); + logger.w('NostrService not accessible yet, attempt ${attempt + 1}/$maxAttempts: $e'); } if (attempt < maxAttempts - 1) { @@ -517,7 +516,7 @@ class RelaysNotifier extends StateNotifier> { } } - _logger.e('NostrService failed to initialize after $maxAttempts attempts'); + logger.e('NostrService failed to initialize after $maxAttempts attempts'); throw Exception('NostrService not available for relay synchronization'); } @@ -528,7 +527,7 @@ class RelaysNotifier extends StateNotifier> { // Validate that this event is from the currently configured Mostro instance if (event.authorPubkey != currentMostroPubkey) { - _logger.w('Ignoring relay list event from wrong Mostro instance. ' + logger.w('Ignoring relay list event from wrong Mostro instance. ' 'Expected: $currentMostroPubkey, Got: ${event.authorPubkey}'); return; } @@ -536,7 +535,7 @@ class RelaysNotifier extends StateNotifier> { // Timestamp validation: ignore events older than the last processed event if (_lastProcessedEventTime != null && event.publishedAt.isBefore(_lastProcessedEventTime!)) { - _logger.i('Ignoring older relay list event from ${event.publishedAt} ' + logger.i('Ignoring older relay list event from ${event.publishedAt} ' '(last processed: $_lastProcessedEventTime)'); return; } @@ -544,11 +543,11 @@ class RelaysNotifier extends StateNotifier> { // Hash-based deduplication: ignore identical relay lists final relayListHash = event.validRelays.join(','); if (_lastRelayListHash == relayListHash) { - _logger.i('Relay list unchanged (hash match), skipping update'); + logger.i('Relay list unchanged (hash match), skipping update'); return; } - _logger.i('Received relay list from Mostro ${event.authorPubkey}: ${event.relays}'); + logger.i('Received relay list from Mostro ${event.authorPubkey}: ${event.relays}'); // Normalize relay URLs to prevent duplicates final normalizedRelays = event.validRelays @@ -571,13 +570,13 @@ class RelaysNotifier extends StateNotifier> { .where((relay) => relay.source == RelaySource.defaultConfig && !blacklistedUrls.contains(_normalizeRelayUrl(relay.url))) .toList(); - _logger.i('Kept ${updatedRelays.length} default relays and ${userRelays.length} user relays'); + logger.i('Kept ${updatedRelays.length} default relays and ${userRelays.length} user relays'); // Process Mostro relays from 10002 event for (final relayUrl in normalizedRelays) { // Skip if blacklisted by user if (blacklistedUrls.contains(relayUrl)) { - _logger.i('Skipping blacklisted Mostro relay: $relayUrl'); + logger.i('Skipping blacklisted Mostro relay: $relayUrl'); continue; } @@ -592,27 +591,27 @@ class RelaysNotifier extends StateNotifier> { userRelays.removeWhere((r) => _normalizeRelayUrl(r.url) == relayUrl); final promotedRelay = Relay.fromMostro(relayUrl); updatedRelays.insert(0, promotedRelay); // Insert at beginning - _logger.i('Promoted user relay to Mostro relay: $relayUrl'); + logger.i('Promoted user relay to Mostro relay: $relayUrl'); continue; } // Skip if already in updatedRelays (avoid duplicates with default relays) if (updatedRelays.any((r) => _normalizeRelayUrl(r.url) == relayUrl)) { - _logger.i('Skipping duplicate relay: $relayUrl'); + logger.i('Skipping duplicate relay: $relayUrl'); continue; } // Add new Mostro relay final mostroRelay = Relay.fromMostro(relayUrl); updatedRelays.add(mostroRelay); - _logger.i('Added Mostro relay: $relayUrl'); + logger.i('Added Mostro relay: $relayUrl'); } // Remove Mostro relays that are no longer in the 10002 event (ELIMINATION case) final currentMostroRelays = state.where((relay) => relay.source == RelaySource.mostro).toList(); for (final mostroRelay in currentMostroRelays) { if (!normalizedRelays.contains(_normalizeRelayUrl(mostroRelay.url))) { - _logger.i('Removing Mostro relay no longer in 10002: ${mostroRelay.url}'); + logger.i('Removing Mostro relay no longer in 10002: ${mostroRelay.url}'); // Relay is eliminated completely - no reverting to user relay } } @@ -625,14 +624,14 @@ class RelaysNotifier extends StateNotifier> { !finalRelays.every((relay) => state.contains(relay))) { state = finalRelays; await _saveRelays(); - _logger.i('Updated relay list with ${finalRelays.length} relays (${blacklistedUrls.length} blacklisted)'); + logger.i('Updated relay list with ${finalRelays.length} relays (${blacklistedUrls.length} blacklisted)'); } // Update tracking variables after successful processing _lastProcessedEventTime = event.publishedAt; _lastRelayListHash = relayListHash; } catch (e, stackTrace) { - _logger.e('Error handling Mostro relay list update', + logger.e('Error handling Mostro relay list update', error: e, stackTrace: stackTrace); } } @@ -644,17 +643,17 @@ class RelaysNotifier extends StateNotifier> { final relay = state.firstWhere((r) => r.url == url, orElse: () => Relay(url: '')); if (relay.url.isEmpty) { - _logger.w('Attempted to remove non-existent relay: $url'); + logger.w('Attempted to remove non-existent relay: $url'); return; } // Blacklist all relays to prevent re-addition during sync await settings.addToBlacklist(url); - _logger.i('Blacklisted ${relay.source} relay: $url'); + logger.i('Blacklisted ${relay.source} relay: $url'); // Remove relay from current state await removeRelay(url); - _logger.i('Removed relay: $url (source: ${relay.source})'); + logger.i('Removed relay: $url (source: ${relay.source})'); } // Removed removeRelayWithSource - no longer needed since all relays are managed via blacklist @@ -674,14 +673,14 @@ class RelaysNotifier extends StateNotifier> { currentPubkey != null && newPubkey.isNotEmpty && currentPubkey!.isNotEmpty) { - _logger.i('Detected REAL Mostro pubkey change: $currentPubkey -> $newPubkey'); + logger.i('Detected REAL Mostro pubkey change: $currentPubkey -> $newPubkey'); currentPubkey = newPubkey; // 🔥 RESET COMPLETO: Limpiar todos los relays y hacer sync fresco _cleanAllRelaysAndResync(); } else if (newPubkey != currentPubkey) { // Just update the tracking variable without reset (initial load) - _logger.i('Initial Mostro pubkey load: $newPubkey'); + logger.i('Initial Mostro pubkey load: $newPubkey'); currentPubkey = newPubkey; syncWithMostroInstance(); } @@ -691,14 +690,14 @@ class RelaysNotifier extends StateNotifier> { /// Clean all relays (except default) and perform fresh sync with new Mostro Future _cleanAllRelaysAndResync() async { try { - _logger.i('Cleaning all relays and performing fresh sync...'); + logger.i('Cleaning all relays and performing fresh sync...'); // CLEAR ALL relays (only keep default) final defaultRelay = Relay.fromDefault('wss://relay.mostro.network'); state = [defaultRelay]; await _saveRelays(); - _logger.i('Reset to default relay only, starting fresh sync'); + logger.i('Reset to default relay only, starting fresh sync'); // Reset hash and timestamp for completely fresh sync with new Mostro _lastRelayListHash = null; @@ -708,7 +707,7 @@ class RelaysNotifier extends StateNotifier> { await syncWithMostroInstance(); } catch (e, stackTrace) { - _logger.e('Error during relay cleanup and resync', + logger.e('Error during relay cleanup and resync', error: e, stackTrace: stackTrace); } } @@ -784,7 +783,7 @@ class RelaysNotifier extends StateNotifier> { final wouldBeBlacklisted = [...currentBlacklist, urlToBlacklist]; final wouldRemainActive = currentActiveRelays.where((url) => !wouldBeBlacklisted.contains(url)).toList(); - _logger.d('Current active: ${currentActiveRelays.length}, Would remain: ${wouldRemainActive.length}'); + logger.d('Current active: ${currentActiveRelays.length}, Would remain: ${wouldRemainActive.length}'); return wouldRemainActive.isEmpty; } @@ -797,7 +796,7 @@ class RelaysNotifier extends StateNotifier> { if (isCurrentlyBlacklisted) { // Remove from blacklist and trigger sync to add back await settings.removeFromBlacklist(url); - _logger.i('Removed $url from blacklist, triggering re-sync'); + logger.i('Removed $url from blacklist, triggering re-sync'); // Reset hash to allow re-processing of the same relay list with updated blacklist context _lastRelayListHash = null; @@ -807,14 +806,14 @@ class RelaysNotifier extends StateNotifier> { // Add to blacklist and remove from current state await settings.addToBlacklist(url); await removeRelay(url); - _logger.i('Blacklisted and removed Mostro relay: $url'); + logger.i('Blacklisted and removed Mostro relay: $url'); } } /// Clear all blacklisted relays and trigger re-sync Future clearBlacklistAndResync() async { await settings.clearBlacklist(); - _logger.i('Cleared blacklist, triggering relay re-sync'); + logger.i('Cleared blacklist, triggering relay re-sync'); // Reset hash to allow re-processing of relay lists with cleared blacklist _lastRelayListHash = null; @@ -839,7 +838,7 @@ class RelaysNotifier extends StateNotifier> { final removedCount = state.length - cleanedRelays.length; state = cleanedRelays; await _saveRelays(); - _logger.i('Cleaned $removedCount Mostro relays from state'); + logger.i('Cleaned $removedCount Mostro relays from state'); } } diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index 7909db272..37a9bb452 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/core/models/relay_list_event.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/subscriptions/subscription.dart'; @@ -18,7 +18,7 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class SubscriptionManager { final Ref ref; final Map _subscriptions = {}; - final _logger = Logger(); + late final ProviderSubscription _sessionListener; final _ordersController = StreamController.broadcast(); @@ -44,7 +44,7 @@ class SubscriptionManager { }, fireImmediately: false, onError: (error, stackTrace) { - _logger.e('Error in session listener', + logger.e('Error in session listener', error: error, stackTrace: stackTrace); }, ); @@ -60,20 +60,20 @@ class SubscriptionManager { try { final existingSessions = ref.read(sessionNotifierProvider); if (existingSessions.isNotEmpty) { - _logger.i('Initializing subscriptions for ${existingSessions.length} existing sessions'); + logger.i('Initializing subscriptions for ${existingSessions.length} existing sessions'); _updateAllSubscriptions(existingSessions); } else { - _logger.i('No existing sessions found during SubscriptionManager initialization'); + logger.i('No existing sessions found during SubscriptionManager initialization'); } } catch (e, stackTrace) { - _logger.e('Error initializing existing sessions', + logger.e('Error initializing existing sessions', error: e, stackTrace: stackTrace); } } void _updateAllSubscriptions(List sessions) { if (sessions.isEmpty) { - _logger.i('No sessions available, clearing all subscriptions'); + logger.i('No sessions available, clearing all subscriptions'); _clearAllSubscriptions(); return; } @@ -91,7 +91,7 @@ class SubscriptionManager { void _updateSubscription(SubscriptionType type, List sessions) { if (sessions.isEmpty) { - _logger.i('No sessions for $type subscription'); + logger.i('No sessions for $type subscription'); unsubscribeByType(type); return; } @@ -108,10 +108,10 @@ class SubscriptionManager { filter: filter, ); - _logger + logger .i('Subscription created for $type with ${sessions.length} sessions'); } catch (e, stackTrace) { - _logger.e('Failed to create $type subscription', + logger.e('Failed to create $type subscription', error: e, stackTrace: stackTrace); } } @@ -164,7 +164,7 @@ class SubscriptionManager { break; } } catch (e, stackTrace) { - _logger.e('Error handling $type event', error: e, stackTrace: stackTrace); + logger.e('Error handling $type event', error: e, stackTrace: stackTrace); } } @@ -182,7 +182,7 @@ class SubscriptionManager { final streamSubscription = stream.listen( (event) => _handleEvent(type, event), onError: (error, stackTrace) { - _logger.e('Error in $type subscription', + logger.e('Error in $type subscription', error: error, stackTrace: stackTrace); }, cancelOnError: false, @@ -270,9 +270,9 @@ class SubscriptionManager { _subscribeToRelayList(filter); - _logger.i('Subscribed to relay list for Mostro: $mostroPubkey'); + logger.i('Subscribed to relay list for Mostro: $mostroPubkey'); } catch (e, stackTrace) { - _logger.e('Failed to subscribe to Mostro relay list', + logger.e('Failed to subscribe to Mostro relay list', error: e, stackTrace: stackTrace); } } @@ -295,7 +295,7 @@ class SubscriptionManager { } }, onError: (error, stackTrace) { - _logger.e('Error in relay list subscription', + logger.e('Error in relay list subscription', error: error, stackTrace: stackTrace); }, cancelOnError: false, @@ -320,7 +320,7 @@ class SubscriptionManager { /// Unsubscribes from Mostro relay list events void unsubscribeFromMostroRelayList() { unsubscribeByType(SubscriptionType.relayList); - _logger.i('Unsubscribed from Mostro relay list'); + logger.i('Unsubscribed from Mostro relay list'); } void dispose() { From f5ed78b1543293ae52bf969b88adf86b324e4c61 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Wed, 7 Jan 2026 13:04:07 -0600 Subject: [PATCH 06/25] docs: updating docs --- docs/LOGGING_IMPLEMENTATION.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/LOGGING_IMPLEMENTATION.md b/docs/LOGGING_IMPLEMENTATION.md index c8cb700a5..a8f46a1f8 100644 --- a/docs/LOGGING_IMPLEMENTATION.md +++ b/docs/LOGGING_IMPLEMENTATION.md @@ -15,14 +15,14 @@ Implementation of a comprehensive logging system for MostroP2P mobile app with i - Toggle resets to OFF on every app restart (not persisted) - Route configuration -### Phase 2: Logger Service & Integration (Current) +### Phase 2: Logger Service & Integration (Completed) - Logger service with memory buffer and sanitization - Riverpod providers - Initialize in main.dart - Connect UI to actual logger - Test with 2 files: RelaysNotifier, SubscriptionManager -### Phase 3: Core Services Migration +### Phase 3: Core Services Migration (Current) - NostrService - MostroService - DeepLinkService @@ -204,6 +204,6 @@ void backgroundMain(SendPort sendPort) async { --- -**Version**: 2 +**Version**: 3 **Status**: Phase 2 - Ready **Last Updated**: 2026-01-06 From d040ffe5ba203ef902636dff00744b9e499d76fe Mon Sep 17 00:00:00 2001 From: bracr10 Date: Mon, 12 Jan 2026 16:29:30 -0600 Subject: [PATCH 07/25] Migrate NostrService to use logger singleton --- .../notfiers/abstract_mostro_notifier.dart | 2 +- lib/services/deep_link_service.dart | 30 +++---- lib/services/mostro_service.dart | 24 +++--- lib/services/nostr_service.dart | 84 +++++++++---------- 4 files changed, 70 insertions(+), 70 deletions(-) diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index f0b8429b6..db0afe62b 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -11,7 +11,7 @@ import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/notifications/providers/notifications_provider.dart'; import 'package:mostro_mobile/features/notifications/utils/notification_data_extractor.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; class AbstractMostroNotifier extends StateNotifier { final String orderId; diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart index cd0436ee6..5d309503b 100644 --- a/lib/services/deep_link_service.dart +++ b/lib/services/deep_link_service.dart @@ -4,7 +4,7 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; @@ -22,7 +22,7 @@ class OrderInfo { } class DeepLinkService { - final Logger _logger = Logger(); + final AppLinks _appLinks = AppLinks(); // Stream controller for deep link events @@ -41,11 +41,11 @@ class DeepLinkService { // Listen for incoming deep links when app is already running _appLinks.uriLinkStream.listen( (Uri uri) { - _logger.i('Deep link received while app running: $uri'); + logger.i('Deep link received while app running: $uri'); _handleDeepLink(uri); }, onError: (Object err) { - _logger.e('Deep link stream error: $err'); + logger.e('Deep link stream error: $err'); }, ); @@ -53,9 +53,9 @@ class DeepLinkService { // The initial link will be handled by the app initialization in app.dart _isInitialized = true; - _logger.i('DeepLinkService initialized successfully'); + logger.i('DeepLinkService initialized successfully'); } catch (e) { - _logger.e('Failed to initialize DeepLinkService: $e'); + logger.e('Failed to initialize DeepLinkService: $e'); rethrow; } } @@ -72,7 +72,7 @@ class DeepLinkService { BuildContext context, ) async { try { - _logger.i('Processing mostro link: $url'); + logger.i('Processing mostro link: $url'); // Validate URL format if (!NostrUtils.isValidMostroUrl(url)) { @@ -98,7 +98,7 @@ class DeepLinkService { return DeepLinkResult.error(S.of(context)!.deepLinkNoRelays); } - _logger.i('Parsed order ID: $orderId, relays: $relays'); + logger.i('Parsed order ID: $orderId, relays: $relays'); // Fetch the order info directly using the order ID final fetchedOrderInfo = await _fetchOrderInfoById( @@ -117,7 +117,7 @@ class DeepLinkService { return DeepLinkResult.success(fetchedOrderInfo); } catch (e) { - _logger.e('Error processing mostro link: $e'); + logger.e('Error processing mostro link: $e'); return DeepLinkResult.error('Failed to process deep link: $e'); } } @@ -148,7 +148,7 @@ class DeepLinkService { // If no events found and we have default relays, try those if (events.isEmpty) { - _logger.i('Order not found in specified relays, trying default relays'); + logger.i('Order not found in specified relays, trying default relays'); final defaultEvents = await nostrService.fetchEvents(filter); events.addAll(defaultEvents); } @@ -177,7 +177,7 @@ class DeepLinkService { return null; } catch (e) { - _logger.e('Error fetching order info by ID: $e'); + logger.e('Error fetching order info by ID: $e'); return null; } } @@ -196,7 +196,7 @@ class DeepLinkService { /// Navigates to the appropriate screen for the given order void navigateToOrder(GoRouter router, OrderInfo orderInfo) { final route = getNavigationRoute(orderInfo); - _logger.i( + logger.i( 'Navigating to: $route (Order: ${orderInfo.orderId}, Type: ${orderInfo.orderType.value})'); // Use post-frame callback to ensure navigation happens after the current frame @@ -208,15 +208,15 @@ class DeepLinkService { if (context != null && context.mounted) { router.push(route); } else { - _logger.w('Router context is not available for navigation to: $route'); + logger.w('Router context is not available for navigation to: $route'); } } catch (e) { - _logger.e('Error navigating to order: $e'); + logger.e('Error navigating to order: $e'); // Fallback: try using go instead of push if push fails try { router.go(route); } catch (fallbackError) { - _logger.e('Fallback navigation also failed: $fallbackError'); + logger.e('Fallback navigation also failed: $fallbackError'); } } }); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index adf531867..239ef816a 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -4,7 +4,7 @@ import 'package:collection/collection.dart'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; @@ -29,7 +29,7 @@ class MostroService { _ordersSubscription = ref.read(subscriptionManagerProvider).orders.listen( _onData, onError: (error, stackTrace) { - _logger.e('Error in orders subscription', + logger.e('Error in orders subscription', error: error, stackTrace: stackTrace); }, cancelOnError: false, @@ -38,7 +38,7 @@ class MostroService { void dispose() { _ordersSubscription?.cancel(); - _logger.i('MostroService disposed'); + logger.i('MostroService disposed'); } //IMPORTANT : The app always use trade index 1 for restore-related messages @@ -112,7 +112,7 @@ class MostroService { (s) => s.tradeKey.public == event.recipient, ); if (matchingSession == null) { - _logger.w('No matching session found for recipient: ${event.recipient}'); + logger.w('No matching session found for recipient: ${event.recipient}'); return; } final privateKey = matchingSession.tradeKey.private; @@ -126,13 +126,13 @@ class MostroService { // Ensure result is a non-empty List before accessing elements if (result is! List || result.isEmpty) { - _logger.w('Received empty or invalid payload, skipping'); + logger.w('Received empty or invalid payload, skipping'); return; } // Skip dispute chat messages (they have "dm" key and are handled by DisputeChatNotifier) if (result[0] is Map && (result[0] as Map).containsKey('dm')) { - _logger.i('Skipping dispute chat message (handled by DisputeChatNotifier)'); + logger.i('Skipping dispute chat message (handled by DisputeChatNotifier)'); return; } @@ -149,13 +149,13 @@ class MostroService { // This handles cases where admin messages might not have an id in the decrypted event final messageKey = decryptedEvent.id ?? event.id ?? 'msg_${DateTime.now().millisecondsSinceEpoch}'; await messageStorage.addMessage(messageKey, msg); - _logger.i( + logger.i( 'Received DM, Event ID: ${decryptedEvent.id ?? event.id} with payload: ${decryptedEvent.content}', ); await _maybeLinkChildOrder(msg, matchingSession); } catch (e) { - _logger.e('Error processing event', error: e); + logger.e('Error processing event', error: e); } } @@ -179,7 +179,7 @@ class MostroService { ref.read(orderNotifierProvider(message.id!).notifier).subscribe(); - _logger.i( + logger.i( 'Linked child order ${message.id} to parent ${session.parentOrderId}', ); } @@ -291,7 +291,7 @@ class MostroService { final remaining = maxAmount - selectedAmount; if (remaining < minAmount) { - _logger.i( + logger.i( '[$callerLabel] Range order $orderId exhausted (remaining $remaining < min $minAmount); skipping child preparation.', ); return null; @@ -310,11 +310,11 @@ class MostroService { parentOrderId: orderId, role: currentSession.role!, ); - _logger.i( + logger.i( '[$callerLabel] Prepared child session for $orderId using key index $nextKeyIndex', ); } else { - _logger.w( + logger.w( '[$callerLabel] Unable to prepare child session for $orderId; session or role missing.', ); } diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index d13f7058d..b648b308d 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -3,7 +3,7 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:dart_nostr/nostr/model/ease.dart'; import 'package:dart_nostr/nostr/model/ok.dart'; import 'package:dart_nostr/nostr/model/relay_informations.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; @@ -14,7 +14,7 @@ import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; class NostrService { Settings? _settings; final Nostr _nostr = Nostr.instance; - final Logger _logger = Logger(); + bool _isInitialized = false; NostrService(); @@ -32,7 +32,7 @@ class NostrService { throw Exception('Cannot initialize NostrService: No relays provided'); } - _logger.i('Initializing NostrService with relays: ${settings.relays}'); + logger.i('Initializing NostrService with relays: ${settings.relays}'); _settings = settings; try { @@ -44,30 +44,30 @@ class NostrService { retryOnError: true, onRelayListening: (relayUrl, receivedData, channel) { if (receivedData is NostrEvent) { - _logger.d('Event from $relayUrl: ${receivedData.id}'); + logger.d('Event from $relayUrl: ${receivedData.id}'); } else if (receivedData is NostrNotice) { - _logger.i('Notice from $relayUrl: ${receivedData.message}'); + logger.i('Notice from $relayUrl: ${receivedData.message}'); } else if (receivedData is NostrEventOkCommand) { - _logger.d('OK from $relayUrl: ${receivedData.eventId} (accepted: ${receivedData.isEventAccepted})'); + logger.d('OK from $relayUrl: ${receivedData.eventId} (accepted: ${receivedData.isEventAccepted})'); } else if (receivedData is NostrRequestEoseCommand) { - _logger.d('EOSE from $relayUrl for subscription: ${receivedData.subscriptionId}'); + logger.d('EOSE from $relayUrl for subscription: ${receivedData.subscriptionId}'); } else if (receivedData is NostrCountResponse) { - _logger.d('Count from $relayUrl: ${receivedData.count}'); + logger.d('Count from $relayUrl: ${receivedData.count}'); } }, onRelayConnectionError: (relay, error, channel) { - _logger.w('Failed to connect to relay $relay: $error'); + logger.w('Failed to connect to relay $relay: $error'); }, onRelayConnectionDone: (relay, socket) { - _logger.i('Successfully connected to relay: $relay'); + logger.i('Successfully connected to relay: $relay'); }, ); _isInitialized = true; - _logger.i('NostrService initialized successfully with ${settings.relays.length} relays'); + logger.i('NostrService initialized successfully with ${settings.relays.length} relays'); } catch (e) { _isInitialized = false; - _logger.e('Failed to initialize NostrService: $e'); + logger.e('Failed to initialize NostrService: $e'); rethrow; } } @@ -75,11 +75,11 @@ class NostrService { Future updateSettings(Settings newSettings) async { // Compare with current settings instead of relying on dart_nostr internal state if (!ListEquality().equals(settings.relays, newSettings.relays)) { - _logger.i('Updating relays from ${settings.relays} to ${newSettings.relays}'); + logger.i('Updating relays from ${settings.relays} to ${newSettings.relays}'); // Validate that new relay list is not empty if (newSettings.relays.isEmpty) { - _logger.w('Warning: Attempting to update with empty relay list'); + logger.w('Warning: Attempting to update with empty relay list'); return; } @@ -89,24 +89,24 @@ class NostrService { // Disconnect from current relays first await _nostr.services.relays.disconnectFromRelays(); - _logger.i('Disconnected from previous relays'); + logger.i('Disconnected from previous relays'); // Initialize with new relay list await init(newSettings); - _logger.i('Successfully updated to new relays: ${newSettings.relays}'); + logger.i('Successfully updated to new relays: ${newSettings.relays}'); } catch (e) { - _logger.e('Failed to update relays: $e'); + logger.e('Failed to update relays: $e'); // Try to restore previous state if update fails try { await init(settings); - _logger.i('Restored previous relay configuration'); + logger.i('Restored previous relay configuration'); } catch (restoreError) { - _logger.e('Failed to restore previous relay configuration: $restoreError'); + logger.e('Failed to restore previous relay configuration: $restoreError'); rethrow; } } } else { - _logger.d('Relay list unchanged, skipping update'); + logger.d('Relay list unchanged, skipping update'); } } @@ -127,16 +127,16 @@ class NostrService { } try { - _logger.i('Publishing event ${event.id} to relays: ${settings.relays}'); + logger.i('Publishing event ${event.id} to relays: ${settings.relays}'); await _nostr.services.relays.sendEventToRelaysAsync( event, timeout: Config.nostrConnectionTimeout, ); - _logger.i('Successfully published event ${event.id}'); + logger.i('Successfully published event ${event.id}'); } catch (e) { - _logger.w('Failed to publish event ${event.id}: $e'); + logger.w('Failed to publish event ${event.id}: $e'); // If it's the empty relay list assertion error, provide better context if (e.toString().contains('relaysUrl.isNotEmpty')) { @@ -181,7 +181,7 @@ class NostrService { await _nostr.services.relays.disconnectFromRelays(); _isInitialized = false; - _logger.i('Disconnected from all relays'); + logger.i('Disconnected from all relays'); } bool get isInitialized => _isInitialized; @@ -270,7 +270,7 @@ class NostrService { } try { - _logger.i('Fetching event with ID: $eventId'); + logger.i('Fetching event with ID: $eventId'); // Create filter to fetch the specific event final filter = NostrFilter( @@ -289,7 +289,7 @@ class NostrService { } if (events.isEmpty) { - _logger.w('No event found with ID: $eventId'); + logger.w('No event found with ID: $eventId'); return null; } @@ -301,20 +301,20 @@ class NostrService { // Validate it's a proper order event if (event.kind != 38383) { - _logger.w('Event $eventId is not an order event (kind: ${event.kind})'); + logger.w('Event $eventId is not an order event (kind: ${event.kind})'); return null; } // Check if it's from a valid Mostro instance if (event.pubkey != settings.mostroPublicKey) { - _logger.w('Event $eventId is not from the configured Mostro instance'); + logger.w('Event $eventId is not from the configured Mostro instance'); return null; } - _logger.i('Successfully found order event: ${event.id}'); + logger.i('Successfully found order event: ${event.id}'); return Order.fromEvent(event); } catch (e) { - _logger.e('Error fetching event by ID: $e'); + logger.e('Error fetching event by ID: $e'); return null; } } @@ -324,7 +324,7 @@ class NostrService { Future fetchOrderInfoByEventId(String eventId, [List? specificRelays]) async { try { - _logger.i('Fetching order ID from event: $eventId'); + logger.i('Fetching order ID from event: $eventId'); final filter = NostrFilter( ids: [eventId], @@ -342,7 +342,7 @@ class NostrService { } if (events.isEmpty) { - _logger.w('No event found with ID: $eventId'); + logger.w('No event found with ID: $eventId'); return null; } @@ -354,13 +354,13 @@ class NostrService { // Validate it's a proper order event if (event.kind != 38383) { - _logger.w('Event $eventId is not an order event (kind: ${event.kind})'); + logger.w('Event $eventId is not an order event (kind: ${event.kind})'); return null; } // Check if it's from a valid Mostro instance if (event.pubkey != settings.mostroPublicKey) { - _logger.w('Event $eventId is not from the configured Mostro instance'); + logger.w('Event $eventId is not from the configured Mostro instance'); return null; } @@ -373,7 +373,7 @@ class NostrService { .firstOrNull; if (dTag == null || dTag.isEmpty) { - _logger.w('Event $eventId does not contain a valid d tag'); + logger.w('Event $eventId does not contain a valid d tag'); return null; } @@ -386,7 +386,7 @@ class NostrService { .firstOrNull; if (kTag == null || kTag.isEmpty) { - _logger.w('Event $eventId does not contain a valid k tag (order type)'); + logger.w('Event $eventId does not contain a valid k tag (order type)'); return null; } @@ -394,15 +394,15 @@ class NostrService { try { orderType = OrderType.fromString(kTag); } catch (e) { - _logger.w('Event $eventId contains invalid order type: $kTag'); + logger.w('Event $eventId contains invalid order type: $kTag'); return null; } - _logger.i( + logger.i( 'Successfully extracted order info - ID: $dTag, Type: ${orderType.value} from event: $eventId'); return OrderInfo(orderId: dTag, orderType: orderType); } catch (e) { - _logger.e('Error fetching order ID from event: $e'); + logger.e('Error fetching order ID from event: $e'); return null; } } @@ -420,7 +420,7 @@ class NostrService { final allRelays = {...originalRelays, ...relays}.toList(); if (!ListEquality().equals(originalRelays, allRelays)) { - _logger.i('Temporarily connecting to additional relays: $relays'); + logger.i('Temporarily connecting to additional relays: $relays'); // Update settings with additional relays final tempSettings = Settings( @@ -445,12 +445,12 @@ class NostrService { return await fetchEvents(filter); } } catch (e) { - _logger.e('Error fetching from specific relays: $e'); + logger.e('Error fetching from specific relays: $e'); // Ensure we restore original settings even on error try { await updateSettings(settings); } catch (restoreError) { - _logger.e('Failed to restore original relay settings: $restoreError'); + logger.e('Failed to restore original relay settings: $restoreError'); } rethrow; } From 64fc2405e631b19c769c9ca324829437a62c9d84 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Mon, 12 Jan 2026 16:29:53 -0600 Subject: [PATCH 08/25] Migrate mostro_storage to use logger singleton --- lib/data/repositories/mostro_storage.dart | 14 +++++++------- .../notfiers/abstract_mostro_notifier.dart | 19 +++++++++---------- .../order/notfiers/add_order_notifier.dart | 1 + .../order/notfiers/order_notifier.dart | 1 + lib/services/mostro_service.dart | 3 +-- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/data/repositories/mostro_storage.dart b/lib/data/repositories/mostro_storage.dart index 12b2b46a6..659180dad 100644 --- a/lib/data/repositories/mostro_storage.dart +++ b/lib/data/repositories/mostro_storage.dart @@ -1,11 +1,11 @@ -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/data/models/payload.dart'; import 'package:sembast/sembast.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/repositories/base_storage.dart'; class MostroStorage extends BaseStorage { - final Logger _logger = Logger(); + MostroStorage({required Database db}) : super(db, stringMapStoreFactory.store('orders')); @@ -21,11 +21,11 @@ class MostroStorage extends BaseStorage { dbMap['timestamp'] = message.timestamp; await store.record(id).put(db, dbMap); - _logger.i( + logger.i( 'Saved message of type ${message.action} with order id ${message.id}', ); } catch (e, stack) { - _logger.e( + logger.e( 'addMessage failed for $id', error: e, stackTrace: stack, @@ -39,7 +39,7 @@ class MostroStorage extends BaseStorage { try { return await getAll(); } catch (e, stack) { - _logger.e('getAllMessages failed', error: e, stackTrace: stack); + logger.e('getAllMessages failed', error: e, stackTrace: stack); return []; } } @@ -48,9 +48,9 @@ class MostroStorage extends BaseStorage { Future deleteAllMessages() async { try { await deleteAll(); - _logger.i('All messages deleted'); + logger.i('All messages deleted'); } catch (e, stack) { - _logger.e('deleteAllMessages failed', error: e, stackTrace: stack); + logger.e('deleteAllMessages failed', error: e, stackTrace: stack); rethrow; } } diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index db0afe62b..eeb7abbd7 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -16,7 +16,6 @@ import 'package:mostro_mobile/services/logger_service.dart'; class AbstractMostroNotifier extends StateNotifier { final String orderId; final Ref ref; - final logger = Logger(); late Session session; @@ -504,17 +503,17 @@ class AbstractMostroNotifier extends StateNotifier { _sessionTimeouts[orderId] = Timer(const Duration(seconds: 10), () { try { ref.read(sessionNotifierProvider.notifier).deleteSession(orderId); - Logger().i('Session cleaned up after 10s timeout: $orderId'); + logger.i('Session cleaned up after 10s timeout: $orderId'); // Show timeout message to user and navigate to order book _showTimeoutNotificationAndNavigate(ref); } catch (e) { - Logger().e('Failed to cleanup session: $orderId', error: e); + logger.e('Failed to cleanup session: $orderId', error: e); } _sessionTimeouts.remove(orderId); }); - Logger().i('Started 10s timeout timer for order: $orderId'); + logger.i('Started 10s timeout timer for order: $orderId'); } /// Shows timeout notification and navigates to order book @@ -528,7 +527,7 @@ class AbstractMostroNotifier extends StateNotifier { final navProvider = ref.read(navigationProvider.notifier); navProvider.go('/'); } catch (e) { - Logger().e('Failed to show timeout notification or navigate', error: e); + logger.e('Failed to show timeout notification or navigate', error: e); } } @@ -541,17 +540,17 @@ class AbstractMostroNotifier extends StateNotifier { _sessionTimeouts[key] = Timer(const Duration(seconds: 10), () { try { ref.read(sessionNotifierProvider.notifier).deleteSessionByRequestId(requestId); - Logger().i('Session cleaned up after 10s timeout for requestId: $requestId'); + logger.i('Session cleaned up after 10s timeout for requestId: $requestId'); // Show timeout message to user and navigate to order book _showTimeoutNotificationAndNavigate(ref); } catch (e) { - Logger().e('Failed to cleanup session for requestId: $requestId', error: e); + logger.e('Failed to cleanup session for requestId: $requestId', error: e); } _sessionTimeouts.remove(key); }); - Logger().i('Started 10s timeout timer for requestId: $requestId'); + logger.i('Started 10s timeout timer for requestId: $requestId'); } /// Cancels the timeout timer for a specific orderId @@ -560,7 +559,7 @@ class AbstractMostroNotifier extends StateNotifier { if (timer != null) { timer.cancel(); _sessionTimeouts.remove(orderId); - Logger().i('Cancelled 10s timeout timer for order: $orderId - Mostro responded'); + logger.i('Cancelled 10s timeout timer for order: $orderId - Mostro responded'); } } @@ -571,7 +570,7 @@ class AbstractMostroNotifier extends StateNotifier { if (timer != null) { timer.cancel(); _sessionTimeouts.remove(key); - Logger().i('Cancelled 10s timeout timer for requestId: $requestId - Mostro responded'); + logger.i('Cancelled 10s timeout timer for requestId: $requestId - Mostro responded'); } } diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index fb1594b8d..78e14ee88 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -6,6 +6,7 @@ import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; class AddOrderNotifier extends AbstractMostroNotifier { diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index dc1a1446a..30da568f6 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -6,6 +6,7 @@ import 'package:mostro_mobile/features/order/models/order_state.dart'; import 'package:mostro_mobile/features/notifications/providers/notifications_provider.dart'; import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; class OrderNotifier extends AbstractMostroNotifier { diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 239ef816a..5233ecbd3 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -16,7 +16,6 @@ import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; class MostroService { final Ref ref; - final _logger = Logger(); Settings _settings; StreamSubscription? _ordersSubscription; @@ -353,7 +352,7 @@ class MostroService { masterKey: session.fullPrivacy ? null : session.masterKey, keyIndex: session.fullPrivacy ? null : session.keyIndex, ); - _logger + logger .i('Sending DM, Event ID: ${event.id} with payload: ${order.toJson()}'); await ref.read(nostrServiceProvider).publishEvent(event); } From c16c74ac6a69efe3c7838aafeb5b4b95ce24e8f9 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Mon, 12 Jan 2026 16:43:49 -0600 Subject: [PATCH 09/25] Update docs for Phase 3 completion --- docs/LOGGING_IMPLEMENTATION.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/LOGGING_IMPLEMENTATION.md b/docs/LOGGING_IMPLEMENTATION.md index a8f46a1f8..469bcaf73 100644 --- a/docs/LOGGING_IMPLEMENTATION.md +++ b/docs/LOGGING_IMPLEMENTATION.md @@ -22,11 +22,12 @@ Implementation of a comprehensive logging system for MostroP2P mobile app with i - Connect UI to actual logger - Test with 2 files: RelaysNotifier, SubscriptionManager -### Phase 3: Core Services Migration (Current) +### Phase 3: Core Services Migration (Completed) - NostrService - MostroService - DeepLinkService -- 2 additional core files +- mostro_storage +- AbstractMostroNotifier + subclases (AddOrderNotifier, OrderNotifier) ### Phase 4: Background Services - Mobile and desktop background service @@ -204,6 +205,6 @@ void backgroundMain(SendPort sendPort) async { --- -**Version**: 3 -**Status**: Phase 2 - Ready -**Last Updated**: 2026-01-06 +**Version**: 4 +**Status**: Phase 3 - Completed +**Last Updated**: 2026-01-12 From 5b7dd99c9d5a3347482d8d7e09787339b8932f64 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Mon, 12 Jan 2026 17:18:11 -0600 Subject: [PATCH 10/25] Enable isolate log receiver for background services --- .../desktop_background_service.dart | 9 ++++++++- lib/background/mobile_background_service.dart | 19 ++++++++++--------- lib/main.dart | 3 +++ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/lib/background/desktop_background_service.dart b/lib/background/desktop_background_service.dart index 6e5535aa8..602a19582 100644 --- a/lib/background/desktop_background_service.dart +++ b/lib/background/desktop_background_service.dart @@ -6,6 +6,7 @@ import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/nostr_filter.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/services/logger_service.dart' as logger_service; import 'abstract_background_service.dart'; class DesktopBackgroundService implements BackgroundService { @@ -22,13 +23,19 @@ class DesktopBackgroundService implements BackgroundService { final isolateReceivePort = ReceivePort(); final mainSendPort = args[0] as SendPort; final token = args[1] as RootIsolateToken; + final loggerSendPort = args.length > 2 ? args[2] as SendPort? : null; mainSendPort.send(isolateReceivePort.sendPort); BackgroundIsolateBinaryMessenger.ensureInitialized(token); + final logger = Logger( + printer: logger_service.SimplePrinter(), + output: logger_service.IsolateLogOutput(loggerSendPort), + level: Level.debug, + ); + final nostrService = NostrService(); - final logger = Logger(); bool isAppForeground = true; isolateReceivePort.listen((message) async { diff --git a/lib/background/mobile_background_service.dart b/lib/background/mobile_background_service.dart index f42c1660f..975a6c89a 100644 --- a/lib/background/mobile_background_service.dart +++ b/lib/background/mobile_background_service.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/background/background.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/services/logger_service.dart' as logger_service; import 'abstract_background_service.dart'; class MobileBackgroundService implements BackgroundService { @@ -16,7 +16,6 @@ class MobileBackgroundService implements BackgroundService { final _subscriptions = >{}; bool _isRunning = false; - final _logger = Logger(); bool _serviceReady = false; final List _pendingOperations = []; @@ -44,19 +43,20 @@ class MobileBackgroundService implements BackgroundService { _isRunning = true; service.invoke('start', { 'settings': _settings.toJson(), + 'loggerSendPort': logger_service.isolateLogSenderPort, }); - _logger.d( + logger_service.logger.d( 'Service started with settings: ${_settings.toJson()}', ); }); service.on('on-stop').listen((event) { _isRunning = false; - _logger.i('Service stopped'); + logger_service.logger.i('Service stopped'); }); service.on('service-ready').listen((data) { - _logger.i("Service confirmed it's ready"); + logger_service.logger.i("Service confirmed it's ready"); _serviceReady = true; _processPendingOperations(); }); @@ -68,7 +68,7 @@ class MobileBackgroundService implements BackgroundService { _subscriptions[subId] = {'filters': filters}; _executeWhenReady(() { - _logger.i("Sending subscription to service"); + logger_service.logger.i("Sending subscription to service"); service.invoke('create-subscription', { 'id': subId, 'filters': filters.map((f) => f.toMap()).toList(), @@ -127,7 +127,7 @@ class MobileBackgroundService implements BackgroundService { try { await _startService(); } catch (e) { - _logger.e('Error starting service: $e'); + logger_service.logger.e('Error starting service: $e'); // Retry with a delay if needed await Future.delayed(Duration(seconds: 1)); await _startService(); @@ -137,7 +137,7 @@ class MobileBackgroundService implements BackgroundService { } Future _startService() async { - _logger.i("Starting service"); + logger_service.logger.i("Starting service"); await service.startService(); _serviceReady = false; // Reset ready state when starting @@ -152,9 +152,10 @@ class MobileBackgroundService implements BackgroundService { await Future.delayed(const Duration(milliseconds: 50)); } - _logger.i("Service running, sending settings"); + logger_service.logger.i("Service running, sending settings"); service.invoke('start', { 'settings': _settings.toJson(), + 'loggerSendPort': logger_service.isolateLogSenderPort, }); } diff --git a/lib/main.dart b/lib/main.dart index 83f2a2268..2222dcb45 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,12 +15,15 @@ import 'package:mostro_mobile/shared/providers/background_service_provider.dart' import 'package:mostro_mobile/shared/providers/providers.dart'; import 'package:mostro_mobile/shared/utils/biometrics_helper.dart'; import 'package:mostro_mobile/shared/utils/notification_permission_helper.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:timeago/timeago.dart' as timeago; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + initIsolateLogReceiver(); + await requestNotificationPermissionIfNeeded(); final biometricsHelper = BiometricsHelper(); From 389b4be8ea496637a57c61094e6c6c3c1f7fd181 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Mon, 12 Jan 2026 17:18:53 -0600 Subject: [PATCH 11/25] Update docs for Phase 4 completion --- docs/LOGGING_IMPLEMENTATION.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/LOGGING_IMPLEMENTATION.md b/docs/LOGGING_IMPLEMENTATION.md index 469bcaf73..08be6de09 100644 --- a/docs/LOGGING_IMPLEMENTATION.md +++ b/docs/LOGGING_IMPLEMENTATION.md @@ -29,9 +29,10 @@ Implementation of a comprehensive logging system for MostroP2P mobile app with i - mostro_storage - AbstractMostroNotifier + subclases (AddOrderNotifier, OrderNotifier) -### Phase 4: Background Services -- Mobile and desktop background service -- Isolate logging +### Phase 4: Background Services (Completed) +- Mobile background service with isolate logging +- Desktop background service with isolate logging +- Isolate log receiver initialized in main.dart ### Phase 5: File Export & Persistence - Auto-save to storage @@ -205,6 +206,6 @@ void backgroundMain(SendPort sendPort) async { --- -**Version**: 4 -**Status**: Phase 3 - Completed +**Version**: 5 +**Status**: Phase 4 - Completed **Last Updated**: 2026-01-12 From cfd4fe77daca7c2ea30bb8656252dfba1b77e562 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Tue, 13 Jan 2026 10:03:01 -0600 Subject: [PATCH 12/25] Add bottom padding to logs list to prevent overlap with system buttons --- lib/features/logs/screens/logs_screen.dart | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/lib/features/logs/screens/logs_screen.dart b/lib/features/logs/screens/logs_screen.dart index cfbf48516..37270b362 100644 --- a/lib/features/logs/screens/logs_screen.dart +++ b/lib/features/logs/screens/logs_screen.dart @@ -184,17 +184,19 @@ class _LogsScreenState extends ConsumerState { ), ], ), - body: Column( - children: [ - _buildStatsHeader(allLogs.length, logs.length, isLoggingEnabled), - _buildSearchBar(), - _buildFilterChips(), - Expanded( - child: logs.isEmpty - ? _buildEmptyState() - : _buildLogsList(logs), - ), - ], + body: SafeArea( + child: Column( + children: [ + _buildStatsHeader(allLogs.length, logs.length, isLoggingEnabled), + _buildSearchBar(), + _buildFilterChips(), + Expanded( + child: logs.isEmpty + ? _buildEmptyState() + : _buildLogsList(logs), + ), + ], + ), ), ), if (_showScrollToTop && logs.isNotEmpty) @@ -431,7 +433,7 @@ class _LogsScreenState extends ConsumerState { Widget _buildLogsList(List logs) { return ListView.builder( controller: _scrollController, - padding: const EdgeInsets.all(8), + padding: const EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 16), itemCount: logs.length, itemBuilder: (context, index) { final log = logs[logs.length - 1 - index]; From 80c6415914f47a01e745aba9e3ce2cd2e5013b24 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Tue, 13 Jan 2026 11:37:17 -0600 Subject: [PATCH 13/25] Localize relative time strings using timeAgoWithLocale extension --- lib/features/logs/screens/logs_screen.dart | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/lib/features/logs/screens/logs_screen.dart b/lib/features/logs/screens/logs_screen.dart index 37270b362..880e10503 100644 --- a/lib/features/logs/screens/logs_screen.dart +++ b/lib/features/logs/screens/logs_screen.dart @@ -7,6 +7,7 @@ import 'package:mostro_mobile/features/logs/logs_provider.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/generated/l10n.dart'; import 'package:mostro_mobile/services/logger_service.dart'; +import 'package:mostro_mobile/shared/utils/datetime_extensions_utils.dart'; class LogsScreen extends ConsumerStatefulWidget { const LogsScreen({super.key}); @@ -534,17 +535,6 @@ class _LogsScreenState extends ConsumerState { } String _formatTime(DateTime timestamp) { - final now = DateTime.now(); - final difference = now.difference(timestamp); - - if (difference.inSeconds < 60) { - return '${difference.inSeconds}s ago'; - } else if (difference.inMinutes < 60) { - return '${difference.inMinutes}m ago'; - } else if (difference.inHours < 24) { - return '${difference.inHours}h ago'; - } else { - return '${timestamp.hour.toString().padLeft(2, '0')}:${timestamp.minute.toString().padLeft(2, '0')}'; - } + return timestamp.timeAgoWithLocale(context); } } From 04c44821fd669ff652a7d12980a726a73f333abf Mon Sep 17 00:00:00 2001 From: bracr10 Date: Tue, 13 Jan 2026 12:01:24 -0600 Subject: [PATCH 14/25] Increase bottom padding in logs list to prevent overlap with system navigation --- lib/features/logs/screens/logs_screen.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/features/logs/screens/logs_screen.dart b/lib/features/logs/screens/logs_screen.dart index 880e10503..fadcdad40 100644 --- a/lib/features/logs/screens/logs_screen.dart +++ b/lib/features/logs/screens/logs_screen.dart @@ -432,9 +432,10 @@ class _LogsScreenState extends ConsumerState { } Widget _buildLogsList(List logs) { + final bottomInset = MediaQuery.of(context).viewPadding.bottom; return ListView.builder( controller: _scrollController, - padding: const EdgeInsets.only(left: 8, right: 8, top: 8, bottom: 16), + padding: EdgeInsets.only(left: 8, right: 8, top: 8, bottom: bottomInset + 64), itemCount: logs.length, itemBuilder: (context, index) { final log = logs[logs.length - 1 - index]; From 8382b5573954734710ef76751d8e70c95f5191ae Mon Sep 17 00:00:00 2001 From: Brian Ramirez <120234671+BraCR10@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:49:43 -0600 Subject: [PATCH 15/25] Fix typo in LOGGING_IMPLEMENTATION.md --- docs/LOGGING_IMPLEMENTATION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/LOGGING_IMPLEMENTATION.md b/docs/LOGGING_IMPLEMENTATION.md index 08be6de09..e4dbc445a 100644 --- a/docs/LOGGING_IMPLEMENTATION.md +++ b/docs/LOGGING_IMPLEMENTATION.md @@ -27,7 +27,7 @@ Implementation of a comprehensive logging system for MostroP2P mobile app with i - MostroService - DeepLinkService - mostro_storage -- AbstractMostroNotifier + subclases (AddOrderNotifier, OrderNotifier) +- AbstractMostroNotifier + subclasses (AddOrderNotifier, OrderNotifier) ### Phase 4: Background Services (Completed) - Mobile background service with isolate logging From 2e03a1e3ca8d5b897deed55f08bc0263354797a8 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Tue, 13 Jan 2026 17:01:36 -0600 Subject: [PATCH 16/25] Configure logger with IsolateLogOutput in mobile background isolate --- lib/background/background.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/lib/background/background.dart b/lib/background/background.dart index f9f6552ec..1eae27403 100644 --- a/lib/background/background.dart +++ b/lib/background/background.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:isolate'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; @@ -8,6 +9,7 @@ import 'package:mostro_mobile/data/repositories/event_storage.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart' as notification_service; import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/services/logger_service.dart' as logger_service; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; bool isAppForeground = true; @@ -15,6 +17,8 @@ String currentLanguage = 'en'; @pragma('vm:entry-point') Future serviceMain(ServiceInstance service) async { + SendPort? loggerSendPort; + Logger? logger; final Map> activeSubscriptions = {}; final nostrService = NostrService(); @@ -31,6 +35,14 @@ Future serviceMain(ServiceInstance service) async { final settingsMap = data['settings']; if (settingsMap == null) return; + loggerSendPort = data['loggerSendPort'] as SendPort?; + + logger = Logger( + printer: logger_service.SimplePrinter(), + output: logger_service.IsolateLogOutput(loggerSendPort), + level: Level.debug, + ); + final settings = Settings.fromJson(settingsMap); currentLanguage = settings.selectedLanguage ?? PlatformDispatcher.instance.locale.languageCode; await nostrService.init(settings); @@ -74,7 +86,7 @@ Future serviceMain(ServiceInstance service) async { } await notification_service.retryNotification(event); } catch (e) { - Logger().e('Error processing event', error: e); + logger?.e('Error processing event', error: e); } }); }); From 8e2e557b8884c9980eab92dff51441c024eef0bb Mon Sep 17 00:00:00 2001 From: bracr10 Date: Tue, 13 Jan 2026 17:24:48 -0600 Subject: [PATCH 17/25] Print background isolate logs to console only in debug mode --- lib/services/logger_service.dart | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart index 6ac821032..c44065fd9 100644 --- a/lib/services/logger_service.dart +++ b/lib/services/logger_service.dart @@ -287,9 +287,11 @@ class IsolateLogOutput extends LogOutput { @override void output(OutputEvent event) { - for (final line in event.lines) { - // ignore: avoid_print - print(line); + if (Config.isDebug) { + for (final line in event.lines) { + // ignore: avoid_print + print(line); + } } if (sendPort != null) { From 30546b7b6fddfe824cfd29c81f033a49391173ae Mon Sep 17 00:00:00 2001 From: bracr10 Date: Thu, 15 Jan 2026 14:17:16 -0600 Subject: [PATCH 18/25] Add fallback logging for errors before logger initialization --- lib/background/background.dart | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/lib/background/background.dart b/lib/background/background.dart index 1eae27403..46f49509a 100644 --- a/lib/background/background.dart +++ b/lib/background/background.dart @@ -86,7 +86,13 @@ Future serviceMain(ServiceInstance service) async { } await notification_service.retryNotification(event); } catch (e) { - logger?.e('Error processing event', error: e); + final currentLogger = logger; + if (currentLogger != null) { + currentLogger.e('Error processing event', error: e); + } else { + // ignore: avoid_print + print('ERROR (logger not ready): Error processing event: $e'); + } } }); }); From cce55415eee6c8b4cef16f1230f46c343239d07d Mon Sep 17 00:00:00 2001 From: bracr10 Date: Sat, 17 Jan 2026 09:55:46 -0600 Subject: [PATCH 19/25] Add logger export service for logs save and share Create LoggerExportService with methods to: - Generate timestamped filenames (mostro_logs_YYYY-MM-DD_HH-MM-SS.txt) - Convert LogEntry list to formatted text with metadata header - Export logs using FilePicker.saveFile() for Android 13+ SAF compatibility - Export logs to temp directory for sharing - Share logs via native share sheet using Share.shareXFiles() --- lib/services/logger_export_service.dart | 70 +++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 lib/services/logger_export_service.dart diff --git a/lib/services/logger_export_service.dart b/lib/services/logger_export_service.dart new file mode 100644 index 000000000..3e3e996c8 --- /dev/null +++ b/lib/services/logger_export_service.dart @@ -0,0 +1,70 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:convert'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; + +class LoggerExportService { + static String _generateFilename() { + final now = DateTime.now(); + final timestamp = '${now.year}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')}_' + '${now.hour.toString().padLeft(2, '0')}-${now.minute.toString().padLeft(2, '0')}-${now.second.toString().padLeft(2, '0')}'; + return 'mostro_logs_$timestamp.txt'; + } + + static String _logsToText(List logs) { + if (logs.isEmpty) return 'No logs available\n'; + + final buffer = StringBuffer(); + buffer.writeln('Mostro P2P Application Logs'); + buffer.writeln('Generated: ${DateTime.now()}'); + buffer.writeln('Total logs: ${logs.length}'); + buffer.writeln('${'=' * 60}\n'); + + for (final log in logs) { + buffer.writeln(log.format()); + } + + return buffer.toString(); + } + + static Future exportLogsToFolder(List logs) async { + final filename = _generateFilename(); + final content = _logsToText(logs); + final bytes = Uint8List.fromList(utf8.encode(content)); + + final result = await FilePicker.platform.saveFile( + dialogTitle: 'Save Logs', + fileName: filename, + type: FileType.custom, + allowedExtensions: ['txt'], + bytes: bytes, + ); + + return result; + } + + static Future exportLogsForSharing(List logs) async { + final tempDir = await getTemporaryDirectory(); + final filename = _generateFilename(); + final filePath = p.join(tempDir.path, filename); + final file = File(filePath); + + final content = _logsToText(logs); + await file.writeAsString(content); + + return file; + } + + static Future shareLogs(File file) async { + final xFile = XFile(file.path); + await Share.shareXFiles( + [xFile], + subject: 'Mostro P2P Logs', + text: 'Application logs from Mostro P2P', + ); + } +} From 13fd6b2411be7d9ba31d0ccc28f3673ffca6404c Mon Sep 17 00:00:00 2001 From: bracr10 Date: Sat, 17 Jan 2026 09:56:09 -0600 Subject: [PATCH 20/25] Add hamburger menu widget for logs screen Create LogsActionsMenu with PopupMenuButton containing three actions: - Save: Export logs to user-selected location via FilePicker - Share: Share logs via native system share sheet - Clear: Delete all logs with confirmation dialog Features: - Uses HeroIcons for consistent iconography - Menu items disabled when no logs available - Proper error handling with SnackBar feedback - Success message without showing file path for cleaner UX --- .../logs/widgets/logs_actions_menu.dart | 187 ++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100644 lib/features/logs/widgets/logs_actions_menu.dart diff --git a/lib/features/logs/widgets/logs_actions_menu.dart b/lib/features/logs/widgets/logs_actions_menu.dart new file mode 100644 index 000000000..17bcf7b37 --- /dev/null +++ b/lib/features/logs/widgets/logs_actions_menu.dart @@ -0,0 +1,187 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:logger/logger.dart'; +import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/features/logs/logs_provider.dart'; +import 'package:mostro_mobile/generated/l10n.dart'; +import 'package:mostro_mobile/services/logger_export_service.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; + +class LogsActionsMenu extends ConsumerWidget { + final _logger = Logger(); + + LogsActionsMenu({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final logs = ref.watch(logsProvider); + final hasLogs = logs.isNotEmpty; + + return PopupMenuButton( + icon: const HeroIcon( + HeroIcons.ellipsisVertical, + style: HeroIconStyle.outline, + color: AppTheme.cream1, + size: 24, + ), + color: AppTheme.backgroundDark, + onSelected: (value) => _handleMenuAction(context, ref, value, logs), + itemBuilder: (context) => [ + _buildMenuItem( + 'save', + HeroIcons.arrowDownTray, + S.of(context)!.saveLogs, + hasLogs ? AppTheme.cream1 : AppTheme.textSecondary, + enabled: hasLogs, + ), + _buildMenuItem( + 'share', + HeroIcons.share, + S.of(context)!.shareLogs, + hasLogs ? AppTheme.cream1 : AppTheme.textSecondary, + enabled: hasLogs, + ), + _buildMenuItem( + 'clear', + HeroIcons.trash, + S.of(context)!.clearLogs, + hasLogs ? AppTheme.statusError : AppTheme.textSecondary, + enabled: hasLogs, + ), + ], + ); + } + + PopupMenuItem _buildMenuItem( + String value, + HeroIcons icon, + String label, + Color color, { + bool enabled = true, + }) { + return PopupMenuItem( + value: value, + enabled: enabled, + child: Row( + children: [ + HeroIcon( + icon, + style: HeroIconStyle.outline, + size: 20, + color: color, + ), + const SizedBox(width: 12), + Text( + label, + style: TextStyle( + color: enabled ? AppTheme.textPrimary : AppTheme.textSecondary, + ), + ), + ], + ), + ); + } + + Future _handleMenuAction( + BuildContext context, + WidgetRef ref, + String action, + List logs, + ) async { + switch (action) { + case 'save': + await _saveLogsToFolder(context, ref, logs); + break; + case 'share': + await _shareLogsFile(context, logs); + break; + case 'clear': + await _showClearConfirmation(context, ref); + break; + } + } + + Future _saveLogsToFolder( + BuildContext context, + WidgetRef ref, + List logs, + ) async { + try { + final filePath = await LoggerExportService.exportLogsToFolder(logs); + + if (filePath != null && context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context)!.logsExportSuccess), + backgroundColor: AppTheme.statusSuccess, + ), + ); + } + } catch (e, stackTrace) { + _logger.e('Error exporting logs', error: e, stackTrace: stackTrace); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${S.of(context)!.logsExportError}: $e'), + backgroundColor: AppTheme.statusError, + ), + ); + } + } + } + + Future _shareLogsFile(BuildContext context, List logs) async { + try { + final file = await LoggerExportService.exportLogsForSharing(logs); + await LoggerExportService.shareLogs(file); + } catch (e, stackTrace) { + _logger.e('Error sharing logs', error: e, stackTrace: stackTrace); + if (context.mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('${S.of(context)!.shareLogsError}: $e'), + backgroundColor: AppTheme.statusError, + ), + ); + } + } + } + + Future _showClearConfirmation(BuildContext context, WidgetRef ref) async { + final confirmed = await showDialog( + context: context, + builder: (context) => AlertDialog( + backgroundColor: AppTheme.backgroundDark, + title: Text( + S.of(context)!.clearLogs, + style: const TextStyle(color: AppTheme.textPrimary), + ), + content: Text( + S.of(context)!.clearLogsConfirmation, + style: const TextStyle(color: AppTheme.textSecondary), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text( + S.of(context)!.cancel, + style: const TextStyle(color: AppTheme.textSecondary), + ), + ), + TextButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text( + S.of(context)!.clear, + style: const TextStyle(color: AppTheme.statusError), + ), + ), + ], + ), + ); + + if (confirmed == true) { + ref.read(logsProvider.notifier).clearLogs(); + } + } +} From 5331c4be221127a59ffa9861ee8f07fa28fa4bc3 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Sat, 17 Jan 2026 09:56:29 -0600 Subject: [PATCH 21/25] Integrate hamburger menu into logs screen Replace individual IconButtons with LogsActionsMenu in AppBar. Remove methods moved to menu widget. Clean up lifecycle observer. Simplified scope: manual save only when user explicitly requests it. --- lib/features/logs/screens/logs_screen.dart | 54 ++++------------------ 1 file changed, 8 insertions(+), 46 deletions(-) diff --git a/lib/features/logs/screens/logs_screen.dart b/lib/features/logs/screens/logs_screen.dart index fadcdad40..598381350 100644 --- a/lib/features/logs/screens/logs_screen.dart +++ b/lib/features/logs/screens/logs_screen.dart @@ -4,6 +4,7 @@ import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/features/logs/logs_provider.dart'; +import 'package:mostro_mobile/features/logs/widgets/logs_actions_menu.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/generated/l10n.dart'; import 'package:mostro_mobile/services/logger_service.dart'; @@ -16,7 +17,7 @@ class LogsScreen extends ConsumerStatefulWidget { ConsumerState createState() => _LogsScreenState(); } -class _LogsScreenState extends ConsumerState { +class _LogsScreenState extends ConsumerState with WidgetsBindingObserver { String? _selectedLevel; String _searchQuery = ''; final TextEditingController _searchController = TextEditingController(); @@ -27,6 +28,7 @@ class _LogsScreenState extends ConsumerState { void initState() { super.initState(); _scrollController.addListener(_onScroll); + WidgetsBinding.instance.addObserver(this); } void _onScroll() { @@ -49,11 +51,15 @@ class _LogsScreenState extends ConsumerState { @override void dispose() { + WidgetsBinding.instance.removeObserver(this); _scrollController.dispose(); _searchController.dispose(); super.dispose(); } + @override + void didChangeAppLifecycleState(AppLifecycleState state) {} + Future _toggleLogging(bool value) async { if (value) { await _showPerformanceWarning(); @@ -109,46 +115,6 @@ class _LogsScreenState extends ConsumerState { } } - Future _showClearConfirmation() async { - final confirmed = await showDialog( - context: context, - builder: (context) => AlertDialog( - backgroundColor: AppTheme.backgroundCard, - title: Text( - S.of(context)!.clearLogs, - style: TextStyle(color: AppTheme.textPrimary), - ), - content: Text( - S.of(context)!.clearLogsConfirmation, - style: TextStyle(color: AppTheme.textSecondary), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(S.of(context)!.cancel), - ), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(true), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.statusError, - ), - child: Text(S.of(context)!.clear), - ), - ], - ), - ); - - if (confirmed == true && mounted) { - ref.read(logsProvider.notifier).clearLogs(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context)!.logsCleared)), - ); - } - }); - } - } @override Widget build(BuildContext context) { @@ -178,11 +144,7 @@ class _LogsScreenState extends ConsumerState { ), iconTheme: const IconThemeData(color: AppTheme.textPrimary), actions: [ - IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: logs.isEmpty ? null : _showClearConfirmation, - tooltip: S.of(context)!.clearLogs, - ), + LogsActionsMenu(), ], ), body: SafeArea( From 5ae1fdf442c71a9a4434b3b07dedc903cbd1342b Mon Sep 17 00:00:00 2001 From: bracr10 Date: Sat, 17 Jan 2026 09:56:35 -0600 Subject: [PATCH 22/25] Remove unused logExportPath from Settings model Clean up Settings model by removing logExportPath field and related code. Simplified implementation: no default folder configuration. --- lib/features/settings/settings.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/features/settings/settings.dart b/lib/features/settings/settings.dart index 0c6f0c03c..35cbef282 100644 --- a/lib/features/settings/settings.dart +++ b/lib/features/settings/settings.dart @@ -5,8 +5,8 @@ class Settings { final String? defaultFiatCode; final String? selectedLanguage; // null means use system locale final String? defaultLightningAddress; - final List blacklistedRelays; // Relays blocked by user from auto-sync - final List> userRelays; // User-added relays with metadata + final List blacklistedRelays; + final List> userRelays; final bool isLoggingEnabled; Settings({ From 2e1cc9aaf4286cd2b3c0a3878c0da654164cc86e Mon Sep 17 00:00:00 2001 From: bracr10 Date: Sat, 17 Jan 2026 09:56:42 -0600 Subject: [PATCH 23/25] Add translations for logs export functionality Add translations in all languages (EN, ES, IT) for: - saveLogs, logsExportSuccess, logsExportError - shareLogsError, shareLogs - exportSettings, close Simplified success message without showing file path. --- lib/l10n/intl_en.arb | 9 ++++++++- lib/l10n/intl_es.arb | 9 ++++++++- lib/l10n/intl_it.arb | 9 ++++++++- 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index e6a663cf3..c0ac60ff6 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1293,6 +1293,13 @@ "shareFeatureComingSoon": "Share feature coming in Phase 2", "devTools": "Dev Tools", "devToolsWarning": "For debugging and troubleshooting only", - "viewAndExportLogs": "View and export application logs" + "viewAndExportLogs": "View and export application logs", + "saveLogs": "Save Logs", + "logsExportSuccess": "Logs exported successfully", + "logsExportError": "Failed to export logs", + "shareLogsError": "Failed to share logs", + "shareLogs": "Share Logs", + "exportSettings": "Export Settings", + "close": "Close" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index 328d8fc53..aa47360dc 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1269,6 +1269,13 @@ "shareFeatureComingSoon": "Función de compartir disponible en Fase 2", "devTools": "Herramientas de Desarrollo", "devToolsWarning": "Solo para depuración y solución de problemas", - "viewAndExportLogs": "Ver y exportar registros de la aplicación" + "viewAndExportLogs": "Ver y exportar registros de la aplicación", + "saveLogs": "Guardar Registros", + "logsExportSuccess": "Registros exportados exitosamente", + "logsExportError": "Error al exportar registros", + "shareLogsError": "Error al compartir registros", + "shareLogs": "Compartir Registros", + "exportSettings": "Configuración de Exportación", + "close": "Cerrar" } diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index d53047d7f..27b5f3d16 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1324,5 +1324,12 @@ "shareFeatureComingSoon": "Funzione di condivisione disponibile nella Fase 2", "devTools": "Strumenti di Sviluppo", "devToolsWarning": "Solo per debug e risoluzione problemi", - "viewAndExportLogs": "Visualizza ed esporta i log dell'applicazione" + "viewAndExportLogs": "Visualizza ed esporta i log dell'applicazione", + "saveLogs": "Salva Log", + "logsExportSuccess": "Log esportati con successo", + "logsExportError": "Impossibile esportare i log", + "shareLogsError": "Impossibile condividere i log", + "shareLogs": "Condividi Log", + "exportSettings": "Impostazioni Esportazione", + "close": "Chiudi" } From eab5ec3c1c11486cdd7e2d295d63b568799508f2 Mon Sep 17 00:00:00 2001 From: bracr10 Date: Sat, 17 Jan 2026 09:56:50 -0600 Subject: [PATCH 24/25] Update documentation for Phase 5 completion Mark Phase 5 as completed with implemented features: - Manual export via hamburger menu - Save logs to user-selected location using FilePicker - Share logs via native system share sheet - Generate timestamped .txt files - Clear logs with confirmation dialog - Simplified scope: manual save only, no auto-save or default folder --- docs/LOGGING_IMPLEMENTATION.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/LOGGING_IMPLEMENTATION.md b/docs/LOGGING_IMPLEMENTATION.md index e4dbc445a..0208bbd68 100644 --- a/docs/LOGGING_IMPLEMENTATION.md +++ b/docs/LOGGING_IMPLEMENTATION.md @@ -34,11 +34,12 @@ Implementation of a comprehensive logging system for MostroP2P mobile app with i - Desktop background service with isolate logging - Isolate log receiver initialized in main.dart -### Phase 5: File Export & Persistence -- Auto-save to storage -- Restore on app restart -- Generate .txt files +### Phase 5: File Export & Persistence (Completed) +- Manual export via hamburger menu +- Save logs to user-selected location using FilePicker +- Share logs via native system share sheet - Folder picker and permissions +- Clear logs with confirmation dialog ### Phase 6: UI Enhancements - Recording indicator widget @@ -206,6 +207,6 @@ void backgroundMain(SendPort sendPort) async { --- -**Version**: 5 -**Status**: Phase 4 - Completed +**Version**: 6 +**Status**: Phase 5 - Completed **Last Updated**: 2026-01-12 From 7c7056078007ba5be1036f6b744996b65858b3da Mon Sep 17 00:00:00 2001 From: bracr10 Date: Tue, 20 Jan 2026 17:56:42 -0600 Subject: [PATCH 25/25] Fix: code rabbit suggestions Refactors log export and sharing to utilize localized strings for file content, subject, and text. This change ensures a consistent user experience across different languages when exporting and sharing logs by using the app's localization system. It also includes a title to the logs file, generated timestamp and the total number of logs written. --- .../logs/widgets/logs_actions_menu.dart | 32 ++++++++++--- lib/l10n/intl_en.arb | 7 ++- lib/l10n/intl_es.arb | 7 ++- lib/l10n/intl_it.arb | 7 ++- lib/services/logger_export_service.dart | 48 ++++++++++++++----- 5 files changed, 77 insertions(+), 24 deletions(-) diff --git a/lib/features/logs/widgets/logs_actions_menu.dart b/lib/features/logs/widgets/logs_actions_menu.dart index 17bcf7b37..b64c0bf3b 100644 --- a/lib/features/logs/widgets/logs_actions_menu.dart +++ b/lib/features/logs/widgets/logs_actions_menu.dart @@ -107,13 +107,21 @@ class LogsActionsMenu extends ConsumerWidget { WidgetRef ref, List logs, ) async { + final localizations = S.of(context)!; + final strings = LogExportStrings( + headerTitle: localizations.logsHeaderTitle, + generatedLabel: localizations.logsGeneratedLabel, + totalLabel: localizations.logsTotalLabel, + emptyMessage: localizations.noLogsAvailable, + ); + try { - final filePath = await LoggerExportService.exportLogsToFolder(logs); + final filePath = await LoggerExportService.exportLogsToFolder(logs, strings); if (filePath != null && context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text(S.of(context)!.logsExportSuccess), + content: Text(localizations.logsExportSuccess), backgroundColor: AppTheme.statusSuccess, ), ); @@ -123,7 +131,7 @@ class LogsActionsMenu extends ConsumerWidget { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${S.of(context)!.logsExportError}: $e'), + content: Text(localizations.logsExportError), backgroundColor: AppTheme.statusError, ), ); @@ -132,15 +140,27 @@ class LogsActionsMenu extends ConsumerWidget { } Future _shareLogsFile(BuildContext context, List logs) async { + final localizations = S.of(context)!; + final strings = LogExportStrings( + headerTitle: localizations.logsHeaderTitle, + generatedLabel: localizations.logsGeneratedLabel, + totalLabel: localizations.logsTotalLabel, + emptyMessage: localizations.noLogsAvailable, + ); + try { - final file = await LoggerExportService.exportLogsForSharing(logs); - await LoggerExportService.shareLogs(file); + final file = await LoggerExportService.exportLogsForSharing(logs, strings); + await LoggerExportService.shareLogs( + file, + subject: localizations.logsShareSubject, + text: localizations.logsShareText, + ); } catch (e, stackTrace) { _logger.e('Error sharing logs', error: e, stackTrace: stackTrace); if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( - content: Text('${S.of(context)!.shareLogsError}: $e'), + content: Text(localizations.shareLogsError), backgroundColor: AppTheme.statusError, ), ); diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index c0ac60ff6..6ddd7bf56 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1298,8 +1298,11 @@ "logsExportSuccess": "Logs exported successfully", "logsExportError": "Failed to export logs", "shareLogsError": "Failed to share logs", - "shareLogs": "Share Logs", "exportSettings": "Export Settings", - "close": "Close" + "logsHeaderTitle": "Mostro P2P Application Logs", + "logsGeneratedLabel": "Generated", + "logsTotalLabel": "Total logs", + "logsShareSubject": "Mostro P2P Logs", + "logsShareText": "Application logs from Mostro P2P" } diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index aa47360dc..75c273ab7 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1274,8 +1274,11 @@ "logsExportSuccess": "Registros exportados exitosamente", "logsExportError": "Error al exportar registros", "shareLogsError": "Error al compartir registros", - "shareLogs": "Compartir Registros", "exportSettings": "Configuración de Exportación", - "close": "Cerrar" + "logsHeaderTitle": "Registros de Aplicación Mostro P2P", + "logsGeneratedLabel": "Generado", + "logsTotalLabel": "Total de registros", + "logsShareSubject": "Registros Mostro P2P", + "logsShareText": "Registros de aplicación de Mostro P2P" } diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 27b5f3d16..42cf86883 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1329,7 +1329,10 @@ "logsExportSuccess": "Log esportati con successo", "logsExportError": "Impossibile esportare i log", "shareLogsError": "Impossibile condividere i log", - "shareLogs": "Condividi Log", "exportSettings": "Impostazioni Esportazione", - "close": "Chiudi" + "logsHeaderTitle": "Log Applicazione Mostro P2P", + "logsGeneratedLabel": "Generato", + "logsTotalLabel": "Totale log", + "logsShareSubject": "Log Mostro P2P", + "logsShareText": "Log dell'applicazione Mostro P2P" } diff --git a/lib/services/logger_export_service.dart b/lib/services/logger_export_service.dart index 3e3e996c8..e13e89ab9 100644 --- a/lib/services/logger_export_service.dart +++ b/lib/services/logger_export_service.dart @@ -7,6 +7,20 @@ import 'package:share_plus/share_plus.dart'; import 'package:file_picker/file_picker.dart'; import 'package:mostro_mobile/services/logger_service.dart'; +class LogExportStrings { + final String headerTitle; + final String generatedLabel; + final String totalLabel; + final String emptyMessage; + + const LogExportStrings({ + required this.headerTitle, + required this.generatedLabel, + required this.totalLabel, + required this.emptyMessage, + }); +} + class LoggerExportService { static String _generateFilename() { final now = DateTime.now(); @@ -15,13 +29,13 @@ class LoggerExportService { return 'mostro_logs_$timestamp.txt'; } - static String _logsToText(List logs) { - if (logs.isEmpty) return 'No logs available\n'; + static String _logsToText(List logs, LogExportStrings strings) { + if (logs.isEmpty) return '${strings.emptyMessage}\n'; final buffer = StringBuffer(); - buffer.writeln('Mostro P2P Application Logs'); - buffer.writeln('Generated: ${DateTime.now()}'); - buffer.writeln('Total logs: ${logs.length}'); + buffer.writeln(strings.headerTitle); + buffer.writeln('${strings.generatedLabel}: ${DateTime.now()}'); + buffer.writeln('${strings.totalLabel}: ${logs.length}'); buffer.writeln('${'=' * 60}\n'); for (final log in logs) { @@ -31,9 +45,12 @@ class LoggerExportService { return buffer.toString(); } - static Future exportLogsToFolder(List logs) async { + static Future exportLogsToFolder( + List logs, + LogExportStrings strings, + ) async { final filename = _generateFilename(); - final content = _logsToText(logs); + final content = _logsToText(logs, strings); final bytes = Uint8List.fromList(utf8.encode(content)); final result = await FilePicker.platform.saveFile( @@ -47,24 +64,31 @@ class LoggerExportService { return result; } - static Future exportLogsForSharing(List logs) async { + static Future exportLogsForSharing( + List logs, + LogExportStrings strings, + ) async { final tempDir = await getTemporaryDirectory(); final filename = _generateFilename(); final filePath = p.join(tempDir.path, filename); final file = File(filePath); - final content = _logsToText(logs); + final content = _logsToText(logs, strings); await file.writeAsString(content); return file; } - static Future shareLogs(File file) async { + static Future shareLogs( + File file, { + required String subject, + required String text, + }) async { final xFile = XFile(file.path); await Share.shareXFiles( [xFile], - subject: 'Mostro P2P Logs', - text: 'Application logs from Mostro P2P', + subject: subject, + text: text, ); } }