diff --git a/lib/core/auth/auth_state_manager.dart b/lib/core/auth/auth_state_manager.dart index 678f575..4e451fb 100644 --- a/lib/core/auth/auth_state_manager.dart +++ b/lib/core/auth/auth_state_manager.dart @@ -1,4 +1,4 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter_riverpod/flutter_riverpod.dart'; // Types are used through app_providers.dart import '../providers/app_providers.dart'; @@ -7,6 +7,11 @@ import 'token_validator.dart'; import 'auth_cache_manager.dart'; import '../utils/debug_logger.dart'; +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'auth/state'); +} + /// Comprehensive auth state representation @immutable class AuthState { diff --git a/lib/core/auth/token_validator.dart b/lib/core/auth/token_validator.dart index c0afbd1..c0935ae 100644 --- a/lib/core/auth/token_validator.dart +++ b/lib/core/auth/token_validator.dart @@ -1,6 +1,11 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:crypto/crypto.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'auth/token-validator'); +} /// JWT token validation utilities class TokenValidator { diff --git a/lib/core/error/api_error_handler.dart b/lib/core/error/api_error_handler.dart index 14e00b9..9cf655a 100644 --- a/lib/core/error/api_error_handler.dart +++ b/lib/core/error/api_error_handler.dart @@ -1,9 +1,14 @@ import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'api_error.dart'; import 'error_parser.dart'; import '../utils/debug_logger.dart'; +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'api/error-handler'); +} + /// Comprehensive API error handler with structured error parsing /// Handles all types of API errors and converts them to standardized format class ApiErrorHandler { diff --git a/lib/core/error/api_error_interceptor.dart b/lib/core/error/api_error_interceptor.dart index caea8ef..6cfdb14 100644 --- a/lib/core/error/api_error_interceptor.dart +++ b/lib/core/error/api_error_interceptor.dart @@ -1,9 +1,14 @@ import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'api_error_handler.dart'; import 'api_error.dart'; import '../utils/debug_logger.dart'; +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'api/error-interceptor'); +} + /// Dio interceptor for automatic error handling and transformation /// Converts all HTTP errors into standardized ApiError format class ApiErrorInterceptor extends Interceptor { diff --git a/lib/core/error/enhanced_error_service.dart b/lib/core/error/enhanced_error_service.dart index 2b75048..2da06a7 100644 --- a/lib/core/error/enhanced_error_service.dart +++ b/lib/core/error/enhanced_error_service.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/material.dart'; import 'api_error.dart'; import 'api_error_handler.dart'; @@ -7,6 +7,12 @@ import 'api_error_interceptor.dart'; import '../../shared/theme/app_theme.dart'; import '../../shared/theme/theme_extensions.dart'; import 'package:conduit/l10n/app_localizations.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'api/error-service'); +} /// Enhanced error service with comprehensive error handling capabilities /// Provides unified error management across the application diff --git a/lib/core/error/error_parser.dart b/lib/core/error/error_parser.dart index 6c0ab8f..65b36c7 100644 --- a/lib/core/error/error_parser.dart +++ b/lib/core/error/error_parser.dart @@ -1,5 +1,10 @@ -import 'package:flutter/foundation.dart'; import 'api_error.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'api/error-parser'); +} /// Comprehensive error response parser /// Handles various API error response formats and extracts structured information diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 1aa3d95..2c488b2 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart' as foundation; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -180,9 +179,7 @@ final apiServiceProvider = Provider((ref) { // Keep legacy callback for backward compatibility during transition apiService.onAuthTokenInvalid = () { // This will be removed once migration is complete - foundation.debugPrint( - 'DEBUG: Legacy auth invalidation callback triggered', - ); + DebugLogger.auth('legacy-token-callback', scope: 'auth/api'); }; return apiService; @@ -251,7 +248,11 @@ final apiTokenUpdaterProvider = Provider((ref) { if (api != null) { api.updateAuthToken(next); final length = next?.length ?? 0; - foundation.debugPrint('DEBUG: Applied auth token to API (len=$length)'); + DebugLogger.auth( + 'token-updated', + scope: 'auth/api', + data: {'length': length}, + ); } }); }); @@ -304,19 +305,21 @@ final modelsProvider = FutureProvider>((ref) async { if (api == null) return []; try { - DebugLogger.log('Fetching models from server'); + DebugLogger.log('fetch-start', scope: 'models'); final models = await api.getModels(); - DebugLogger.log('Successfully fetched ${models.length} models'); + DebugLogger.log( + 'fetch-ok', + scope: 'models', + data: {'count': models.length}, + ); return models; } catch (e) { - foundation.debugPrint('ERROR: Failed to fetch models: $e'); + DebugLogger.error('fetch-failed', scope: 'models', error: e); // If models endpoint returns 403, this should now clear auth token // and redirect user to login since it's marked as a core endpoint if (e.toString().contains('403')) { - DebugLogger.warning( - 'Models endpoint returned 403 - authentication may be invalid', - ); + DebugLogger.warning('endpoint-403', scope: 'models'); } return []; @@ -396,13 +399,17 @@ final defaultModelAutoSelectionProvider = Provider((ref) { if (selected != null) { ref.read(selectedModelProvider.notifier).set(selected); - foundation.debugPrint( - 'DEBUG: Auto-applied default model (by ID): ${selected.name}', + DebugLogger.log( + 'auto-apply', + scope: 'models/default', + data: {'name': selected.name}, ); } } catch (e) { - foundation.debugPrint( - 'DEBUG: defaultModel auto-selection listener failed: $e', + DebugLogger.error( + 'auto-select-failed', + scope: 'models/default', + error: e, ); } }); @@ -428,7 +435,9 @@ final conversationsProvider = FutureProvider>((ref) async { final lastFetch = ref.read(_conversationsCacheTimestamp); if (lastFetch != null && DateTime.now().difference(lastFetch).inSeconds < 5) { DebugLogger.log( - 'Using cached conversations (fetched ${DateTime.now().difference(lastFetch).inSeconds}s ago)', + 'cache-hit', + scope: 'conversations', + data: {'ageSecs': DateTime.now().difference(lastFetch).inSeconds}, ); // Note: Can't read our own provider here, would cause a cycle // The caching is handled by Riverpod's built-in mechanism @@ -458,23 +467,27 @@ final conversationsProvider = FutureProvider>((ref) async { } final api = ref.watch(apiServiceProvider); if (api == null) { - DebugLogger.log('No API service available'); + DebugLogger.warning('api-missing', scope: 'conversations'); return []; } try { - DebugLogger.log('Fetching conversations from OpenWebUI API...'); + DebugLogger.log('fetch-start', scope: 'conversations'); final conversations = await api .getConversations(); // Fetch all conversations DebugLogger.log( - 'Successfully fetched ${conversations.length} conversations', + 'fetch-ok', + scope: 'conversations', + data: {'count': conversations.length}, ); // Also fetch folder information and update conversations with folder IDs try { final foldersData = await api.getFolders(); DebugLogger.log( - 'Fetched ${foldersData.length} folders for conversation mapping', + 'folders-fetched', + scope: 'conversations', + data: {'count': foldersData.length}, ); // Parse folder data into Folder objects @@ -485,13 +498,21 @@ final conversationsProvider = FutureProvider>((ref) async { // Create a map of conversation ID to folder ID final conversationToFolder = {}; for (final folder in folders) { - foundation.debugPrint( - 'DEBUG: Folder "${folder.name}" (${folder.id}) has ${folder.conversationIds.length} conversations', + DebugLogger.log( + 'folder', + scope: 'conversations/map', + data: { + 'id': folder.id, + 'name': folder.name, + 'count': folder.conversationIds.length, + }, ); for (final conversationId in folder.conversationIds) { conversationToFolder[conversationId] = folder.id; - foundation.debugPrint( - 'DEBUG: Mapping conversation $conversationId to folder ${folder.id}', + DebugLogger.log( + 'map', + scope: 'conversations/map', + data: {'conversationId': conversationId, 'folderId': folder.id}, ); } } @@ -509,11 +530,14 @@ final conversationsProvider = FutureProvider>((ref) async { conversationMap[conversation.id] = conversation.copyWith( folderId: folderIdToUse, ); - final idPreview = conversation.id.length > 8 - ? conversation.id.substring(0, 8) - : conversation.id; - foundation.debugPrint( - 'DEBUG: Updated conversation $idPreview with folderId: $folderIdToUse (explicit: ${explicitFolderId != null})', + DebugLogger.log( + 'update-folder', + scope: 'conversations/map', + data: { + 'conversationId': conversation.id, + 'folderId': folderIdToUse, + 'explicit': explicitFolderId != null, + }, ); } else { conversationMap[conversation.id] = conversation; @@ -529,17 +553,16 @@ final conversationsProvider = FutureProvider>((ref) async { .where((id) => !existingIds.contains(id)) .toList(); if (missingInBase.isNotEmpty) { - foundation.debugPrint( - 'DEBUG: ${missingInBase.length} conversations referenced by folders are missing from base list', - ); - final preview = missingInBase.take(10).toList(); - foundation.debugPrint( - 'DEBUG: Missing IDs sample: $preview${missingInBase.length > 10 ? ' ...' : ''}', + DebugLogger.warning( + 'missing-in-base', + scope: 'conversations/map', + data: { + 'count': missingInBase.length, + 'preview': missingInBase.take(5).toList(), + }, ); } else { - foundation.debugPrint( - 'DEBUG: All folder-referenced conversations are present in base list', - ); + DebugLogger.log('folders-synced', scope: 'conversations/map'); } // Attempt to fetch missing conversations per-folder to construct accurate entries @@ -558,8 +581,11 @@ final conversationsProvider = FutureProvider>((ref) async { folderConvs = await apiSvc.getConversationsInFolder(folder.id); } } catch (e) { - foundation.debugPrint( - 'DEBUG: getConversationsInFolder failed for ${folder.id}: $e', + DebugLogger.error( + 'folder-fetch-failed', + scope: 'conversations/map', + error: e, + data: {'folderId': folder.id}, ); } @@ -575,11 +601,10 @@ final conversationsProvider = FutureProvider>((ref) async { // Use map to prevent duplicates - this will overwrite if ID already exists conversationMap[toAdd.id] = toAdd; existingIds.add(toAdd.id); - final idPreview = toAdd.id.length > 8 - ? toAdd.id.substring(0, 8) - : toAdd.id; - foundation.debugPrint( - 'DEBUG: Added missing conversation from folder fetch: $idPreview -> folder ${folder.id}', + DebugLogger.log( + 'add-missing', + scope: 'conversations/map', + data: {'conversationId': toAdd.id, 'folderId': folder.id}, ); } else { // Create a minimal placeholder if not returned by folder API @@ -594,11 +619,10 @@ final conversationsProvider = FutureProvider>((ref) async { // Use map to prevent duplicates conversationMap[convId] = placeholder; existingIds.add(convId); - final idPreview = convId.length > 8 - ? convId.substring(0, 8) - : convId; - foundation.debugPrint( - 'DEBUG: Added placeholder conversation for missing ID: $idPreview -> folder ${folder.id}', + DebugLogger.log( + 'add-placeholder', + scope: 'conversations/map', + data: {'conversationId': convId, 'folderId': folder.id}, ); } } @@ -609,8 +633,10 @@ final conversationsProvider = FutureProvider>((ref) async { // Sort conversations by updatedAt in descending order (most recent first) sortedConversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); - foundation.debugPrint( - 'DEBUG: Sorted conversations by updatedAt (most recent first)', + DebugLogger.log( + 'sort', + scope: 'conversations', + data: {'source': 'folder-sync'}, ); // Update cache timestamp @@ -618,11 +644,17 @@ final conversationsProvider = FutureProvider>((ref) async { return sortedConversations; } catch (e) { - foundation.debugPrint('DEBUG: Failed to fetch folder information: $e'); + DebugLogger.error( + 'folders-fetch-failed', + scope: 'conversations', + error: e, + ); // Sort conversations even when folder fetch fails conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); - foundation.debugPrint( - 'DEBUG: Sorted conversations by updatedAt (fallback case)', + DebugLogger.log( + 'sort', + scope: 'conversations', + data: {'source': 'fallback'}, ); // Update cache timestamp @@ -631,15 +663,17 @@ final conversationsProvider = FutureProvider>((ref) async { return conversations; // Return original conversations if folder fetch fails } } catch (e, stackTrace) { - foundation.debugPrint('DEBUG: Error fetching conversations: $e'); - foundation.debugPrint('DEBUG: Stack trace: $stackTrace'); + DebugLogger.error( + 'fetch-failed', + scope: 'conversations', + error: e, + stackTrace: stackTrace, + ); // If conversations endpoint returns 403, this should now clear auth token // and redirect user to login since it's marked as a core endpoint if (e.toString().contains('403')) { - foundation.debugPrint( - 'DEBUG: Conversations endpoint returned 403 - authentication may be invalid', - ); + DebugLogger.warning('endpoint-403', scope: 'conversations'); } // Return empty list instead of re-throwing to allow app to continue functioning @@ -671,10 +705,16 @@ final loadConversationProvider = FutureProvider.family(( throw Exception('No API service available'); } - foundation.debugPrint('DEBUG: Loading full conversation: $conversationId'); + DebugLogger.log( + 'load-start', + scope: 'conversation', + data: {'id': conversationId}, + ); final fullConversation = await api.getConversation(conversationId); - foundation.debugPrint( - 'DEBUG: Loaded conversation with ${fullConversation.messages.length} messages', + DebugLogger.log( + 'load-ok', + scope: 'conversation', + data: {'messages': fullConversation.messages.length}, ); return fullConversation; @@ -692,8 +732,10 @@ final defaultModelProvider = FutureProvider((ref) async { final isManualSelection = ref.read(isManualModelSelectionProvider); if (currentSelected != null && isManualSelection) { - foundation.debugPrint( - 'DEBUG: Manual model selected in reviewer mode: ${currentSelected.name}', + DebugLogger.log( + 'manual', + scope: 'models/default', + data: {'name': currentSelected.name}, ); return currentSelected; } @@ -704,8 +746,10 @@ final defaultModelProvider = FutureProvider((ref) async { final defaultModel = models.first; if (!ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(defaultModel); - foundation.debugPrint( - 'DEBUG: Auto-selected demo model: ${defaultModel.name}', + DebugLogger.log( + 'auto-select', + scope: 'models/default', + data: {'name': defaultModel.name}, ); } return defaultModel; @@ -751,8 +795,10 @@ final defaultModelProvider = FutureProvider((ref) async { resolved ??= models.isNotEmpty ? models.first : null; if (resolved != null && !ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(resolved); - foundation.debugPrint( - 'DEBUG: Reconciled default model to ${resolved.name}', + DebugLogger.log( + 'reconcile', + scope: 'models/default', + data: {'name': resolved.name, 'source': 'stored'}, ); } } catch (_) {} @@ -789,8 +835,10 @@ final defaultModelProvider = FutureProvider((ref) async { resolved ??= models.isNotEmpty ? models.first : null; if (resolved != null && !ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(resolved); - foundation.debugPrint( - 'DEBUG: Reconciled server default to ${resolved.name}', + DebugLogger.log( + 'reconcile', + scope: 'models/default', + data: {'name': resolved.name, 'source': 'server'}, ); } } catch (_) {} @@ -802,19 +850,21 @@ final defaultModelProvider = FutureProvider((ref) async { // 3) Fallback: fetch models and pick first available final models = await ref.read(modelsProvider.future); if (models.isEmpty) { - foundation.debugPrint('DEBUG: No models available'); + DebugLogger.warning('no-models', scope: 'models/default'); return null; } final selectedModel = models.first; if (!ref.read(isManualModelSelectionProvider)) { ref.read(selectedModelProvider.notifier).set(selectedModel); - foundation.debugPrint( - 'DEBUG: Set default model (fallback): ${selectedModel.name}', + DebugLogger.log( + 'fallback', + scope: 'models/default', + data: {'name': selectedModel.name}, ); } return selectedModel; } catch (e) { - foundation.debugPrint('DEBUG: Error setting default model: $e'); + DebugLogger.error('set-default-failed', scope: 'models/default', error: e); return null; } }); @@ -830,15 +880,15 @@ final backgroundModelLoadProvider = Provider((ref) { // Wait a bit to ensure auth is complete await Future.delayed(const Duration(milliseconds: 200)); - foundation.debugPrint('DEBUG: Starting background model loading'); + DebugLogger.log('bg-start', scope: 'models/background'); // Load default model in background try { await ref.read(defaultModelProvider.future); - foundation.debugPrint('DEBUG: Background model loading completed'); + DebugLogger.log('bg-complete', scope: 'models/background'); } catch (e) { // Ignore errors in background loading - foundation.debugPrint('DEBUG: Background model loading failed: $e'); + DebugLogger.error('bg-failed', scope: 'models/background', error: e); } }); @@ -872,11 +922,16 @@ final serverSearchProvider = FutureProvider.family, String>(( if (api == null) return []; try { - foundation.debugPrint('DEBUG: Performing server-side search for: "$query"'); + final trimmedQuery = query.trim(); + DebugLogger.log( + 'server-search', + scope: 'search', + data: {'length': trimmedQuery.length}, + ); // Use the new server-side search API final chatHits = await api.searchChats( - query: query.trim(), + query: trimmedQuery, archived: false, // Only search non-archived conversations limit: 50, sortBy: 'updated_at', @@ -888,7 +943,7 @@ final serverSearchProvider = FutureProvider.family, String>(( // Perform message-level search and merge chat hits try { final messageHits = await api.searchMessages( - query: query.trim(), + query: trimmedQuery, limit: 100, ); @@ -914,8 +969,10 @@ final serverSearchProvider = FutureProvider.family, String>(( const maxFetch = 50; final fetchList = idsToFetch.take(maxFetch).toList(); if (fetchList.isNotEmpty) { - foundation.debugPrint( - 'DEBUG: Fetching ${fetchList.length} conversations from message hits', + DebugLogger.log( + 'fetch-from-messages', + scope: 'search', + data: {'count': fetchList.length}, ); final fetched = await Future.wait( fetchList.map((id) async { @@ -939,18 +996,21 @@ final serverSearchProvider = FutureProvider.family, String>(( conversations.sort((a, b) => b.updatedAt.compareTo(a.updatedAt)); } } catch (e) { - foundation.debugPrint('DEBUG: Message-level search failed: $e'); + DebugLogger.error('message-search-failed', scope: 'search', error: e); } - foundation.debugPrint( - 'DEBUG: Server search returned ${conversations.length} results', + DebugLogger.log( + 'server-results', + scope: 'search', + data: {'count': conversations.length}, ); return conversations; } catch (e) { - foundation.debugPrint('DEBUG: Server search failed, fallback to local: $e'); + DebugLogger.error('server-search-failed', scope: 'search', error: e); // Fallback to local search if server search fails final allConversations = await ref.read(conversationsProvider.future); + DebugLogger.log('fallback-local', scope: 'search'); return allConversations.where((conv) { return !conv.archived && (conv.title.toLowerCase().contains(query.toLowerCase()) || @@ -1098,7 +1158,7 @@ final userSettingsProvider = FutureProvider((ref) async { final settingsData = await api.getUserSettings(); return UserSettings.fromJson(settingsData); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching user settings: $e'); + DebugLogger.error('user-settings-failed', scope: 'settings', error: e); // Return default settings on error return const UserSettings(); } @@ -1114,7 +1174,7 @@ final conversationSuggestionsProvider = FutureProvider>(( try { return await api.getSuggestions(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching suggestions: $e'); + DebugLogger.error('suggestions-failed', scope: 'suggestions', error: e); return []; } }); @@ -1129,7 +1189,7 @@ final userPermissionsProvider = FutureProvider>(( try { return await api.getUserPermissions(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching user permissions: $e'); + DebugLogger.error('permissions-failed', scope: 'permissions', error: e); return {}; } }); @@ -1170,21 +1230,23 @@ final webSearchAvailableProvider = Provider((ref) { final foldersProvider = FutureProvider>((ref) async { final api = ref.watch(apiServiceProvider); if (api == null) { - foundation.debugPrint('DEBUG: No API service available for folders'); + DebugLogger.warning('api-missing', scope: 'folders'); return []; } try { - foundation.debugPrint('DEBUG: Fetching folders from API...'); final foldersData = await api.getFolders(); - foundation.debugPrint('DEBUG: Raw folders data received successfully'); final folders = foldersData .map((folderData) => Folder.fromJson(folderData)) .toList(); - foundation.debugPrint('DEBUG: Parsed ${folders.length} folders'); + DebugLogger.log( + 'fetch-ok', + scope: 'folders', + data: {'count': folders.length}, + ); return folders; } catch (e) { - foundation.debugPrint('DEBUG: Error fetching folders: $e'); + DebugLogger.error('fetch-failed', scope: 'folders', error: e); return []; } }); @@ -1198,7 +1260,7 @@ final userFilesProvider = FutureProvider>((ref) async { final filesData = await api.getUserFiles(); return filesData.map((fileData) => FileInfo.fromJson(fileData)).toList(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching files: $e'); + DebugLogger.error('files-failed', scope: 'files', error: e); return []; } }); @@ -1214,7 +1276,12 @@ final fileContentProvider = FutureProvider.family(( try { return await api.getFileContent(fileId); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching file content: $e'); + DebugLogger.error( + 'file-content-failed', + scope: 'files', + error: e, + data: {'fileId': fileId}, + ); throw Exception('Failed to load file content: $e'); } }); @@ -1228,7 +1295,7 @@ final knowledgeBasesProvider = FutureProvider>((ref) async { final kbData = await api.getKnowledgeBases(); return kbData.map((data) => KnowledgeBase.fromJson(data)).toList(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching knowledge bases: $e'); + DebugLogger.error('knowledge-bases-failed', scope: 'knowledge', error: e); return []; } }); @@ -1244,7 +1311,11 @@ final knowledgeBaseItemsProvider = .map((data) => KnowledgeBaseItem.fromJson(data)) .toList(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching knowledge base items: $e'); + DebugLogger.error( + 'knowledge-items-failed', + scope: 'knowledge', + error: e, + ); return []; } }); @@ -1257,7 +1328,7 @@ final availableVoicesProvider = FutureProvider>((ref) async { try { return await api.getAvailableVoices(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching voices: $e'); + DebugLogger.error('voices-failed', scope: 'voices', error: e); return []; } }); @@ -1272,7 +1343,7 @@ final imageModelsProvider = FutureProvider>>(( try { return await api.getImageModels(); } catch (e) { - foundation.debugPrint('DEBUG: Error fetching image models: $e'); + DebugLogger.error('image-models-failed', scope: 'image-models', error: e); return []; } }); diff --git a/lib/core/providers/app_startup_providers.dart b/lib/core/providers/app_startup_providers.dart index b6fd26f..9cd7328 100644 --- a/lib/core/providers/app_startup_providers.dart +++ b/lib/core/providers/app_startup_providers.dart @@ -210,7 +210,9 @@ final appStartupFlowProvider = Provider((ref) { await ref.read(defaultModelProvider.future); } catch (e) { DebugLogger.warning( - 'StartupFlow: default model preload failed: $e', + 'model-preload-failed', + scope: 'startup', + data: {'error': e}, ); } }); @@ -221,7 +223,7 @@ final appStartupFlowProvider = Provider((ref) { // Show onboarding once when user reaches chat and hasn't seen it yet await _maybeShowOnboarding(ref); } catch (e) { - DebugLogger.error('StartupFlow error', e); + DebugLogger.error('startup-flow-failed', scope: 'startup', error: e); } }); } else { diff --git a/lib/core/services/api_service.dart b/lib/core/services/api_service.dart index a72dca8..724d3c9 100644 --- a/lib/core/services/api_service.dart +++ b/lib/core/services/api_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'package:dio/dio.dart'; // import 'package:http_parser/http_parser.dart'; // Removed legacy websocket/socket.io imports @@ -17,6 +17,17 @@ import '../error/api_error_interceptor.dart'; import 'persistent_streaming_service.dart'; import '../utils/debug_logger.dart'; +const bool _enableLegacyApiLogs = false; +const bool _traceConversationParsing = false; +const bool _traceFullChatParsing = false; + +void debugPrint(String? message, {int? wrapWidth}) { + if (!_enableLegacyApiLogs || message == null) { + return; + } + DebugLogger.fromLegacy(message, scope: 'api/legacy'); +} + class ApiService { final Dio _dio; final ServerConfig serverConfig; @@ -187,7 +198,7 @@ class ApiService { // User info Future getCurrentUser() async { final response = await _dio.get('/api/v1/auths/'); - DebugLogger.log('User info retrieved successfully'); + DebugLogger.log('user-info', scope: 'api/user'); return User.fromJson(response.data); } @@ -204,11 +215,15 @@ class ApiService { // Response is a direct array models = response.data as List; } else { - DebugLogger.error('Unexpected models response format'); + DebugLogger.error('models-format', scope: 'api/models'); return []; } - DebugLogger.log('Found ${models.length} models'); + DebugLogger.log( + 'models-count', + scope: 'api/models', + data: {'count': models.length}, + ); return models.map((m) => Model.fromJson(m)).toList(); } @@ -217,12 +232,14 @@ class ApiService { try { final response = await _dio.get('/api/v1/users/user/settings'); - DebugLogger.log('User settings retrieved successfully'); + DebugLogger.log('settings-ok', scope: 'api/user-settings'); final data = response.data; if (data is! Map) { DebugLogger.warning( - 'User settings response is empty or unexpected type: ${data.runtimeType}', + 'settings-format', + scope: 'api/user-settings', + data: {'type': data.runtimeType}, ); return null; } @@ -235,16 +252,22 @@ class ApiService { // Return the first model in the user's preferred models list final defaultModel = models.first.toString(); DebugLogger.log( - 'Found default model from user settings: $defaultModel', + 'default-model', + scope: 'api/user-settings', + data: {'id': defaultModel}, ); return defaultModel; } } - DebugLogger.log('No default model found in user settings'); + DebugLogger.warning('default-model-missing', scope: 'api/user-settings'); return null; } catch (e) { - DebugLogger.error('Error fetching default model from user settings', e); + DebugLogger.error( + 'default-model-error', + scope: 'api/user-settings', + error: e, + ); // Do not call admin-only configs endpoint here; let the caller // handle fallback (e.g., first available model from /api/models). return null; @@ -327,9 +350,15 @@ class ApiService { ); final regularChatList = allRegularChats; - debugPrint('DEBUG: Found ${regularChatList.length} regular chats'); - debugPrint('DEBUG: Found ${pinnedChatList.length} pinned chats'); - debugPrint('DEBUG: Found ${archivedChatList.length} archived chats'); + DebugLogger.log( + 'summary', + scope: 'api/conversations', + data: { + 'regular': regularChatList.length, + 'pinned': pinnedChatList.length, + 'archived': archivedChatList.length, + }, + ); // Convert OpenWebUI chat format to our Conversation format final conversations = []; @@ -345,7 +374,12 @@ class ApiService { conversations.add(pinnedConversation); pinnedIds.add(conversation.id); } catch (e) { - debugPrint('DEBUG: Error parsing pinned chat ${chatData['id']}: $e'); + DebugLogger.error( + 'parse-pinned-failed', + scope: 'api/conversations', + error: e, + data: {'conversationId': chatData['id']}, + ); } } @@ -358,7 +392,12 @@ class ApiService { conversations.add(archivedConversation); archivedIds.add(conversation.id); } catch (e) { - debugPrint('DEBUG: Error parsing archived chat ${chatData['id']}: $e'); + DebugLogger.error( + 'parse-archived-failed', + scope: 'api/conversations', + error: e, + data: {'conversationId': chatData['id']}, + ); } } @@ -369,21 +408,28 @@ class ApiService { // Debug: Check if conversation has folder_id in raw data if (chatData.containsKey('folder_id') && chatData['folder_id'] != null) { - debugPrint( - '🔍 DEBUG: Found conversation with folder_id in raw data: ${chatData['id']} -> ${chatData['folder_id']}', + DebugLogger.log( + 'folder-ref', + scope: 'api/conversations', + data: { + 'conversationId': chatData['id'], + 'folderId': chatData['folder_id'], + }, ); } - if (!loggedSampleChat) { + if (!loggedSampleChat && _traceConversationParsing) { loggedSampleChat = true; - debugPrint( - '🔍 DEBUG: Sample chat data fields: ${chatData.keys.toList()}', + DebugLogger.log( + 'sample-keys', + scope: 'api/conversations', + data: {'keys': chatData.keys.take(6).toList()}, + ); + DebugLogger.log( + 'sample-data', + scope: 'api/conversations', + data: {'preview': chatData.toString()}, ); - final samplePreviewSource = chatData.toString(); - final preview = samplePreviewSource.length > 200 - ? samplePreviewSource.substring(0, 200) - : samplePreviewSource; - debugPrint('🔍 DEBUG: Sample chat data: $preview...'); } final conversation = _parseOpenWebUIChat(chatData); @@ -393,13 +439,24 @@ class ApiService { conversations.add(conversation); } } catch (e) { - debugPrint('DEBUG: Error parsing chat ${chatData['id']}: $e'); + DebugLogger.error( + 'parse-regular-failed', + scope: 'api/conversations', + error: e, + data: {'conversationId': chatData['id']}, + ); // Continue with other chats even if one fails } } - debugPrint( - 'DEBUG: Successfully parsed ${conversations.length} conversations (${pinnedIds.length} pinned, ${archivedIds.length} archived)', + DebugLogger.log( + 'parse-complete', + scope: 'api/conversations', + data: { + 'total': conversations.length, + 'pinned': pinnedIds.length, + 'archived': archivedIds.length, + }, ); return conversations; } @@ -408,21 +465,30 @@ class ApiService { String path, { required String debugLabel, }) async { + final scope = 'api/collection/${debugLabel.replaceAll(' ', '-')}'; try { final response = await _dio.get(path); - DebugLogger.log('$debugLabel response status: ${response.statusCode}'); + DebugLogger.log( + 'status', + scope: scope, + data: {'code': response.statusCode}, + ); if (response.data is List) { return (response.data as List).cast(); } DebugLogger.warning( - 'Expected array for $debugLabel, got ${response.data.runtimeType}', + 'unexpected-type', + scope: scope, + data: {'type': response.data.runtimeType}, ); } on DioException catch (e) { DebugLogger.warning( - 'Skipping $debugLabel due to network error: ${e.message}', + 'network-skip', + scope: scope, + data: {'message': e.message}, ); } catch (e) { - DebugLogger.warning('Skipping $debugLabel due to error: $e'); + DebugLogger.warning('error-skip', scope: scope, data: {'error': e}); } return []; } @@ -484,14 +550,22 @@ class ApiService { final folderId = chatData['folder_id'] as String?; // Debug logging for folder assignment - if (folderId != null) { + if (_traceConversationParsing && folderId != null) { final idPreview = id.length > 8 ? id.substring(0, 8) : id; - debugPrint('🔍 DEBUG: Conversation $idPreview has folderId: $folderId'); + DebugLogger.log( + 'folder-ref', + scope: 'api/conversations', + data: {'conversationId': idPreview, 'folderId': folderId}, + ); } - debugPrint( - 'DEBUG: Parsed conversation $id: pinned=$pinned, archived=$archived', - ); + if (_traceConversationParsing) { + DebugLogger.log( + 'parsed', + scope: 'api/conversations', + data: {'id': id, 'pinned': pinned, 'archived': archived}, + ); + } String? systemPrompt; final chatObject = chatData['chat'] as Map?; @@ -522,10 +596,10 @@ class ApiService { } Future getConversation(String id) async { - DebugLogger.log('Fetching individual chat: $id'); + DebugLogger.log('fetch', scope: 'api/chat', data: {'id': id}); final response = await _dio.get('/api/v1/chats/$id'); - DebugLogger.log('Chat response received successfully'); + DebugLogger.log('fetch-ok', scope: 'api/chat'); // Parse OpenWebUI ChatResponse format final chatData = response.data as Map; @@ -534,14 +608,24 @@ class ApiService { // Parse full OpenWebUI chat with messages Conversation _parseFullOpenWebUIChat(Map chatData) { - debugPrint('DEBUG: Parsing full OpenWebUI chat data'); - debugPrint('DEBUG: Chat data keys: ${chatData.keys.toList()}'); + if (_traceFullChatParsing) { + DebugLogger.log( + 'parse-full', + scope: 'api/chat', + data: {'keys': chatData.keys.take(8).toList()}, + ); + } final id = chatData['id'] as String; final title = chatData['title'] as String; - debugPrint('DEBUG: Parsed chat ID: $id'); - debugPrint('DEBUG: Parsed chat title: $title'); + if (_traceFullChatParsing) { + DebugLogger.log( + 'chat-meta', + scope: 'api/chat', + data: {'id': id, 'title': title}, + ); + } // Safely parse timestamps with validation final updatedAt = _parseTimestamp(chatData['updated_at']); @@ -573,7 +657,13 @@ class ApiService { final models = chatObject['models'] as List?; if (models != null && models.isNotEmpty) { model = models.first as String; - debugPrint('DEBUG: Extracted model from chat.models: $model'); + if (_traceFullChatParsing) { + DebugLogger.log( + 'model', + scope: 'api/chat', + data: {'id': id, 'model': model}, + ); + } } } @@ -591,24 +681,40 @@ class ApiService { final currentId = history['currentId']?.toString(); if (currentId != null && currentId.isNotEmpty) { messagesList = _buildMessagesListFromHistory(history); - debugPrint( - 'DEBUG: Built ${messagesList.length} messages from history chain to currentId=$currentId', - ); + if (_traceFullChatParsing) { + DebugLogger.log( + 'history-chain', + scope: 'api/chat', + data: { + 'id': id, + 'count': messagesList.length, + 'currentId': currentId, + }, + ); + } } } // Fallback to chat.messages (list format) if history is missing or empty if (((messagesList?.isEmpty ?? true)) && chatObject['messages'] != null) { messagesList = chatObject['messages'] as List; - debugPrint( - 'DEBUG: Found ${messagesList.length} messages in chat.messages (fallback)', - ); + if (_traceFullChatParsing) { + DebugLogger.log( + 'messages-fallback', + scope: 'api/chat', + data: {'id': id, 'count': messagesList.length}, + ); + } } } else if (chatData['messages'] != null) { messagesList = chatData['messages'] as List; - debugPrint( - 'DEBUG: Found ${messagesList.length} messages in top-level messages', - ); + if (_traceFullChatParsing) { + DebugLogger.log( + 'messages-top-level', + scope: 'api/chat', + data: {'id': id, 'count': messagesList.length}, + ); + } } // Parse messages from list format only (avoiding duplication) @@ -616,9 +722,18 @@ class ApiService { for (int idx = 0; idx < messagesList.length; idx++) { final msgData = messagesList[idx] as Map; try { - debugPrint( - 'DEBUG: Parsing message: ${msgData['id']} - role: ${msgData['role']} - content length: ${msgData['content']?.toString().length ?? 0}', - ); + if (_traceFullChatParsing) { + DebugLogger.log( + 'message-parse', + scope: 'api/chat', + data: { + 'chatId': id, + 'messageId': msgData['id'], + 'role': msgData['role'], + 'contentLen': msgData['content']?.toString().length ?? 0, + }, + ); + } // If this assistant message includes tool_calls, merge following tool results final historyMsg = historyMessagesMap != null @@ -667,9 +782,13 @@ class ApiService { // Skip the tool messages we just merged idx = j - 1; - debugPrint( - 'DEBUG: Successfully parsed tool_call assistant turn: ${message.id}', - ); + if (_traceFullChatParsing) { + DebugLogger.log( + 'message-tool-call', + scope: 'api/chat', + data: {'chatId': id, 'messageId': message.id}, + ); + } continue; } @@ -679,16 +798,35 @@ class ApiService { historyMsg: historyMsg, ); messages.add(message); - debugPrint( - 'DEBUG: Successfully parsed message: ${message.id} - ${message.role}', - ); + if (_traceFullChatParsing) { + DebugLogger.log( + 'message', + scope: 'api/chat', + data: { + 'chatId': id, + 'messageId': message.id, + 'role': message.role, + }, + ); + } } catch (e) { - debugPrint('DEBUG: Error parsing message: $e'); + DebugLogger.error( + 'message-parse-failed', + scope: 'api/chat', + error: e, + data: {'chatId': id, 'messageId': msgData['id']}, + ); } } } - debugPrint('DEBUG: Total parsed messages: ${messages.length}'); + if (_traceFullChatParsing) { + DebugLogger.log( + 'message-count', + scope: 'api/chat', + data: {'chatId': id, 'count': messages.length}, + ); + } return Conversation( id: id, @@ -1188,9 +1326,11 @@ class ApiService { final response = await _dio.post('/api/v1/chats/new', data: chatData); DebugLogger.log( - 'Create conversation response status: ${response.statusCode}', + 'create-status', + scope: 'api/conversation', + data: {'code': response.statusCode}, ); - DebugLogger.log('Create conversation response received successfully'); + DebugLogger.log('create-ok', scope: 'api/conversation'); // Parse the response final responseData = response.data as Map; @@ -1290,7 +1430,7 @@ class ApiService { // OpenWebUI uses POST not PUT for updating chats await _dio.post('/api/v1/chats/$conversationId', data: chatData); - DebugLogger.log('Sync conversation response received successfully'); + DebugLogger.log('sync-ok', scope: 'api/conversation'); } Future updateConversation( @@ -1381,21 +1521,28 @@ class ApiService { // Folders Future>> getFolders() async { try { - debugPrint('DEBUG: Fetching folders from /api/v1/folders/'); final response = await _dio.get('/api/v1/folders/'); - DebugLogger.log('Folders response status: ${response.statusCode}'); - DebugLogger.log('Folders response received successfully'); + DebugLogger.log( + 'fetch-status', + scope: 'api/folders', + data: {'code': response.statusCode}, + ); + DebugLogger.log('fetch-ok', scope: 'api/folders'); final data = response.data; if (data is List) { debugPrint('DEBUG: Found ${data.length} folders'); return data.cast>(); } else { - DebugLogger.log('Response data is not a list: ${data.runtimeType}'); + DebugLogger.warning( + 'unexpected-type', + scope: 'api/folders', + data: {'type': data.runtimeType}, + ); return []; } } catch (e) { - debugPrint('DEBUG: Error in getFolders: $e'); + DebugLogger.error('fetch-failed', scope: 'api/folders', error: e); rethrow; } } @@ -1783,17 +1930,23 @@ class ApiService { data: {'queries': queries}, ); - DebugLogger.log('Web search response status: ${response.statusCode}'); - DebugLogger.log('Web search response type: ${response.data.runtimeType}'); - DebugLogger.log('Web search response received successfully'); + DebugLogger.log( + 'status', + scope: 'api/web-search', + data: {'code': response.statusCode}, + ); + DebugLogger.log( + 'response-type', + scope: 'api/web-search', + data: {'type': response.data.runtimeType}, + ); + DebugLogger.log('fetch-ok', scope: 'api/web-search'); return response.data as Map; } catch (e) { debugPrint('DEBUG: Web search API error: $e'); if (e is DioException) { - DebugLogger.error( - 'Web search error response available (truncated for security)', - ); + DebugLogger.error('error-response', scope: 'api/web-search', error: e); debugPrint('DEBUG: Web search error status: ${e.response?.statusCode}'); } rethrow; @@ -1810,7 +1963,7 @@ class ApiService { if (response.statusCode == 200 && response.data != null) { final modelData = response.data as Map; - DebugLogger.log('Model details for $modelId retrieved successfully'); + DebugLogger.log('details', scope: 'api/models', data: {'id': modelId}); return modelData; } } catch (e) { @@ -1911,7 +2064,11 @@ class ApiService { debugPrint( 'DEBUG: Collection query response type: ${response.data.runtimeType}', ); - DebugLogger.log('Collection query response received successfully'); + DebugLogger.log( + 'query-ok', + scope: 'api/collection', + data: {'name': collectionName}, + ); if (response.data is List) { return response.data as List; @@ -1951,7 +2108,7 @@ class ApiService { debugPrint( 'DEBUG: Retrieval config response status: ${response.statusCode}', ); - DebugLogger.log('Retrieval config response received successfully'); + DebugLogger.log('config-ok', scope: 'api/retrieval'); return response.data as Map; } catch (e) { @@ -2036,8 +2193,10 @@ class ApiService { } on DioException catch (e) { debugPrint('DEBUG: images/generations failed: ${e.response?.statusCode}'); DebugLogger.error( - 'Image generation request to /api/v1/images/generations failed', - e, + 'images-generate-failed', + scope: 'api/images', + error: e, + data: {'status': e.response?.statusCode}, ); // Do not attempt singular fallback here - surface the original error rethrow; @@ -3325,8 +3484,12 @@ class ApiService { debugPrint('DEBUG: Uploading to /api/v1/files/'); final response = await _dio.post('/api/v1/files/', data: formData); - DebugLogger.log('Upload response status: ${response.statusCode}'); - DebugLogger.log('Upload response received successfully'); + DebugLogger.log( + 'upload-status', + scope: 'api/files', + data: {'code': response.statusCode}, + ); + DebugLogger.log('upload-ok', scope: 'api/files'); if (response.data is Map && response.data['id'] != null) { final fileId = response.data['id'] as String; @@ -3336,7 +3499,7 @@ class ApiService { throw Exception('Invalid response format: missing file ID'); } } catch (e) { - debugPrint('ERROR: File upload failed: $e'); + DebugLogger.error('upload-failed', scope: 'api/files', error: e); rethrow; } } @@ -3370,17 +3533,35 @@ class ApiService { debugPrint('Testing endpoint: $endpoint'); final response = await _dio.get(endpoint); debugPrint('✅ $endpoint - Status: ${response.statusCode}'); - DebugLogger.log(' Response type: ${response.data.runtimeType}'); + DebugLogger.log( + 'response-type', + scope: 'api/diagnostics', + data: {'endpoint': endpoint, 'type': response.data.runtimeType}, + ); if (response.data is List) { - DebugLogger.log(' Array length: ${(response.data as List).length}'); + DebugLogger.log( + 'array-length', + scope: 'api/diagnostics', + data: { + 'endpoint': endpoint, + 'count': (response.data as List).length, + }, + ); } else if (response.data is Map) { - DebugLogger.log(' Object keys: ${(response.data as Map).keys}'); + DebugLogger.log( + 'object-keys', + scope: 'api/diagnostics', + data: { + 'endpoint': endpoint, + 'keys': (response.data as Map).keys.take(5).toList(), + }, + ); } - final dataSampleSource = response.data.toString(); - final dataPreview = dataSampleSource.length > 200 - ? dataSampleSource.substring(0, 200) - : dataSampleSource; - DebugLogger.log(' Sample data: $dataPreview...'); + DebugLogger.log( + 'sample', + scope: 'api/diagnostics', + data: {'endpoint': endpoint, 'preview': response.data.toString()}, + ); } catch (e) { debugPrint('❌ $endpoint - Error: $e'); } diff --git a/lib/core/services/attachment_upload_queue.dart b/lib/core/services/attachment_upload_queue.dart index 47b98e7..23a5c4b 100644 --- a/lib/core/services/attachment_upload_queue.dart +++ b/lib/core/services/attachment_upload_queue.dart @@ -2,8 +2,13 @@ import 'dart:async'; import 'dart:convert'; import 'dart:math'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:shared_preferences/shared_preferences.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'attachments/queue'); +} /// Status of a queued attachment upload enum QueuedAttachmentStatus { pending, uploading, completed, failed, cancelled } diff --git a/lib/core/services/background_streaming_handler.dart b/lib/core/services/background_streaming_handler.dart index 3abf022..296e23f 100644 --- a/lib/core/services/background_streaming_handler.dart +++ b/lib/core/services/background_streaming_handler.dart @@ -42,7 +42,9 @@ class BackgroundStreamingHandler { final String reason = args['reason'] as String; DebugLogger.stream( - 'Background: Streams suspending - $streamIds (reason: $reason)', + 'suspending', + scope: 'background', + data: {'count': streamIds.length, 'reason': reason}, ); onStreamsSuspending?.call(streamIds); @@ -51,7 +53,7 @@ class BackgroundStreamingHandler { break; case 'backgroundTaskExpiring': - DebugLogger.stream('Background: Background task expiring'); + DebugLogger.stream('task-expiring', scope: 'background'); onBackgroundTaskExpiring?.call(); break; } @@ -70,10 +72,17 @@ class BackgroundStreamingHandler { }); DebugLogger.stream( - 'Background: Started background execution for ${streamIds.length} streams', + 'start', + scope: 'background', + data: {'count': streamIds.length}, ); } catch (e) { - DebugLogger.error('Background: Failed to start background execution', e); + DebugLogger.error( + 'start-failed', + scope: 'background', + error: e, + data: {'count': streamIds.length}, + ); } } @@ -90,10 +99,17 @@ class BackgroundStreamingHandler { }); DebugLogger.stream( - 'Background: Stopped background execution for ${streamIds.length} streams', + 'stop', + scope: 'background', + data: {'count': streamIds.length}, ); } catch (e) { - DebugLogger.error('Background: Failed to stop background execution', e); + DebugLogger.error( + 'stop-failed', + scope: 'background', + error: e, + data: {'count': streamIds.length}, + ); } } @@ -156,7 +172,7 @@ class BackgroundStreamingHandler { try { await _channel.invokeMethod('keepAlive'); } catch (e) { - DebugLogger.error('Background: Failed to keep alive', e); + DebugLogger.error('keepalive-failed', scope: 'background', error: e); } } @@ -181,11 +197,13 @@ class BackgroundStreamingHandler { } DebugLogger.stream( - 'Background: Recovered ${recovered.length} stream states', + 'recovered', + scope: 'background', + data: {'count': recovered.length}, ); return recovered; } catch (e) { - DebugLogger.error('Background: Failed to recover stream states', e); + DebugLogger.error('recover-failed', scope: 'background', error: e); return []; } } @@ -207,7 +225,12 @@ class BackgroundStreamingHandler { 'reason': reason, }); } catch (e) { - DebugLogger.error('Background: Failed to save stream states', e); + DebugLogger.error( + 'save-states-failed', + scope: 'background', + error: e, + data: {'count': streamIds.length, 'reason': reason}, + ); } } @@ -290,7 +313,7 @@ class StreamState { ), ); } catch (e) { - DebugLogger.error('Failed to parse StreamState from map', e); + DebugLogger.error('parse-failed', scope: 'background', error: e); return null; } } diff --git a/lib/core/services/optimized_storage_service.dart b/lib/core/services/optimized_storage_service.dart index 415a0aa..5871d31 100644 --- a/lib/core/services/optimized_storage_service.dart +++ b/lib/core/services/optimized_storage_service.dart @@ -1,10 +1,15 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'secure_credential_storage.dart'; import '../models/server_config.dart'; import '../models/conversation.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'storage/optimized'); +} /// Optimized storage service with single secure storage implementation /// Eliminates dual storage overhead and improves performance diff --git a/lib/core/services/persistent_streaming_service.dart b/lib/core/services/persistent_streaming_service.dart index ee32261..5d6545a 100644 --- a/lib/core/services/persistent_streaming_service.dart +++ b/lib/core/services/persistent_streaming_service.dart @@ -312,8 +312,10 @@ class PersistentStreamingService with WidgetsBindingObserver { _retryAttempts.remove(streamId); } catch (e) { DebugLogger.error( - 'PersistentStreaming: Failed to recover stream $streamId', - e, + 'recover-failed', + scope: 'streaming/persistent', + error: e, + data: {'streamId': streamId}, ); // Schedule next retry if under limit @@ -405,11 +407,12 @@ class PersistentStreamingService with WidgetsBindingObserver { void _enableWakeLock() async { try { await WakelockPlus.enable(); - DebugLogger.stream('PersistentStreamingService: Wake lock enabled'); + DebugLogger.stream('wake-lock-enabled', scope: 'streaming/persistent'); } catch (e) { DebugLogger.error( - 'PersistentStreamingService: Failed to enable wake lock', - e, + 'wake-lock-enable-failed', + scope: 'streaming/persistent', + error: e, ); } } @@ -417,11 +420,12 @@ class PersistentStreamingService with WidgetsBindingObserver { void _disableWakeLock() async { try { await WakelockPlus.disable(); - DebugLogger.stream('PersistentStreamingService: Wake lock disabled'); + DebugLogger.stream('wake-lock-disabled', scope: 'streaming/persistent'); } catch (e) { DebugLogger.error( - 'PersistentStreamingService: Failed to disable wake lock', - e, + 'wake-lock-disable-failed', + scope: 'streaming/persistent', + error: e, ); } } diff --git a/lib/core/services/secure_credential_storage.dart b/lib/core/services/secure_credential_storage.dart index 54ed77f..35ad173 100644 --- a/lib/core/services/secure_credential_storage.dart +++ b/lib/core/services/secure_credential_storage.dart @@ -73,9 +73,13 @@ class SecureCredentialStorage { ); } - DebugLogger.storage('Credentials saved and verified securely'); + DebugLogger.storage( + 'save-ok', + scope: 'credentials', + data: {'version': '2.0'}, + ); } catch (e) { - DebugLogger.error('Failed to save credentials', e); + DebugLogger.error('save-failed', scope: 'credentials', error: e); rethrow; } } @@ -92,7 +96,7 @@ class SecureCredentialStorage { final decoded = jsonDecode(jsonString); if (decoded is! Map) { - DebugLogger.warning('Invalid credentials format'); + DebugLogger.warning('invalid-format', scope: 'credentials'); await deleteSavedCredentials(); return null; } @@ -104,7 +108,9 @@ class SecureCredentialStorage { if (savedDeviceId != currentDeviceId) { DebugLogger.info( - 'Device fingerprint changed, but allowing credential access for better UX', + 'fingerprint-mismatch-allowed', + scope: 'credentials', + data: {'previous': savedDeviceId, 'current': currentDeviceId}, ); // Don't clear credentials immediately - allow the user to continue // They can re-login if needed, which will update the fingerprint @@ -115,9 +121,7 @@ class SecureCredentialStorage { if (!decoded.containsKey('serverId') || !decoded.containsKey('username') || !decoded.containsKey('password')) { - DebugLogger.warning( - 'Invalid saved credentials format - missing required fields', - ); + DebugLogger.warning('missing-fields', scope: 'credentials'); await deleteSavedCredentials(); return null; } @@ -133,11 +137,17 @@ class SecureCredentialStorage { // Warn if credentials are very old (but don't delete them) if (daysSinceCreated > 90) { DebugLogger.info( - 'Saved credentials are $daysSinceCreated days old', + 'credentials-old', + scope: 'credentials', + data: {'ageDays': daysSinceCreated}, ); } } catch (e) { - DebugLogger.warning('Could not parse savedAt timestamp: $e'); + DebugLogger.warning( + 'savedat-parse-failed', + scope: 'credentials', + data: {'raw': savedAt, 'error': e.toString()}, + ); } } @@ -148,7 +158,7 @@ class SecureCredentialStorage { 'savedAt': decoded['savedAt']?.toString() ?? '', }; } catch (e) { - DebugLogger.error('Failed to retrieve credentials', e); + DebugLogger.error('read-failed', scope: 'credentials', error: e); // Don't delete credentials on retrieval errors - they might be recoverable return null; } @@ -158,9 +168,9 @@ class SecureCredentialStorage { Future deleteSavedCredentials() async { try { await _secureStorage.delete(key: _credentialsKey); - DebugLogger.storage('Credentials deleted'); + DebugLogger.storage('delete-ok', scope: 'credentials'); } catch (e) { - DebugLogger.error('Failed to delete credentials', e); + DebugLogger.error('delete-failed', scope: 'credentials', error: e); } } @@ -170,7 +180,11 @@ class SecureCredentialStorage { final encryptedToken = await _encryptData(token); await _secureStorage.write(key: _authTokenKey, value: encryptedToken); } catch (e) { - DebugLogger.error('Failed to save auth token', e); + DebugLogger.error( + 'save-token-failed', + scope: 'credentials/token', + error: e, + ); rethrow; } } @@ -183,7 +197,11 @@ class SecureCredentialStorage { return await _decryptData(encryptedToken); } catch (e) { - DebugLogger.error('Failed to retrieve auth token', e); + DebugLogger.error( + 'read-token-failed', + scope: 'credentials/token', + error: e, + ); return null; } } @@ -193,7 +211,11 @@ class SecureCredentialStorage { try { await _secureStorage.delete(key: _authTokenKey); } catch (e) { - DebugLogger.error('Failed to delete auth token', e); + DebugLogger.error( + 'delete-token-failed', + scope: 'credentials/token', + error: e, + ); } } @@ -206,7 +228,11 @@ class SecureCredentialStorage { value: encryptedConfigs, ); } catch (e) { - DebugLogger.error('Failed to save server configs', e); + DebugLogger.error( + 'save-configs-failed', + scope: 'credentials/server-configs', + error: e, + ); rethrow; } } @@ -221,7 +247,11 @@ class SecureCredentialStorage { return await _decryptData(encryptedConfigs); } catch (e) { - DebugLogger.error('Failed to retrieve server configs', e); + DebugLogger.error( + 'read-configs-failed', + scope: 'credentials/server-configs', + error: e, + ); return null; } } @@ -239,7 +269,11 @@ class SecureCredentialStorage { return result == testValue; } catch (e) { - DebugLogger.warning('Secure storage not available: $e'); + DebugLogger.warning( + 'storage-unavailable', + scope: 'credentials/health', + data: {'error': e.toString()}, + ); return false; } } @@ -248,9 +282,9 @@ class SecureCredentialStorage { Future clearAll() async { try { await _secureStorage.deleteAll(); - DebugLogger.storage('All secure data cleared'); + DebugLogger.storage('clear-ok', scope: 'credentials'); } catch (e) { - DebugLogger.error('Failed to clear secure data', e); + DebugLogger.error('clear-failed', scope: 'credentials', error: e); } } @@ -261,7 +295,11 @@ class SecureCredentialStorage { // In a more advanced implementation, you could add an additional layer of AES encryption return data; } catch (e) { - DebugLogger.error('Failed to encrypt data', e); + DebugLogger.error( + 'encrypt-failed', + scope: 'credentials/crypto', + error: e, + ); rethrow; } } @@ -273,7 +311,11 @@ class SecureCredentialStorage { // This matches the encryption method above return encryptedData; } catch (e) { - DebugLogger.error('Failed to decrypt data', e); + DebugLogger.error( + 'decrypt-failed', + scope: 'credentials/crypto', + error: e, + ); rethrow; } } @@ -297,7 +339,11 @@ class SecureCredentialStorage { return digest.toString(); } catch (e) { - DebugLogger.warning('Failed to generate device fingerprint: $e'); + DebugLogger.warning( + 'fingerprint-failed', + scope: 'credentials', + data: {'error': e.toString()}, + ); // Return a consistent fallback fingerprint return 'stable_fallback_device_id'; } @@ -315,11 +361,9 @@ class SecureCredentialStorage { username: oldCredentials['username'] ?? '', password: oldCredentials['password'] ?? '', ); - DebugLogger.storage( - 'Successfully migrated credentials to new secure format', - ); + DebugLogger.storage('migrate-ok', scope: 'credentials'); } catch (e) { - DebugLogger.error('Failed to migrate credentials', e); + DebugLogger.error('migrate-failed', scope: 'credentials', error: e); } } } diff --git a/lib/core/services/share_receiver_service.dart b/lib/core/services/share_receiver_service.dart index d29ea86..4cdddaf 100644 --- a/lib/core/services/share_receiver_service.dart +++ b/lib/core/services/share_receiver_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:share_handler/share_handler.dart' as sh; @@ -12,6 +12,7 @@ import '../../core/providers/app_providers.dart'; import '../../shared/services/tasks/task_queue.dart'; import 'package:path/path.dart' as path; import 'navigation_service.dart'; +import '../utils/debug_logger.dart'; // Server chat creation/title generation occur on first send via chat providers /// Lightweight payload for a share event @@ -199,3 +200,8 @@ Future _processPayload(Ref ref, SharedPayload payload) async { debugPrint('ShareReceiver: failed to process payload: $e'); } } + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'share'); +} diff --git a/lib/core/services/socket_service.dart b/lib/core/services/socket_service.dart index 77b5ede..1edfab4 100644 --- a/lib/core/services/socket_service.dart +++ b/lib/core/services/socket_service.dart @@ -1,6 +1,11 @@ import 'package:socket_io_client/socket_io_client.dart' as io; -import 'package:flutter/foundation.dart'; import '../models/server_config.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'socket'); +} class SocketService { final ServerConfig serverConfig; diff --git a/lib/core/services/storage_service.dart b/lib/core/services/storage_service.dart index 954f800..e33857b 100644 --- a/lib/core/services/storage_service.dart +++ b/lib/core/services/storage_service.dart @@ -1,10 +1,15 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/server_config.dart'; import '../models/conversation.dart'; import 'secure_credential_storage.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'storage'); +} class StorageService { final FlutterSecureStorage _secureStorage; diff --git a/lib/core/services/streaming_helper.dart b/lib/core/services/streaming_helper.dart index 2e2db98..f432a95 100644 --- a/lib/core/services/streaming_helper.dart +++ b/lib/core/services/streaming_helper.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide debugPrint; import '../../core/models/chat_message.dart'; import '../../core/services/persistent_streaming_service.dart'; @@ -12,6 +12,12 @@ import '../../core/utils/tool_calls_parser.dart'; import 'navigation_service.dart'; import '../../shared/widgets/themed_dialogs.dart'; import '../../shared/theme/theme_extensions.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'streaming/helper'); +} // Keep local verbosity toggle for socket logs const bool kSocketVerboseLogging = false; diff --git a/lib/core/services/user_friendly_error_handler.dart b/lib/core/services/user_friendly_error_handler.dart index a0f3afa..4d1b061 100644 --- a/lib/core/services/user_friendly_error_handler.dart +++ b/lib/core/services/user_friendly_error_handler.dart @@ -1,8 +1,14 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' hide debugPrint; import 'package:flutter/material.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../../shared/theme/theme_extensions.dart'; import 'navigation_service.dart'; +import '../utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'errors/user-friendly'); +} /// User-friendly error messages and recovery actions class UserFriendlyErrorHandler { diff --git a/lib/core/utils/debug_logger.dart b/lib/core/utils/debug_logger.dart index 410d040..0c554ae 100644 --- a/lib/core/utils/debug_logger.dart +++ b/lib/core/utils/debug_logger.dart @@ -1,73 +1,283 @@ import 'package:flutter/foundation.dart'; -/// Centralized debug logging utility for the entire app +/// Centralized debug logging utility for the entire app. +/// +/// Messages are rendered in a compact `PREFIX[scope] message key=value` format +/// to keep Flutter's debug console readable while still providing +/// machine-friendly key/value pairs for quick scanning. class DebugLogger { static const bool _enabled = kDebugMode; - /// Log debug information - static void log(String message) { - if (_enabled) { - debugPrint('DEBUG: $message'); - } + /// Log debug information. + static void log(String message, {String? scope, Map? data}) { + _emit(_LogCategory.debug, message, scope: scope, data: data); } - /// Log errors - static void error(String message, [Object? error]) { - if (_enabled) { - if (error != null) { - debugPrint('ERROR: $message: $error'); - } else { - debugPrint('ERROR: $message'); - } - } + /// Log errors with optional error objects and stack traces. + static void error( + String message, { + Object? error, + StackTrace? stackTrace, + String? scope, + Map? data, + }) { + _emit( + _LogCategory.error, + message, + scope: scope, + data: data, + error: error, + stackTrace: stackTrace, + ); } - /// Log warnings - static void warning(String message) { - if (_enabled) { - debugPrint('WARN: $message'); - } + /// Log warnings. + static void warning( + String message, { + String? scope, + Map? data, + }) { + _emit(_LogCategory.warning, message, scope: scope, data: data); } - /// Log success/info messages - static void info(String message) { - if (_enabled) { - debugPrint('INFO: $message'); - } + /// Log informational messages. + static void info( + String message, { + String? scope, + Map? data, + }) { + _emit(_LogCategory.info, message, scope: scope, data: data); } - /// Log navigation events - static void navigation(String message) { - if (_enabled) { - debugPrint('NAV: $message'); - } + /// Log navigation events. + static void navigation( + String message, { + String? scope, + Map? data, + }) { + _emit( + _LogCategory.navigation, + message, + scope: _mergeScope('nav', scope), + data: data, + ); } - /// Log authentication events - static void auth(String message) { - if (_enabled) { - debugPrint('AUTH: $message'); - } + /// Log authentication events. + static void auth( + String message, { + String? scope, + Map? data, + }) { + _emit( + _LogCategory.auth, + message, + scope: _mergeScope('auth', scope), + data: data, + ); } - /// Log streaming events - static void stream(String message) { - if (_enabled) { - debugPrint('STREAM: $message'); - } + /// Log streaming events. + static void stream( + String message, { + String? scope, + Map? data, + }) { + _emit( + _LogCategory.stream, + message, + scope: _mergeScope('stream', scope), + data: data, + ); } - /// Log validation events - static void validation(String message) { - if (_enabled) { - debugPrint('VALIDATION: $message'); - } + /// Log validation events. + static void validation( + String message, { + String? scope, + Map? data, + }) { + _emit( + _LogCategory.validation, + message, + scope: _mergeScope('validation', scope), + data: data, + ); } - /// Log storage events - static void storage(String message) { - if (_enabled) { - debugPrint('STORAGE: $message'); + /// Log storage events. + static void storage( + String message, { + String? scope, + Map? data, + }) { + _emit( + _LogCategory.storage, + message, + scope: _mergeScope('storage', scope), + data: data, + ); + } + + /// Bridge legacy debugPrint messages onto the structured logger. + static void fromLegacy(String message, {String? scope}) { + final trimmed = message.trim(); + if (trimmed.isEmpty) { + return; } + + var working = _stripLegacyDecorations(trimmed); + var category = _LogCategory.debug; + + String stripPrefix(String prefix) { + working = working.substring(prefix.length).trimLeft(); + return working; + } + + final lower = working.toLowerCase(); + if (lower.startsWith('error:')) { + category = _LogCategory.error; + stripPrefix('error:'); + } else if (lower.startsWith('warning:')) { + category = _LogCategory.warning; + stripPrefix('warning:'); + } else if (lower.startsWith('warn:')) { + category = _LogCategory.warning; + stripPrefix('warn:'); + } else if (lower.startsWith('info:')) { + category = _LogCategory.info; + stripPrefix('info:'); + } else if (lower.startsWith('debug:')) { + category = _LogCategory.debug; + stripPrefix('debug:'); + } + + if (working.isEmpty) { + return; + } + + _emit(category, working, scope: scope); + } + + static void _emit( + _LogCategory category, + String message, { + String? scope, + Map? data, + Object? error, + StackTrace? stackTrace, + }) { + if (!_enabled) { + return; + } + + final buffer = StringBuffer(_prefixes[category] ?? 'DBG'); + + final effectiveScope = scope?.trim(); + if (effectiveScope != null && effectiveScope.isNotEmpty) { + buffer + ..write('[') + ..write(effectiveScope) + ..write(']'); + } + + final trimmedMessage = message.trim(); + if (trimmedMessage.isNotEmpty) { + buffer + ..write(' ') + ..write(trimmedMessage); + } + + final formattedData = _formatData(data); + if (formattedData.isNotEmpty) { + buffer + ..write(' ') + ..write(formattedData); + } + + if (error != null) { + buffer + ..write(' err=') + ..write(_stringify(error)); + } + + if (stackTrace != null) { + buffer + ..write(' stack=') + ..write(_stringify(stackTrace)); + } + + debugPrint(buffer.toString()); + } + + static String _formatData(Map? data) { + if (data == null || data.isEmpty) { + return ''; + } + + return data.entries + .map( + (entry) => '${entry.key}=${_stringify(entry.value, maxLength: 48)}', + ) + .join(' '); + } + + static String _stringify(Object? value, {int maxLength = 96}) { + if (value == null) { + return 'null'; + } + + var text = value.toString().replaceAll(RegExp(r'\s+'), ' '); + if (text.length <= maxLength) { + return text; + } + return '${text.substring(0, maxLength - 1)}…'; + } + + static String? _mergeScope(String base, String? scope) { + final trimmed = scope?.trim(); + if (trimmed == null || trimmed.isEmpty) { + return base; + } + return '$base/$trimmed'; } } + +enum _LogCategory { + debug, + info, + warning, + error, + navigation, + auth, + stream, + validation, + storage, +} + +const Map<_LogCategory, String> _prefixes = <_LogCategory, String>{ + _LogCategory.debug: 'DBG', + _LogCategory.info: 'INF', + _LogCategory.warning: 'WRN', + _LogCategory.error: 'ERR', + _LogCategory.navigation: 'NAV', + _LogCategory.auth: 'AUT', + _LogCategory.stream: 'STR', + _LogCategory.validation: 'VAL', + _LogCategory.storage: 'STO', +}; + +String _stripLegacyDecorations(String value) { + var text = value; + // Remove common emoji and decoration prefixes. + const decorations = ['🔍', '✅', '❌', '🟡', '⚠️', 'DEBUG -']; + for (final decoration in decorations) { + if (text.startsWith(decoration)) { + text = text.substring(decoration.length).trimLeft(); + } + } + if (text.startsWith('DEBUG:')) { + text = text.substring(6).trimLeft(); + } + return text; +} diff --git a/lib/features/chat/providers/chat_providers.dart b/lib/features/chat/providers/chat_providers.dart index b1ea78b..05ee93d 100644 --- a/lib/features/chat/providers/chat_providers.dart +++ b/lib/features/chat/providers/chat_providers.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:yaml/yaml.dart' as yaml; -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; import '../../../core/utils/tool_calls_parser.dart'; @@ -15,6 +14,12 @@ import '../services/reviewer_mode_service.dart'; import '../../../shared/services/tasks/task_queue.dart'; import '../../tools/providers/tools_providers.dart'; import 'dart:async'; +import '../../../core/utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'chat/providers'); +} const bool kSocketVerboseLogging = false; diff --git a/lib/features/chat/services/file_attachment_service.dart b/lib/features/chat/services/file_attachment_service.dart index f2cb5cc..d71a136 100644 --- a/lib/features/chat/services/file_attachment_service.dart +++ b/lib/features/chat/services/file_attachment_service.dart @@ -2,13 +2,13 @@ import 'dart:io'; import 'dart:convert'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart' as foundation; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:file_picker/file_picker.dart'; import 'package:image_picker/image_picker.dart'; import 'package:path/path.dart' as path; import '../../../core/services/api_service.dart'; import '../../../core/providers/app_providers.dart'; +import '../../../core/utils/debug_logger.dart'; class FileAttachmentService { final ApiService _apiService; @@ -139,7 +139,11 @@ class FileAttachmentService { final compressedBase64 = base64Encode(compressedBytes); return 'data:image/png;base64,$compressedBase64'; } catch (e) { - foundation.debugPrint('DEBUG: Image compression failed: $e'); + DebugLogger.error( + 'compress-failed', + scope: 'attachments/image', + error: e, + ); return imageDataUrl; // Return original if compression fails } } @@ -152,8 +156,10 @@ class FileAttachmentService { int? maxHeight, }) async { try { - foundation.debugPrint( - 'DEBUG: Converting image to data URL: ${imageFile.path}', + DebugLogger.log( + 'convert-start', + scope: 'attachments/image', + data: {'path': imageFile.path}, ); // Read the file as bytes @@ -180,25 +186,33 @@ class FileAttachmentService { dataUrl = await compressImage(dataUrl, maxWidth, maxHeight); } - foundation.debugPrint( - 'DEBUG: Image converted to data URL with MIME type: $mimeType', + DebugLogger.log( + 'convert-done', + scope: 'attachments/image', + data: {'mime': mimeType}, ); return dataUrl; } catch (e) { - foundation.debugPrint('DEBUG: Failed to convert image to data URL: $e'); + DebugLogger.error('convert-failed', scope: 'attachments/image', error: e); return null; } } // Upload file with progress tracking Stream uploadFile(File file) async* { - foundation.debugPrint('DEBUG: Starting file upload for: ${file.path}'); + DebugLogger.log( + 'upload-start', + scope: 'attachments/file', + data: {'path': file.path}, + ); try { final fileName = path.basename(file.path); final fileSize = await file.length(); - foundation.debugPrint( - 'DEBUG: File details - Name: $fileName, Size: $fileSize bytes', + DebugLogger.log( + 'file-details', + scope: 'attachments/file', + data: {'name': fileName, 'bytes': fileSize}, ); yield FileUploadState( @@ -220,10 +234,12 @@ class FileAttachmentService { ].contains(ext.substring(1)); // Upload ALL files (including images) to server for consistency with web client - foundation.debugPrint('DEBUG: Uploading file to server...'); + DebugLogger.log('upload-progress', scope: 'attachments/file'); final fileId = await _apiService.uploadFile(file.path, fileName); - foundation.debugPrint( - 'DEBUG: File uploaded successfully with ID: $fileId', + DebugLogger.log( + 'upload-complete', + scope: 'attachments/file', + data: {'fileId': fileId}, ); yield FileUploadState( @@ -236,7 +252,7 @@ class FileAttachmentService { isImage: isImage, ); } catch (e) { - foundation.debugPrint('DEBUG: File upload failed: $e'); + DebugLogger.error('upload-failed', scope: 'attachments/file', error: e); final fileName = path.basename(file.path); final fileSize = await file.length(); @@ -421,7 +437,11 @@ class MockFileAttachmentService { // Mock upload file with progress tracking Stream uploadFile(File file) async* { - foundation.debugPrint('DEBUG: Mock file upload for: ${file.path}'); + DebugLogger.log( + 'mock-upload', + scope: 'attachments/mock', + data: {'path': file.path}, + ); final fileName = path.basename(file.path); final fileSize = await file.length(); @@ -457,7 +477,7 @@ class MockFileAttachmentService { fileId: 'mock_file_${DateTime.now().millisecondsSinceEpoch}', ); - foundation.debugPrint('DEBUG: Mock file upload completed'); + DebugLogger.log('mock-complete', scope: 'attachments/mock'); } Future> uploadFiles( diff --git a/lib/features/chat/views/chat_page.dart b/lib/features/chat/views/chat_page.dart index 5d7c02e..abd952a 100644 --- a/lib/features/chat/views/chat_page.dart +++ b/lib/features/chat/views/chat_page.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide debugPrint; import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/widgets/error_boundary.dart'; import '../../../shared/widgets/optimized_list.dart'; @@ -48,6 +48,11 @@ import '../../../shared/widgets/model_avatar.dart'; import '../../../core/services/platform_service.dart' as ps; import 'package:flutter/gestures.dart' show DragStartBehavior; +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'chat/page'); +} + class ChatPage extends ConsumerStatefulWidget { const ChatPage({super.key}); @@ -105,11 +110,15 @@ class _ChatPageState extends ConsumerState { // Check if a model is already selected final selectedModel = ref.read(selectedModelProvider); if (selectedModel != null) { - DebugLogger.log('Model already selected: ${selectedModel.name}'); + DebugLogger.log( + 'selected', + scope: 'chat/model', + data: {'name': selectedModel.name}, + ); return; } - DebugLogger.log('No model selected, attempting auto-selection'); + DebugLogger.log('auto-select-start', scope: 'chat/model'); try { // First ensure models are loaded @@ -119,14 +128,18 @@ class _ChatPageState extends ConsumerState { if (modelsAsync.hasValue) { models = modelsAsync.value!; } else { - DebugLogger.log('Models not loaded yet, fetching...'); + DebugLogger.log('models-fetch', scope: 'chat/model'); models = await ref.read(modelsProvider.future); } - DebugLogger.log('Found ${models.length} models available'); + DebugLogger.log( + 'models-count', + scope: 'chat/model', + data: {'count': models.length}, + ); if (models.isEmpty) { - DebugLogger.log('No models available for selection'); + DebugLogger.warning('models-empty', scope: 'chat/model'); return; } @@ -134,18 +147,24 @@ class _ChatPageState extends ConsumerState { try { final Model? model = await ref.read(defaultModelProvider.future); if (model != null) { - DebugLogger.log('Model auto-selected via provider: ${model.name}'); + DebugLogger.log( + 'auto-select', + scope: 'chat/model', + data: {'name': model.name}, + ); } } catch (e) { - DebugLogger.log( - 'Default provider failed, selecting first model directly', - ); + DebugLogger.warning('provider-fallback', scope: 'chat/model'); // Fallback: select the first available model ref.read(selectedModelProvider.notifier).set(models.first); - DebugLogger.log('Fallback model selected: ${models.first.name}'); + DebugLogger.log( + 'fallback', + scope: 'chat/model', + data: {'name': models.first.name}, + ); } } catch (e) { - DebugLogger.error('Failed to auto-select model', e); + DebugLogger.error('auto-select-failed', scope: 'chat/model', error: e); } } @@ -154,20 +173,28 @@ class _ChatPageState extends ConsumerState { // Check if onboarding has been seen final storage = ref.read(optimizedStorageServiceProvider); final seen = await storage.getOnboardingSeen(); - DebugLogger.log('Chat page - Onboarding seen status: $seen'); + DebugLogger.log( + 'onboarding-status', + scope: 'chat/onboarding', + data: {'seen': seen}, + ); if (!seen && mounted) { // Small delay to ensure navigation has settled await Future.delayed(const Duration(milliseconds: 500)); if (!mounted) return; - DebugLogger.log('Showing onboarding from chat page'); + DebugLogger.log('onboarding-show', scope: 'chat/onboarding'); _showOnboarding(); await storage.setOnboardingSeen(true); - DebugLogger.log('Onboarding marked as seen'); + DebugLogger.log('onboarding-marked', scope: 'chat/onboarding'); } } catch (e) { - DebugLogger.error('Error checking onboarding status', e); + DebugLogger.error( + 'onboarding-status-failed', + scope: 'chat/onboarding', + error: e, + ); } } @@ -199,7 +226,9 @@ class _ChatPageState extends ConsumerState { final activeConversation = ref.read(activeConversationProvider); if (activeConversation != null) { DebugLogger.log( - 'Conversation already active: ${activeConversation.title}', + 'active', + scope: 'chat/demo', + data: {'title': activeConversation.title}, ); return; } diff --git a/lib/features/chat/widgets/assistant_message_widget.dart b/lib/features/chat/widgets/assistant_message_widget.dart index 5816a60..39620ce 100644 --- a/lib/features/chat/widgets/assistant_message_widget.dart +++ b/lib/features/chat/widgets/assistant_message_widget.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart' show listEquals; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -20,6 +20,12 @@ import 'package:conduit/shared/widgets/chat_action_button.dart'; import '../../../shared/widgets/model_avatar.dart'; import 'package:url_launcher/url_launcher_string.dart'; import '../providers/chat_providers.dart' show sendMessage; +import '../../../core/utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'chat/assistant'); +} class AssistantMessageWidget extends ConsumerStatefulWidget { final dynamic message; diff --git a/lib/features/chat/widgets/enhanced_image_attachment.dart b/lib/features/chat/widgets/enhanced_image_attachment.dart index b1f44e7..ad421db 100644 --- a/lib/features/chat/widgets/enhanced_image_attachment.dart +++ b/lib/features/chat/widgets/enhanced_image_attachment.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter_animate/flutter_animate.dart'; @@ -12,6 +12,12 @@ import '../../../shared/theme/theme_extensions.dart'; import 'package:conduit/l10n/app_localizations.dart'; import '../../../core/providers/app_providers.dart'; import '../../auth/providers/unified_auth_providers.dart'; +import '../../../core/utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'chat/image-attachment'); +} // Simple global cache to prevent reloading final _globalImageCache = {}; diff --git a/lib/main.dart b/lib/main.dart index 6ee1201..5e9fedc 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,6 @@ import 'dart:async'; import 'dart:developer' as developer; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide debugPrint; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'core/widgets/error_boundary.dart'; @@ -22,6 +22,11 @@ import 'core/providers/app_startup_providers.dart'; developer.TimelineTask? _startupTimeline; +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'app/main'); +} + void main() { runZonedGuarded( () async { @@ -29,14 +34,23 @@ void main() { // Global error handlers FlutterError.onError = (FlutterErrorDetails details) { - DebugLogger.error('Flutter error', details.exception); + DebugLogger.error( + 'flutter-error', + scope: 'app/framework', + error: details.exception, + ); final stack = details.stack; if (stack != null) { debugPrint(stack.toString()); } }; WidgetsBinding.instance.platformDispatcher.onError = (error, stack) { - DebugLogger.error('Uncaught platform error', error); + DebugLogger.error( + 'platform-error', + scope: 'app/platform', + error: error, + stackTrace: stack, + ); debugPrint(stack.toString()); return true; }; @@ -88,7 +102,12 @@ void main() { developer.Timeline.instantSync('runApp_called'); }, (error, stack) { - DebugLogger.error('Uncaught zone error', error); + DebugLogger.error( + 'zone-error', + scope: 'app', + error: error, + stackTrace: stack, + ); debugPrint(stack.toString()); }, ); @@ -118,7 +137,7 @@ class _ConduitAppState extends ConsumerState { } void _initializeAppState() { - DebugLogger.auth('Initializing unified auth system'); + DebugLogger.auth('init', scope: 'app'); ref.read(authStateManagerProvider); ref.read(authApiIntegrationProvider); diff --git a/lib/shared/services/tasks/task_queue.dart b/lib/shared/services/tasks/task_queue.dart index 6661bb9..9aa1db5 100644 --- a/lib/shared/services/tasks/task_queue.dart +++ b/lib/shared/services/tasks/task_queue.dart @@ -1,13 +1,18 @@ import 'dart:async'; import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:uuid/uuid.dart'; import '../../../core/providers/app_providers.dart'; import 'outbound_task.dart'; import 'task_worker.dart'; +import '../../../core/utils/debug_logger.dart'; + +void debugPrint(String? message, {int? wrapWidth}) { + if (message == null) return; + DebugLogger.fromLegacy(message, scope: 'tasks/queue'); +} final taskQueueProvider = NotifierProvider>(