refactor: debug logs

This commit is contained in:
cogwheel0
2025-08-20 22:15:26 +05:30
parent 9a5c5a573f
commit 4dc9ce1762
27 changed files with 1965 additions and 2195 deletions

View File

@@ -1,5 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import '../utils/debug_logger.dart';
/// Consistent authentication interceptor for all API requests
/// Implements security requirements from OpenAPI specification
@@ -77,8 +77,8 @@ class ApiAuthInterceptor extends Interceptor {
final requiresAuth = _requiresAuth(path);
final hasOptionalAuth = _hasOptionalAuth(path);
debugPrint(
'DEBUG: Auth interceptor for $path - requires: $requiresAuth, optional: $hasOptionalAuth, token present: ${_authToken != null}',
DebugLogger.auth(
'Auth interceptor for $path - requires: $requiresAuth, optional: $hasOptionalAuth, token present: ${_authToken != null}',
);
if (requiresAuth) {
@@ -109,12 +109,14 @@ class ApiAuthInterceptor extends Interceptor {
customHeaders.forEach((key, value) {
// Don't override critical headers that we manage
final lowerKey = key.toLowerCase();
if (lowerKey != 'authorization' &&
lowerKey != 'content-type' &&
if (lowerKey != 'authorization' &&
lowerKey != 'content-type' &&
lowerKey != 'accept') {
options.headers[key] = value;
} else {
debugPrint('WARNING: Skipping reserved header override attempt: $key');
DebugLogger.warning(
'Skipping reserved header override attempt: $key',
);
}
});
}
@@ -134,20 +136,20 @@ class ApiAuthInterceptor extends Interceptor {
// Handle authentication errors consistently
if (statusCode == 401) {
// 401 always indicates invalid/expired auth token
debugPrint('DEBUG: 401 Unauthorized on $path - clearing auth token');
DebugLogger.auth('401 Unauthorized on $path - clearing auth token');
_clearAuthToken();
} else if (statusCode == 403) {
// 403 on protected endpoints indicates insufficient permissions or invalid token
final requiresAuth = _requiresAuth(path);
final optionalAuth = _hasOptionalAuth(path);
if (requiresAuth && !optionalAuth) {
debugPrint(
'DEBUG: 403 Forbidden on protected endpoint $path - clearing auth token',
DebugLogger.auth(
'403 Forbidden on protected endpoint $path - clearing auth token',
);
_clearAuthToken();
} else {
debugPrint(
'DEBUG: 403 Forbidden on public/optional endpoint $path - keeping auth token',
DebugLogger.auth(
'403 Forbidden on public/optional endpoint $path - keeping auth token',
);
}
}

View File

@@ -1,5 +1,5 @@
import 'package:flutter/foundation.dart';
import 'auth_state_manager.dart';
import '../utils/debug_logger.dart';
/// Comprehensive caching manager for auth-related operations
/// Reduces redundant operations and improves app performance
@@ -31,13 +31,13 @@ class AuthCacheManager {
void cacheUserData(dynamic userData) {
_cache[_userDataKey] = userData;
_cacheTimestamps[_userDataKey] = DateTime.now();
debugPrint('DEBUG: User data cached');
DebugLogger.storage('User data cached');
}
/// Get cached user data
dynamic getCachedUserData() {
if (_isCacheValid(_userDataKey, _mediumCache)) {
debugPrint('DEBUG: Using cached user data');
DebugLogger.storage('Using cached user data');
return _cache[_userDataKey];
}
return null;
@@ -97,14 +97,14 @@ class AuthCacheManager {
void clearCacheEntry(String key) {
_cache.remove(key);
_cacheTimestamps.remove(key);
debugPrint('DEBUG: Cache entry cleared: $key');
DebugLogger.storage('Cache entry cleared: $key');
}
/// Clear all auth-related cache
void clearAuthCache() {
_cache.clear();
_cacheTimestamps.clear();
debugPrint('DEBUG: All auth cache cleared');
DebugLogger.storage('All auth cache cleared');
}
/// Clear expired cache entries
@@ -125,7 +125,9 @@ class AuthCacheManager {
}
if (expiredKeys.isNotEmpty) {
debugPrint('DEBUG: Cleaned ${expiredKeys.length} expired cache entries');
DebugLogger.storage(
'Cleaned ${expiredKeys.length} expired cache entries',
);
}
}
@@ -168,7 +170,9 @@ class AuthCacheManager {
_cacheTimestamps.remove(key);
}
debugPrint('DEBUG: Cache optimized, removed $entriesToRemove old entries');
DebugLogger.storage(
'Cache optimized, removed $entriesToRemove old entries',
);
}
/// Cache state from AuthState for quick access

View File

@@ -5,6 +5,7 @@ import '../providers/app_providers.dart';
import '../models/user.dart';
import 'token_validator.dart';
import 'auth_cache_manager.dart';
import '../utils/debug_logger.dart';
/// Comprehensive auth state representation
@immutable
@@ -95,10 +96,10 @@ class AuthStateManager extends StateNotifier<AuthState> {
final token = await storage.getAuthToken();
if (token != null && token.isNotEmpty) {
debugPrint('DEBUG: Found stored token during initialization: ${token.substring(0, 10)}...');
DebugLogger.auth('Found stored token during initialization');
// Validate token before setting authenticated state
final isValid = await _validateToken(token);
debugPrint('DEBUG: Token validation result: $isValid');
DebugLogger.auth('Token validation result: $isValid');
if (isValid) {
state = state.copyWith(
status: AuthStatus.authenticated,
@@ -114,7 +115,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
_loadUserData();
} else {
// Token is invalid, clear it
debugPrint('DEBUG: Token validation failed, deleting token');
DebugLogger.auth('Token validation failed, deleting token');
await storage.deleteAuthToken();
state = state.copyWith(
status: AuthStatus.unauthenticated,
@@ -167,19 +168,19 @@ class AuthStateManager extends StateNotifier<AuthState> {
// Use API key directly as Bearer token
final tokenStr = apiKey.trim();
// Validate token format (consistent with credentials method)
if (!_isValidTokenFormat(tokenStr)) {
throw Exception('Invalid API key format');
}
// Update API service with the API key
_updateApiServiceToken(tokenStr);
// Validate by attempting to fetch user info
try {
await api.getCurrentUser(); // Just validate, don't store user data yet
// Save token to storage
final storage = _ref.read(optimizedStorageServiceProvider);
await storage.saveAuthToken(tokenStr);
@@ -191,7 +192,8 @@ class AuthStateManager extends StateNotifier<AuthState> {
// Store API key as a special credential type
await storage.saveCredentials(
serverId: activeServer.id,
username: 'api_key_user', // Special username to indicate API key auth
username:
'api_key_user', // Special username to indicate API key auth
password: tokenStr, // Store API key in password field
);
await storage.setRememberCredentials(true);
@@ -215,7 +217,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
// Load user data in background (consistent with credentials method)
_loadUserData();
debugPrint('DEBUG: API key login successful');
DebugLogger.auth('API key login successful');
return true;
} catch (e) {
// If user fetch fails, the API key might be invalid
@@ -300,7 +302,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
// Load user data in background
_loadUserData();
debugPrint('DEBUG: Login successful');
DebugLogger.auth('Login successful');
return true;
} catch (e) {
debugPrint('ERROR: Login failed: $e');
@@ -399,7 +401,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
/// Handle token invalidation (called by API service)
Future<void> onTokenInvalidated() async {
debugPrint('DEBUG: Auth token invalidated');
DebugLogger.auth('Auth token invalidated');
// Clear token from storage
final storage = _ref.read(optimizedStorageServiceProvider);
@@ -416,7 +418,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
// Attempt silent re-login if credentials are available
final hasCredentials = await storage.getSavedCredentials() != null;
if (hasCredentials) {
debugPrint('DEBUG: Attempting silent re-login after token invalidation');
DebugLogger.auth('Attempting silent re-login after token invalidation');
await silentLogin();
}
}
@@ -449,7 +451,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
clearError: true,
);
debugPrint('DEBUG: Logout complete');
DebugLogger.auth('Logout complete');
} catch (e) {
debugPrint('ERROR: Logout failed: $e');
// Even if logout fails, clear local state
@@ -470,7 +472,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
if (state.token != null) {
final jwtUserInfo = TokenValidator.extractUserInfo(state.token!);
if (jwtUserInfo != null) {
debugPrint('DEBUG: Extracted user info from JWT token');
DebugLogger.auth('Extracted user info from JWT token');
state = state.copyWith(user: jwtUserInfo);
// Still try to load from server in background for complete data
@@ -502,7 +504,7 @@ class AuthStateManager extends StateNotifier<AuthState> {
final user = await api.getCurrentUser();
state = state.copyWith(user: user);
debugPrint('DEBUG: Loaded complete user data from server');
DebugLogger.auth('Loaded complete user data from server');
}
} catch (e) {
debugPrint('Warning: Failed to load server user data: $e');
@@ -527,8 +529,8 @@ class AuthStateManager extends StateNotifier<AuthState> {
// Check cache first
final cachedResult = TokenValidationCache.getCachedResult(token);
if (cachedResult != null) {
debugPrint(
'DEBUG: Using cached token validation result: ${cachedResult.isValid}',
DebugLogger.auth(
'Using cached token validation result: ${cachedResult.isValid}',
);
return cachedResult.isValid;
}
@@ -571,13 +573,13 @@ class AuthStateManager extends StateNotifier<AuthState> {
validationUser != null &&
state.isAuthenticated) {
state = state.copyWith(user: validationUser);
debugPrint('DEBUG: Cached user data from token validation');
DebugLogger.auth('Cached user data from token validation');
}
TokenValidationCache.cacheResult(token, serverResult);
debugPrint(
'DEBUG: Server token validation: ${serverResult.isValid} - ${serverResult.message}',
DebugLogger.auth(
'Server token validation: ${serverResult.isValid} - ${serverResult.message}',
);
return serverResult.isValid;
} catch (e) {

View File

@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'api_error.dart';
import 'error_parser.dart';
import '../utils/debug_logger.dart';
/// Comprehensive API error handler with structured error parsing
/// Handles all types of API errors and converts them to standardized format
@@ -319,7 +320,7 @@ class ApiErrorHandler {
debugPrint(' Status: ${dioError.response?.statusCode}');
if (dioError.response?.data != null) {
debugPrint(' Response: ${dioError.response?.data}');
DebugLogger.error('Response data available (truncated for security)');
}
if (dioError.requestOptions.data != null) {

View File

@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'api_error_handler.dart';
import 'api_error.dart';
import '../utils/debug_logger.dart';
/// Dio interceptor for automatic error handling and transformation
/// Converts all HTTP errors into standardized ApiError format
@@ -152,7 +153,7 @@ class ApiErrorInterceptor extends Interceptor {
// Log response data if available and not too large
final responseData = originalError.response?.data;
if (responseData != null && responseData.toString().length < 1000) {
debugPrint(' Response: $responseData');
DebugLogger.error('Response data available (truncated for security)');
}
}

View File

@@ -20,6 +20,7 @@ import '../models/file_info.dart';
import '../models/knowledge_base.dart';
import '../services/settings_service.dart';
import '../services/optimized_storage_service.dart';
import '../utils/debug_logger.dart';
// Storage providers
final sharedPreferencesProvider = Provider<SharedPreferences>((ref) {
@@ -236,11 +237,9 @@ final modelsProvider = FutureProvider<List<Model>>((ref) async {
if (api == null) return [];
try {
foundation.debugPrint('DEBUG: Fetching models from server');
DebugLogger.log('Fetching models from server');
final models = await api.getModels();
foundation.debugPrint(
'DEBUG: Successfully fetched ${models.length} models',
);
DebugLogger.log('Successfully fetched ${models.length} models');
return models;
} catch (e) {
foundation.debugPrint('ERROR: Failed to fetch models: $e');
@@ -248,8 +247,8 @@ final modelsProvider = FutureProvider<List<Model>>((ref) async {
// 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')) {
foundation.debugPrint(
'DEBUG: Models endpoint returned 403 - authentication may be invalid',
DebugLogger.warning(
'Models endpoint returned 403 - authentication may be invalid',
);
}
@@ -280,8 +279,8 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
// Check if we have a recent cache (within 5 seconds)
final lastFetch = ref.read(_conversationsCacheTimestamp);
if (lastFetch != null && DateTime.now().difference(lastFetch).inSeconds < 5) {
foundation.debugPrint(
'DEBUG: Using cached conversations (fetched ${DateTime.now().difference(lastFetch).inSeconds}s ago)',
DebugLogger.log(
'Using cached conversations (fetched ${DateTime.now().difference(lastFetch).inSeconds}s ago)',
);
// Note: Can't read our own provider here, would cause a cycle
// The caching is handled by Riverpod's built-in mechanism
@@ -311,25 +310,23 @@ final conversationsProvider = FutureProvider<List<Conversation>>((ref) async {
}
final api = ref.watch(apiServiceProvider);
if (api == null) {
foundation.debugPrint('DEBUG: No API service available');
DebugLogger.log('No API service available');
return [];
}
try {
foundation.debugPrint(
'DEBUG: Fetching conversations from OpenWebUI API...',
);
DebugLogger.log('Fetching conversations from OpenWebUI API...');
final conversations = await api
.getConversations(); // Fetch all conversations
foundation.debugPrint(
'DEBUG: Successfully fetched ${conversations.length} conversations',
DebugLogger.log(
'Successfully fetched ${conversations.length} conversations',
);
// Also fetch folder information and update conversations with folder IDs
try {
final foldersData = await api.getFolders();
foundation.debugPrint(
'DEBUG: Fetched ${foldersData.length} folders for conversation mapping',
DebugLogger.log(
'Fetched ${foldersData.length} folders for conversation mapping',
);
// Parse folder data into Folder objects
@@ -526,7 +523,7 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
// Check if a model is manually selected
final currentSelected = ref.read(selectedModelProvider);
final isManualSelection = ref.read(isManualModelSelectionProvider);
if (currentSelected != null && isManualSelection) {
foundation.debugPrint(
'DEBUG: Manual model selected in reviewer mode: ${currentSelected.name}',
@@ -567,7 +564,7 @@ final defaultModelProvider = FutureProvider<Model?>((ref) async {
// First check user's preferred default model
final userSettings = ref.read(appSettingsProvider);
final userDefaultModelId = userSettings.defaultModel;
if (userDefaultModelId != null && userDefaultModelId.isNotEmpty) {
try {
selectedModel = models.firstWhere(
@@ -910,7 +907,7 @@ final foldersProvider = FutureProvider<List<Folder>>((ref) async {
try {
foundation.debugPrint('DEBUG: Fetching folders from API...');
final foldersData = await api.getFolders();
foundation.debugPrint('DEBUG: Raw folders data: $foldersData');
foundation.debugPrint('DEBUG: Raw folders data received successfully');
final folders = foldersData
.map((folderData) => Folder.fromJson(folderData))
.toList();

View File

@@ -17,6 +17,7 @@ import '../error/api_error_interceptor.dart';
import 'sse_parser.dart';
import 'stream_recovery_service.dart';
import 'persistent_streaming_service.dart';
import '../utils/debug_logger.dart';
class ApiService {
final Dio _dio;
@@ -98,19 +99,23 @@ class ApiService {
if (options.data != null) {
if (options.data is Map) {
final dataMap = options.data as Map<String, dynamic>;
debugPrint('Data type: Map');
debugPrint('Data keys: ${dataMap.keys.toList()}');
debugPrint(
DebugLogger.log('Data type: Map');
DebugLogger.log('Data keys: ${dataMap.keys.toList()}');
DebugLogger.log(
'Has background_tasks: ${dataMap.containsKey('background_tasks')}',
);
debugPrint(
DebugLogger.log(
'Has session_id: ${dataMap.containsKey('session_id')}',
);
debugPrint('Has id: ${dataMap.containsKey('id')}');
debugPrint('Full data: ${jsonEncode(dataMap)}');
DebugLogger.log('Has id: ${dataMap.containsKey('id')}');
DebugLogger.log(
'Data structure logged (full data suppressed)',
);
} else {
debugPrint('Data type: ${options.data.runtimeType}');
debugPrint('Data: ${options.data}');
DebugLogger.log('Data type: ${options.data.runtimeType}');
DebugLogger.log(
'Data structure logged (full data suppressed)',
);
}
}
debugPrint('===== END SSE REQUEST DEBUG =====');
@@ -120,17 +125,8 @@ class ApiService {
),
);
// 5. Standard logging interceptor
_dio.interceptors.add(
LogInterceptor(
requestBody: true,
responseBody: false, // Don't log response bodies to reduce noise
requestHeader: true,
responseHeader: false,
error: true,
logPrint: (obj) => debugPrint('API: $obj'),
),
);
// LogInterceptor removed - was exposing sensitive data and creating verbose logs
// We now use custom interceptors with secure logging via DebugLogger
}
// Initialize validation interceptor asynchronously
@@ -247,14 +243,13 @@ class ApiService {
// User info
Future<User> getCurrentUser() async {
final response = await _dio.get('/api/v1/auths/');
debugPrint('DEBUG: /api/v1/auths/ response: ${jsonEncode(response.data)}');
DebugLogger.log('User info retrieved successfully');
return User.fromJson(response.data);
}
// Models
Future<List<Model>> getModels() async {
final response = await _dio.get('/api/models');
debugPrint('DEBUG: /api/models raw response: ${jsonEncode(response.data)}');
// Handle different response formats
List<dynamic> models;
@@ -265,11 +260,11 @@ class ApiService {
// Response is a direct array
models = response.data as List;
} else {
debugPrint('ERROR: Unexpected models response format');
DebugLogger.error('Unexpected models response format');
return [];
}
debugPrint('DEBUG: Found ${models.length} models');
DebugLogger.log('Found ${models.length} models');
return models.map((m) => Model.fromJson(m)).toList();
}
@@ -279,7 +274,7 @@ class ApiService {
debugPrint('DEBUG: Fetching default model from user settings');
final response = await _dio.get('/api/v1/users/user/settings');
debugPrint('DEBUG: User settings response: ${jsonEncode(response.data)}');
DebugLogger.log('User settings retrieved successfully');
final settings = response.data as Map<String, dynamic>;
@@ -290,20 +285,20 @@ class ApiService {
if (models != null && models.isNotEmpty) {
// Return the first model in the user's preferred models list
final defaultModel = models.first.toString();
debugPrint(
'DEBUG: Found default model from user settings: $defaultModel',
DebugLogger.log(
'Found default model from user settings: $defaultModel',
);
return defaultModel;
}
}
debugPrint('DEBUG: No default model found in user settings');
DebugLogger.log('No default model found in user settings');
return null;
} catch (e) {
debugPrint('DEBUG: Error fetching default model from user settings: $e');
DebugLogger.error('Error fetching default model from user settings', e);
// Fall back to trying the old endpoint
try {
debugPrint('DEBUG: Falling back to configs/models endpoint');
DebugLogger.log('Falling back to configs/models endpoint');
final response = await _dio.get('/api/v1/configs/models');
final config = response.data as Map<String, dynamic>;
@@ -313,11 +308,11 @@ class ApiService {
config['default_model'] as String?;
if (defaultModel != null && defaultModel.isNotEmpty) {
debugPrint('DEBUG: Found default model from fallback: $defaultModel');
DebugLogger.log('Found default model from fallback: $defaultModel');
return defaultModel;
}
} catch (fallbackError) {
debugPrint('DEBUG: Fallback also failed: $fallbackError');
DebugLogger.error('Fallback also failed', fallbackError);
}
return null;
@@ -572,10 +567,10 @@ class ApiService {
}
Future<Conversation> getConversation(String id) async {
debugPrint('DEBUG: Fetching individual chat: $id');
DebugLogger.log('Fetching individual chat: $id');
final response = await _dio.get('/api/v1/chats/$id');
debugPrint('DEBUG: Chat response: ${response.data}');
DebugLogger.log('Chat response received successfully');
// Parse OpenWebUI ChatResponse format
final chatData = response.data as Map<String, dynamic>;
@@ -820,10 +815,10 @@ class ApiService {
final response = await _dio.post('/api/v1/chats/new', data: chatData);
debugPrint(
'DEBUG: Create conversation response status: ${response.statusCode}',
DebugLogger.log(
'Create conversation response status: ${response.statusCode}',
);
debugPrint('DEBUG: Create conversation response data: ${response.data}');
DebugLogger.log('Create conversation response received successfully');
// Parse the response
final responseData = response.data as Map<String, dynamic>;
@@ -914,12 +909,9 @@ class ApiService {
debugPrint('DEBUG: Updating chat with OpenWebUI format data using POST');
// OpenWebUI uses POST not PUT for updating chats
final response = await _dio.post(
'/api/v1/chats/$conversationId',
data: chatData,
);
await _dio.post('/api/v1/chats/$conversationId', data: chatData);
debugPrint('DEBUG: Update conversation response: ${response.data}');
DebugLogger.log('Update conversation response received successfully');
}
Future<void> updateConversation(
@@ -1012,15 +1004,15 @@ class ApiService {
try {
debugPrint('DEBUG: Fetching folders from /api/v1/folders/');
final response = await _dio.get('/api/v1/folders/');
debugPrint('DEBUG: Folders response status: ${response.statusCode}');
debugPrint('DEBUG: Folders response data: ${response.data}');
DebugLogger.log('Folders response status: ${response.statusCode}');
DebugLogger.log('Folders response received successfully');
final data = response.data;
if (data is List) {
debugPrint('DEBUG: Found ${data.length} folders');
return data.cast<Map<String, dynamic>>();
} else {
debugPrint('DEBUG: Response data is not a list: ${data.runtimeType}');
DebugLogger.log('Response data is not a list: ${data.runtimeType}');
return [];
}
} catch (e) {
@@ -1379,17 +1371,17 @@ class ApiService {
data: {'queries': queries},
);
debugPrint('DEBUG: Web search response status: ${response.statusCode}');
debugPrint(
'DEBUG: Web search response type: ${response.data.runtimeType}',
);
debugPrint('DEBUG: Web search response data: ${response.data}');
DebugLogger.log('Web search response status: ${response.statusCode}');
DebugLogger.log('Web search response type: ${response.data.runtimeType}');
DebugLogger.log('Web search response received successfully');
return response.data as Map<String, dynamic>;
} catch (e) {
debugPrint('DEBUG: Web search API error: $e');
if (e is DioException) {
debugPrint('DEBUG: Web search error response: ${e.response?.data}');
DebugLogger.error(
'Web search error response available (truncated for security)',
);
debugPrint('DEBUG: Web search error status: ${e.response?.statusCode}');
}
rethrow;
@@ -1406,7 +1398,7 @@ class ApiService {
if (response.statusCode == 200 && response.data != null) {
final modelData = response.data as Map<String, dynamic>;
debugPrint('DEBUG: Model details for $modelId: $modelData');
DebugLogger.log('Model details for $modelId retrieved successfully');
return modelData;
}
} catch (e) {
@@ -1430,7 +1422,7 @@ class ApiService {
);
if (response.statusCode == 200 && response.data != null) {
debugPrint('DEBUG: Raw title response: ${response.data}');
DebugLogger.log('Raw title response received successfully');
// Parse the complex response structure
String? extractedTitle;
@@ -1604,7 +1596,7 @@ class ApiService {
debugPrint(
'DEBUG: Collection query response type: ${response.data.runtimeType}',
);
debugPrint('DEBUG: Collection query response data: ${response.data}');
DebugLogger.log('Collection query response received successfully');
if (response.data is List) {
return response.data as List<dynamic>;
@@ -1644,7 +1636,7 @@ class ApiService {
debugPrint(
'DEBUG: Retrieval config response status: ${response.statusCode}',
);
debugPrint('DEBUG: Retrieval config response data: ${response.data}');
DebugLogger.log('Retrieval config response received successfully');
return response.data as Map<String, dynamic>;
} catch (e) {
@@ -1730,7 +1722,7 @@ class ApiService {
debugPrint(
'DEBUG: Transcription response status: ${response.statusCode}',
);
debugPrint('DEBUG: Transcription response data: $data');
DebugLogger.log('Transcription response received successfully');
if (data is String) return data;
if (data is Map<String, dynamic>) {
final text = data['text'] ?? data['transcription'] ?? data['result'];
@@ -1760,7 +1752,9 @@ class ApiService {
debugPrint(
'DEBUG: Transcription retry status: ${retryResponse.statusCode}',
);
debugPrint('DEBUG: Transcription retry data: $rdata');
DebugLogger.log(
'Transcription retry response received successfully',
);
if (rdata is String) return rdata;
if (rdata is Map<String, dynamic>) {
final text =
@@ -2603,7 +2597,7 @@ class ApiService {
),
);
debugPrint('DEBUG: Sending SSE request with data: ${jsonEncode(data)}');
DebugLogger.log('Sending SSE request with data structure logged');
final response = await streamDio.post(
'/api/chat/completions',
@@ -2678,8 +2672,8 @@ class ApiService {
// Log what we got if we couldn't extract content
if (!streamController.isClosed) {
debugPrint('DEBUG: JSON response structure: ${json.keys}');
debugPrint('DEBUG: Full JSON response: $json');
DebugLogger.log('JSON response structure: ${json.keys}');
DebugLogger.log('JSON response received (full data suppressed)');
// Check if it's a task-based response
if (json is Map && json.containsKey('task_id')) {
@@ -3020,8 +3014,8 @@ class ApiService {
debugPrint('DEBUG: Uploading to /api/v1/files/');
final response = await _dio.post('/api/v1/files/', data: formData);
debugPrint('DEBUG: Upload response status: ${response.statusCode}');
debugPrint('DEBUG: Upload response data: ${response.data}');
DebugLogger.log('Upload response status: ${response.statusCode}');
DebugLogger.log('Upload response received successfully');
if (response.data is Map && response.data['id'] != null) {
final fileId = response.data['id'] as String;
@@ -3065,13 +3059,13 @@ class ApiService {
debugPrint('Testing endpoint: $endpoint');
final response = await _dio.get(endpoint);
debugPrint('$endpoint - Status: ${response.statusCode}');
debugPrint(' Response type: ${response.data.runtimeType}');
DebugLogger.log(' Response type: ${response.data.runtimeType}');
if (response.data is List) {
debugPrint(' Array length: ${(response.data as List).length}');
DebugLogger.log(' Array length: ${(response.data as List).length}');
} else if (response.data is Map) {
debugPrint(' Object keys: ${(response.data as Map).keys}');
DebugLogger.log(' Object keys: ${(response.data as Map).keys}');
}
debugPrint(
DebugLogger.log(
' Sample data: ${response.data.toString().substring(0, 200)}...',
);
} catch (e) {

View File

@@ -1,93 +1,105 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../utils/debug_logger.dart';
/// Handles background streaming continuation for iOS and Android
///
///
/// On iOS: Uses background tasks to keep streams alive for ~30 seconds
/// On Android: Uses foreground service notifications
class BackgroundStreamingHandler {
static const MethodChannel _channel = MethodChannel('conduit/background_streaming');
static const MethodChannel _channel = MethodChannel(
'conduit/background_streaming',
);
static BackgroundStreamingHandler? _instance;
static BackgroundStreamingHandler get instance => _instance ??= BackgroundStreamingHandler._();
static BackgroundStreamingHandler get instance =>
_instance ??= BackgroundStreamingHandler._();
BackgroundStreamingHandler._() {
_setupMethodCallHandler();
}
final Set<String> _activeStreamIds = <String>{};
final Map<String, StreamState> _streamStates = <String, StreamState>{};
// Callbacks for platform-specific events
void Function(List<String> streamIds)? onStreamsSuspending;
void Function()? onBackgroundTaskExpiring;
bool Function()? shouldContinueInBackground;
void _setupMethodCallHandler() {
_channel.setMethodCallHandler((call) async {
switch (call.method) {
case 'checkStreams':
return _activeStreamIds.length;
case 'streamsSuspending':
final Map<String, dynamic> args = call.arguments as Map<String, dynamic>;
final List<String> streamIds = (args['streamIds'] as List).cast<String>();
final Map<String, dynamic> args =
call.arguments as Map<String, dynamic>;
final List<String> streamIds = (args['streamIds'] as List)
.cast<String>();
final String reason = args['reason'] as String;
debugPrint('Background: Streams suspending - $streamIds (reason: $reason)');
DebugLogger.stream(
'Background: Streams suspending - $streamIds (reason: $reason)',
);
onStreamsSuspending?.call(streamIds);
// Save stream states for recovery
await _saveStreamStatesForRecovery(streamIds, reason);
break;
case 'backgroundTaskExpiring':
debugPrint('Background: Background task expiring');
DebugLogger.stream('Background: Background task expiring');
onBackgroundTaskExpiring?.call();
break;
}
});
}
/// Start background execution for given stream IDs
Future<void> startBackgroundExecution(List<String> streamIds) async {
if (!Platform.isIOS && !Platform.isAndroid) return;
_activeStreamIds.addAll(streamIds);
try {
await _channel.invokeMethod('startBackgroundExecution', {
'streamIds': streamIds,
});
debugPrint('Background: Started background execution for ${streamIds.length} streams');
DebugLogger.stream(
'Background: Started background execution for ${streamIds.length} streams',
);
} catch (e) {
debugPrint('Background: Failed to start background execution: $e');
DebugLogger.error('Background: Failed to start background execution', e);
}
}
/// Stop background execution for given stream IDs
Future<void> stopBackgroundExecution(List<String> streamIds) async {
if (!Platform.isIOS && !Platform.isAndroid) return;
_activeStreamIds.removeAll(streamIds);
streamIds.forEach(_streamStates.remove);
try {
await _channel.invokeMethod('stopBackgroundExecution', {
'streamIds': streamIds,
});
debugPrint('Background: Stopped background execution for ${streamIds.length} streams');
DebugLogger.stream(
'Background: Stopped background execution for ${streamIds.length} streams',
);
} catch (e) {
debugPrint('Background: Failed to stop background execution: $e');
DebugLogger.error('Background: Failed to stop background execution', e);
}
}
/// Register a stream with its current state
void registerStream(String streamId, {
void registerStream(
String streamId, {
required String conversationId,
required String messageId,
String? sessionId,
@@ -103,58 +115,61 @@ class BackgroundStreamingHandler {
lastContent: lastContent ?? '',
timestamp: DateTime.now(),
);
_activeStreamIds.add(streamId);
}
/// Update stream state with new chunk
void updateStreamState(String streamId, {
void updateStreamState(
String streamId, {
int? chunkSequence,
String? content,
String? appendedContent,
}) {
final state = _streamStates[streamId];
if (state == null) return;
_streamStates[streamId] = state.copyWith(
lastChunkSequence: chunkSequence ?? state.lastChunkSequence,
lastContent: appendedContent != null
lastContent: appendedContent != null
? (state.lastContent + appendedContent)
: (content ?? state.lastContent),
timestamp: DateTime.now(),
);
}
/// Unregister a stream when it completes
void unregisterStream(String streamId) {
_activeStreamIds.remove(streamId);
_streamStates.remove(streamId);
}
/// Get current stream state for recovery
StreamState? getStreamState(String streamId) {
return _streamStates[streamId];
}
/// Keep alive the background task (iOS only)
Future<void> keepAlive() async {
if (!Platform.isIOS) return;
try {
await _channel.invokeMethod('keepAlive');
} catch (e) {
debugPrint('Background: Failed to keep alive: $e');
DebugLogger.error('Background: Failed to keep alive', e);
}
}
/// Recover stream states from previous app session
Future<List<StreamState>> recoverStreamStates() async {
if (!Platform.isIOS && !Platform.isAndroid) return [];
try {
final List<dynamic>? states = await _channel.invokeMethod('recoverStreamStates');
final List<dynamic>? states = await _channel.invokeMethod(
'recoverStreamStates',
);
if (states == null) return [];
final recovered = <StreamState>[];
for (final stateData in states) {
final map = stateData as Map<String, dynamic>;
@@ -164,39 +179,44 @@ class BackgroundStreamingHandler {
_streamStates[state.streamId] = state;
}
}
debugPrint('Background: Recovered ${recovered.length} stream states');
DebugLogger.stream(
'Background: Recovered ${recovered.length} stream states',
);
return recovered;
} catch (e) {
debugPrint('Background: Failed to recover stream states: $e');
DebugLogger.error('Background: Failed to recover stream states', e);
return [];
}
}
/// Save stream states for recovery after app restart
Future<void> _saveStreamStatesForRecovery(List<String> streamIds, String reason) async {
Future<void> _saveStreamStatesForRecovery(
List<String> streamIds,
String reason,
) async {
final statesToSave = streamIds
.map((id) => _streamStates[id])
.where((state) => state != null)
.map((state) => state!.toMap())
.toList();
try {
await _channel.invokeMethod('saveStreamStates', {
'states': statesToSave,
'reason': reason,
});
} catch (e) {
debugPrint('Background: Failed to save stream states: $e');
DebugLogger.error('Background: Failed to save stream states', e);
}
}
/// Check if any streams are currently active
bool get hasActiveStreams => _activeStreamIds.isNotEmpty;
/// Get list of active stream IDs
List<String> get activeStreamIds => _activeStreamIds.toList();
/// Clear all stream data (usually on app termination)
void clearAll() {
_activeStreamIds.clear();
@@ -213,7 +233,7 @@ class StreamState {
final int lastChunkSequence;
final String lastContent;
final DateTime timestamp;
const StreamState({
required this.streamId,
required this.conversationId,
@@ -223,7 +243,7 @@ class StreamState {
required this.lastContent,
required this.timestamp,
});
StreamState copyWith({
String? streamId,
String? conversationId,
@@ -243,7 +263,7 @@ class StreamState {
timestamp: timestamp ?? this.timestamp,
);
}
Map<String, dynamic> toMap() {
return {
'streamId': streamId,
@@ -255,7 +275,7 @@ class StreamState {
'timestamp': timestamp.millisecondsSinceEpoch,
};
}
static StreamState? fromMap(Map<String, dynamic> map) {
try {
return StreamState(
@@ -270,20 +290,20 @@ class StreamState {
),
);
} catch (e) {
debugPrint('Failed to parse StreamState from map: $e');
DebugLogger.error('Failed to parse StreamState from map', e);
return null;
}
}
/// Check if this state is stale (older than threshold)
bool isStale({Duration threshold = const Duration(minutes: 5)}) {
return DateTime.now().difference(timestamp) > threshold;
}
@override
String toString() {
return 'StreamState(streamId: $streamId, conversationId: $conversationId, '
'messageId: $messageId, sequence: $lastChunkSequence, '
'contentLength: ${lastContent.length}, timestamp: $timestamp)';
'messageId: $messageId, sequence: $lastChunkSequence, '
'contentLength: ${lastContent.length}, timestamp: $timestamp)';
}
}
}

View File

@@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/debug_logger.dart';
/// Navigation state data model
class NavigationState {
@@ -58,9 +59,9 @@ class NavigationStateService {
try {
_prefs = await SharedPreferences.getInstance();
await _loadNavigationState();
debugPrint('DEBUG: NavigationStateService initialized');
DebugLogger.navigation('NavigationStateService initialized');
} catch (e) {
debugPrint('ERROR: Failed to initialize NavigationStateService: $e');
DebugLogger.error('Failed to initialize NavigationStateService', e);
}
}
@@ -95,9 +96,9 @@ class NavigationStateService {
await _saveNavigationState();
debugPrint('DEBUG: Navigation state pushed - ${state.routeName}');
DebugLogger.navigation('Navigation state pushed - ${state.routeName}');
} catch (e) {
debugPrint('ERROR: Failed to push navigation state: $e');
DebugLogger.error('Failed to push navigation state', e);
}
}
@@ -114,10 +115,12 @@ class NavigationStateService {
await _saveNavigationState();
debugPrint('DEBUG: Navigation state popped - ${poppedState.routeName}');
DebugLogger.navigation(
'Navigation state popped - ${poppedState.routeName}',
);
return poppedState;
} catch (e) {
debugPrint('ERROR: Failed to pop navigation state: $e');
DebugLogger.error('Failed to pop navigation state', e);
return null;
}
}
@@ -153,9 +156,9 @@ class NavigationStateService {
_stateNotifier.value = updatedState;
await _saveNavigationState();
debugPrint('DEBUG: Navigation state updated');
DebugLogger.navigation('Navigation state updated');
} catch (e) {
debugPrint('ERROR: Failed to update navigation state: $e');
DebugLogger.error('Failed to update navigation state', e);
}
}
@@ -167,9 +170,9 @@ class NavigationStateService {
_navigationStack.add(_currentState!);
}
await _saveNavigationState();
debugPrint('DEBUG: Navigation stack cleared');
DebugLogger.navigation('Navigation stack cleared');
} catch (e) {
debugPrint('ERROR: Failed to clear navigation stack: $e');
DebugLogger.error('Failed to clear navigation stack', e);
}
}
@@ -182,11 +185,11 @@ class NavigationStateService {
_stateNotifier.value = _currentState;
await _saveNavigationState();
debugPrint(
'DEBUG: Navigation stack replaced with ${newStack.length} states',
DebugLogger.navigation(
'Navigation stack replaced with ${newStack.length} states',
);
} catch (e) {
debugPrint('ERROR: Failed to replace navigation stack: $e');
DebugLogger.error('Failed to replace navigation stack', e);
}
}
@@ -219,9 +222,9 @@ class NavigationStateService {
await replaceStack([deepLinkState]);
}
debugPrint('DEBUG: Deep link handled - $routeName');
DebugLogger.navigation('Deep link handled - $routeName');
} catch (e) {
debugPrint('ERROR: Failed to handle deep link: $e');
DebugLogger.error('Failed to handle deep link', e);
}
}
@@ -275,8 +278,8 @@ class NavigationStateService {
if (_currentState != null) {
// Attempt to restore to the last known state
debugPrint(
'DEBUG: Restoring navigation to ${_currentState!.routeName}',
DebugLogger.navigation(
'Restoring navigation to ${_currentState!.routeName}',
);
// This would need to be implemented based on your routing setup
@@ -287,7 +290,7 @@ class NavigationStateService {
// );
}
} catch (e) {
debugPrint('ERROR: Failed to restore navigation state: $e');
DebugLogger.error('Failed to restore navigation state', e);
}
}
@@ -302,9 +305,9 @@ class NavigationStateService {
await _prefs?.remove(_currentStateKey);
await _prefs?.remove(_deepLinkStateKey);
debugPrint('DEBUG: All navigation state cleared');
DebugLogger.navigation('All navigation state cleared');
} catch (e) {
debugPrint('ERROR: Failed to clear navigation state: $e');
DebugLogger.error('Failed to clear navigation state', e);
}
}
@@ -329,7 +332,7 @@ class NavigationStateService {
await _prefs!.remove(_currentStateKey);
}
} catch (e) {
debugPrint('ERROR: Failed to save navigation state: $e');
DebugLogger.error('Failed to save navigation state', e);
}
}
@@ -359,11 +362,11 @@ class NavigationStateService {
_stateNotifier.value = _currentState;
}
debugPrint(
'DEBUG: Navigation state loaded - ${_navigationStack.length} states',
DebugLogger.navigation(
'Navigation state loaded - ${_navigationStack.length} states',
);
} catch (e) {
debugPrint('ERROR: Failed to load navigation state: $e');
DebugLogger.error('Failed to load navigation state', e);
// Clear corrupted state
await clearAll();
}
@@ -376,7 +379,7 @@ class NavigationStateService {
try {
await _prefs!.setString(_deepLinkStateKey, jsonEncode(state.toJson()));
} catch (e) {
debugPrint('ERROR: Failed to save deep link state: $e');
DebugLogger.error('Failed to save deep link state', e);
}
}

View File

@@ -4,9 +4,11 @@ import 'package:wakelock_plus/wakelock_plus.dart';
import 'package:dio/dio.dart';
import 'background_streaming_handler.dart';
import 'connectivity_service.dart';
import '../utils/debug_logger.dart';
class PersistentStreamingService with WidgetsBindingObserver {
static final PersistentStreamingService _instance = PersistentStreamingService._internal();
static final PersistentStreamingService _instance =
PersistentStreamingService._internal();
factory PersistentStreamingService() => _instance;
PersistentStreamingService._internal() {
_initialize();
@@ -17,25 +19,25 @@ class PersistentStreamingService with WidgetsBindingObserver {
final Map<String, StreamController> _streamControllers = {};
final Map<String, Function> _streamRecoveryCallbacks = {};
final Map<String, Map<String, dynamic>> _streamMetadata = {};
// App lifecycle state
// AppLifecycleState? _lastLifecycleState; // Removed as it's unused
bool _isInBackground = false;
Timer? _backgroundTimer;
Timer? _heartbeatTimer;
// Background streaming handler
late final BackgroundStreamingHandler _backgroundHandler;
// Connectivity monitoring
StreamSubscription<bool>? _connectivitySubscription;
bool _hasConnectivity = true;
// Recovery state
final Map<String, int> _retryAttempts = {};
static const int _maxRetryAttempts = 3;
static const Duration _retryDelay = Duration(seconds: 2);
void _initialize() {
WidgetsBinding.instance.addObserver(this);
_backgroundHandler = BackgroundStreamingHandler.instance;
@@ -43,48 +45,56 @@ class PersistentStreamingService with WidgetsBindingObserver {
_setupConnectivityMonitoring();
_startHeartbeat();
}
void _setupBackgroundHandlerCallbacks() {
_backgroundHandler.onStreamsSuspending = (streamIds) {
debugPrint('PersistentStreaming: Streams suspending - $streamIds');
DebugLogger.stream(
'PersistentStreaming: Streams suspending - $streamIds',
);
// Mark streams as suspended but don't close them yet
for (final streamId in streamIds) {
_markStreamAsSuspended(streamId);
}
};
_backgroundHandler.onBackgroundTaskExpiring = () {
debugPrint('PersistentStreaming: Background task expiring');
DebugLogger.stream('PersistentStreaming: Background task expiring');
// Save states and prepare for recovery
_saveStreamStatesForRecovery();
};
_backgroundHandler.shouldContinueInBackground = () {
return _activeStreams.isNotEmpty;
};
}
void _setupConnectivityMonitoring() {
// Create a connectivity service instance - this would normally be injected
// For now, create a temporary instance just for monitoring
final connectivityService = ConnectivityService(Dio());
_connectivitySubscription = connectivityService.isConnected.listen((connected) {
_connectivitySubscription = connectivityService.isConnected.listen((
connected,
) {
final wasConnected = _hasConnectivity;
_hasConnectivity = connected;
if (!wasConnected && connected) {
// Connectivity restored - try to recover streams
debugPrint('PersistentStreaming: Connectivity restored, recovering streams');
DebugLogger.stream(
'PersistentStreaming: Connectivity restored, recovering streams',
);
_recoverActiveStreams();
} else if (wasConnected && !connected) {
// Connectivity lost - mark streams as suspended
debugPrint('PersistentStreaming: Connectivity lost, suspending streams');
DebugLogger.stream(
'PersistentStreaming: Connectivity lost, suspending streams',
);
_suspendAllStreams();
}
});
}
void _startHeartbeat() {
_heartbeatTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (_activeStreams.isNotEmpty && _isInBackground) {
@@ -92,11 +102,11 @@ class PersistentStreamingService with WidgetsBindingObserver {
}
});
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// _lastLifecycleState = state; // Removed as it's unused
switch (state) {
case AppLifecycleState.paused:
case AppLifecycleState.inactive:
@@ -112,47 +122,49 @@ class PersistentStreamingService with WidgetsBindingObserver {
break;
}
}
void _onAppBackground() {
debugPrint('PersistentStreamingService: App went to background');
DebugLogger.stream('PersistentStreamingService: App went to background');
_isInBackground = true;
// Enable wake lock to prevent device sleep during streaming
if (_activeStreams.isNotEmpty) {
_enableWakeLock();
_startBackgroundExecution();
}
}
void _onAppForeground() {
debugPrint('PersistentStreamingService: App returned to foreground');
DebugLogger.stream(
'PersistentStreamingService: App returned to foreground',
);
_isInBackground = false;
// Cancel background timer
_backgroundTimer?.cancel();
_backgroundTimer = null;
// Disable wake lock if no active streams
if (_activeStreams.isEmpty) {
_disableWakeLock();
}
// Check and recover any interrupted streams
_recoverActiveStreams();
}
void _onAppDetached() {
debugPrint('PersistentStreamingService: App detached');
DebugLogger.stream('PersistentStreamingService: App detached');
// Save stream states for recovery
_saveStreamStatesForRecovery();
// Clean up
_backgroundTimer?.cancel();
_heartbeatTimer?.cancel();
_disableWakeLock();
}
// Register a stream for persistent handling
String registerStream({
required StreamSubscription subscription,
@@ -161,17 +173,17 @@ class PersistentStreamingService with WidgetsBindingObserver {
Map<String, dynamic>? metadata,
}) {
final streamId = DateTime.now().millisecondsSinceEpoch.toString();
_activeStreams[streamId] = subscription;
_streamControllers[streamId] = controller;
if (recoveryCallback != null) {
_streamRecoveryCallbacks[streamId] = recoveryCallback;
}
// Store metadata for recovery
if (metadata != null) {
_streamMetadata[streamId] = metadata;
// Register with background handler
_backgroundHandler.registerStream(
streamId,
@@ -182,22 +194,24 @@ class PersistentStreamingService with WidgetsBindingObserver {
lastContent: metadata['lastContent'],
);
}
// Enable wake lock when streaming starts
if (_activeStreams.length == 1) {
_enableWakeLock();
}
// Start background execution if app is backgrounded
if (_isInBackground) {
_startBackgroundExecution();
}
debugPrint('PersistentStreamingService: Registered stream $streamId');
DebugLogger.stream(
'PersistentStreamingService: Registered stream $streamId',
);
return streamId;
}
// Unregister a stream
void unregisterStream(String streamId) {
_activeStreams.remove(streamId);
@@ -205,31 +219,35 @@ class PersistentStreamingService with WidgetsBindingObserver {
_streamRecoveryCallbacks.remove(streamId);
_streamMetadata.remove(streamId);
_retryAttempts.remove(streamId);
// Unregister from background handler
_backgroundHandler.unregisterStream(streamId);
// Stop background execution if no more streams
if (_activeStreams.isEmpty) {
_backgroundHandler.stopBackgroundExecution([streamId]);
_disableWakeLock();
}
debugPrint('PersistentStreamingService: Unregistered stream $streamId');
DebugLogger.stream(
'PersistentStreamingService: Unregistered stream $streamId',
);
}
// Check if a stream is still active
bool isStreamActive(String streamId) {
return _activeStreams.containsKey(streamId);
}
// Recover interrupted streams
Future<void> _recoverActiveStreams() async {
if (!_hasConnectivity) {
debugPrint('PersistentStreaming: No connectivity, skipping recovery');
DebugLogger.stream(
'PersistentStreaming: No connectivity, skipping recovery',
);
return;
}
// First, try to recover from background handler saved states
final savedStates = await _backgroundHandler.recoverStreamStates();
for (final state in savedStates) {
@@ -237,12 +255,12 @@ class PersistentStreamingService with WidgetsBindingObserver {
await _recoverStreamFromState(state);
}
}
// Then check active streams for recovery
for (final entry in _streamRecoveryCallbacks.entries) {
final streamId = entry.key;
final recoveryCallback = entry.value;
// Check if stream was interrupted or needs recovery
final subscription = _activeStreams[streamId];
if (subscription == null || _needsRecovery(streamId)) {
@@ -250,69 +268,84 @@ class PersistentStreamingService with WidgetsBindingObserver {
}
}
}
Future<void> _recoverStreamFromState(StreamState state) async {
final recoveryCallback = _streamRecoveryCallbacks[state.streamId];
if (recoveryCallback != null) {
debugPrint('PersistentStreaming: Recovering stream from saved state: ${state.streamId}');
DebugLogger.stream(
'PersistentStreaming: Recovering stream from saved state: ${state.streamId}',
);
await _attemptStreamRecovery(state.streamId, recoveryCallback);
}
}
Future<void> _attemptStreamRecovery(String streamId, Function recoveryCallback) async {
Future<void> _attemptStreamRecovery(
String streamId,
Function recoveryCallback,
) async {
final attempts = _retryAttempts[streamId] ?? 0;
if (attempts >= _maxRetryAttempts) {
debugPrint('PersistentStreaming: Max retry attempts reached for stream $streamId');
DebugLogger.warning(
'PersistentStreaming: Max retry attempts reached for stream $streamId',
);
return;
}
debugPrint('PersistentStreaming: Recovering stream $streamId (attempt ${attempts + 1})');
DebugLogger.stream(
'PersistentStreaming: Recovering stream $streamId (attempt ${attempts + 1})',
);
try {
_retryAttempts[streamId] = attempts + 1;
// Add exponential backoff delay
if (attempts > 0) {
final delay = _retryDelay * (1 << (attempts - 1)); // 2s, 4s, 8s...
await Future.delayed(delay);
}
// Call recovery callback to restart the stream
await recoveryCallback();
// Reset retry count on success
_retryAttempts.remove(streamId);
} catch (e) {
debugPrint('PersistentStreaming: Failed to recover stream $streamId: $e');
DebugLogger.error(
'PersistentStreaming: Failed to recover stream $streamId',
e,
);
// Schedule next retry if under limit
if (_retryAttempts[streamId]! < _maxRetryAttempts) {
Timer(_retryDelay, () => _attemptStreamRecovery(streamId, recoveryCallback));
Timer(
_retryDelay,
() => _attemptStreamRecovery(streamId, recoveryCallback),
);
}
}
}
bool _needsRecovery(String streamId) {
final metadata = _streamMetadata[streamId];
if (metadata == null) return false;
// Check if stream has been inactive for too long
final lastUpdate = metadata['lastUpdate'] as DateTime?;
if (lastUpdate != null) {
final timeSinceUpdate = DateTime.now().difference(lastUpdate);
return timeSinceUpdate > const Duration(minutes: 1);
}
return false;
}
// Platform-specific background execution
void _startBackgroundExecution() {
if (_activeStreams.isNotEmpty) {
_backgroundHandler.startBackgroundExecution(_activeStreams.keys.toList());
}
}
void _markStreamAsSuspended(String streamId) {
final metadata = _streamMetadata[streamId];
if (metadata != null) {
@@ -320,20 +353,23 @@ class PersistentStreamingService with WidgetsBindingObserver {
metadata['suspendedAt'] = DateTime.now();
}
}
void _suspendAllStreams() {
for (final streamId in _activeStreams.keys) {
_markStreamAsSuspended(streamId);
}
}
void _saveStreamStatesForRecovery() {
// The background handler will handle the actual saving
debugPrint('PersistentStreaming: Saving ${_activeStreams.length} stream states for recovery');
DebugLogger.stream(
'PersistentStreaming: Saving ${_activeStreams.length} stream states for recovery',
);
}
// Update stream metadata when chunks are received
void updateStreamProgress(String streamId, {
void updateStreamProgress(
String streamId, {
int? chunkSequence,
String? content,
String? appendedContent,
@@ -345,54 +381,62 @@ class PersistentStreamingService with WidgetsBindingObserver {
content: content,
appendedContent: appendedContent,
);
// Update local metadata
final metadata = _streamMetadata[streamId];
if (metadata != null) {
metadata['lastUpdate'] = DateTime.now();
metadata['lastChunkSequence'] = chunkSequence ?? metadata['lastChunkSequence'];
metadata['lastChunkSequence'] =
chunkSequence ?? metadata['lastChunkSequence'];
if (appendedContent != null) {
metadata['lastContent'] = (metadata['lastContent'] ?? '') + appendedContent;
metadata['lastContent'] =
(metadata['lastContent'] ?? '') + appendedContent;
} else if (content != null) {
metadata['lastContent'] = content;
}
metadata['suspended'] = false; // Mark as active
}
}
// Wake lock management
void _enableWakeLock() async {
try {
await WakelockPlus.enable();
debugPrint('PersistentStreamingService: Wake lock enabled');
DebugLogger.stream('PersistentStreamingService: Wake lock enabled');
} catch (e) {
debugPrint('PersistentStreamingService: Failed to enable wake lock: $e');
DebugLogger.error(
'PersistentStreamingService: Failed to enable wake lock',
e,
);
}
}
void _disableWakeLock() async {
try {
await WakelockPlus.disable();
debugPrint('PersistentStreamingService: Wake lock disabled');
DebugLogger.stream('PersistentStreamingService: Wake lock disabled');
} catch (e) {
debugPrint('PersistentStreamingService: Failed to disable wake lock: $e');
DebugLogger.error(
'PersistentStreamingService: Failed to disable wake lock',
e,
);
}
}
// Get active stream count
int get activeStreamCount => _activeStreams.length;
// Get stream metadata
Map<String, dynamic>? getStreamMetadata(String streamId) {
return _streamMetadata[streamId];
}
// Check if stream is suspended
bool isStreamSuspended(String streamId) {
final metadata = _streamMetadata[streamId];
return metadata?['suspended'] == true;
}
// Force recovery of a specific stream
Future<void> forceRecoverStream(String streamId) async {
final recoveryCallback = _streamRecoveryCallbacks[streamId];
@@ -401,7 +445,7 @@ class PersistentStreamingService with WidgetsBindingObserver {
await _attemptStreamRecovery(streamId, recoveryCallback);
}
}
// Cleanup
void dispose() {
WidgetsBinding.instance.removeObserver(this);
@@ -409,18 +453,18 @@ class PersistentStreamingService with WidgetsBindingObserver {
_heartbeatTimer?.cancel();
_connectivitySubscription?.cancel();
_disableWakeLock();
// Stop all background execution
if (_activeStreams.isNotEmpty) {
_backgroundHandler.stopBackgroundExecution(_activeStreams.keys.toList());
}
// Cancel all active streams
for (final subscription in _activeStreams.values) {
subscription.cancel();
}
_activeStreams.clear();
// Close all controllers
for (final controller in _streamControllers.values) {
if (!controller.isClosed) {
@@ -428,13 +472,13 @@ class PersistentStreamingService with WidgetsBindingObserver {
}
}
_streamControllers.clear();
// Clear all metadata
_streamMetadata.clear();
_streamRecoveryCallbacks.clear();
_retryAttempts.clear();
// Clear background handler
_backgroundHandler.clearAll();
}
}
}

View File

@@ -1,8 +1,8 @@
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:crypto/crypto.dart';
import '../utils/debug_logger.dart';
/// Enhanced secure credential storage with platform-specific optimizations
class SecureCredentialStorage {
@@ -73,9 +73,9 @@ class SecureCredentialStorage {
);
}
debugPrint('DEBUG: Credentials saved and verified securely');
DebugLogger.storage('Credentials saved and verified securely');
} catch (e) {
debugPrint('ERROR: Failed to save credentials: $e');
DebugLogger.error('Failed to save credentials', e);
rethrow;
}
}
@@ -92,7 +92,7 @@ class SecureCredentialStorage {
final decoded = jsonDecode(jsonString);
if (decoded is! Map<String, dynamic>) {
debugPrint('Warning: Invalid credentials format');
DebugLogger.warning('Invalid credentials format');
await deleteSavedCredentials();
return null;
}
@@ -103,8 +103,8 @@ class SecureCredentialStorage {
final currentDeviceId = await _getDeviceFingerprint();
if (savedDeviceId != currentDeviceId) {
debugPrint(
'Info: Device fingerprint changed, but allowing credential access for better UX',
DebugLogger.info(
'Device fingerprint changed, but allowing credential access for better UX',
);
// Don't clear credentials immediately - allow the user to continue
// They can re-login if needed, which will update the fingerprint
@@ -115,8 +115,8 @@ class SecureCredentialStorage {
if (!decoded.containsKey('serverId') ||
!decoded.containsKey('username') ||
!decoded.containsKey('password')) {
debugPrint(
'Warning: Invalid saved credentials format - missing required fields',
DebugLogger.warning(
'Invalid saved credentials format - missing required fields',
);
await deleteSavedCredentials();
return null;
@@ -132,12 +132,12 @@ class SecureCredentialStorage {
// Warn if credentials are very old (but don't delete them)
if (daysSinceCreated > 90) {
debugPrint(
'Info: Saved credentials are $daysSinceCreated days old',
DebugLogger.info(
'Saved credentials are $daysSinceCreated days old',
);
}
} catch (e) {
debugPrint('Warning: Could not parse savedAt timestamp: $e');
DebugLogger.warning('Could not parse savedAt timestamp: $e');
}
}
@@ -148,7 +148,7 @@ class SecureCredentialStorage {
'savedAt': decoded['savedAt']?.toString() ?? '',
};
} catch (e) {
debugPrint('ERROR: Failed to retrieve credentials: $e');
DebugLogger.error('Failed to retrieve credentials', e);
// Don't delete credentials on retrieval errors - they might be recoverable
return null;
}
@@ -158,9 +158,9 @@ class SecureCredentialStorage {
Future<void> deleteSavedCredentials() async {
try {
await _secureStorage.delete(key: _credentialsKey);
debugPrint('DEBUG: Credentials deleted');
DebugLogger.storage('Credentials deleted');
} catch (e) {
debugPrint('ERROR: Failed to delete credentials: $e');
DebugLogger.error('Failed to delete credentials', e);
}
}
@@ -170,7 +170,7 @@ class SecureCredentialStorage {
final encryptedToken = await _encryptData(token);
await _secureStorage.write(key: _authTokenKey, value: encryptedToken);
} catch (e) {
debugPrint('ERROR: Failed to save auth token: $e');
DebugLogger.error('Failed to save auth token', e);
rethrow;
}
}
@@ -183,7 +183,7 @@ class SecureCredentialStorage {
return await _decryptData(encryptedToken);
} catch (e) {
debugPrint('ERROR: Failed to retrieve auth token: $e');
DebugLogger.error('Failed to retrieve auth token', e);
return null;
}
}
@@ -193,7 +193,7 @@ class SecureCredentialStorage {
try {
await _secureStorage.delete(key: _authTokenKey);
} catch (e) {
debugPrint('ERROR: Failed to delete auth token: $e');
DebugLogger.error('Failed to delete auth token', e);
}
}
@@ -206,7 +206,7 @@ class SecureCredentialStorage {
value: encryptedConfigs,
);
} catch (e) {
debugPrint('ERROR: Failed to save server configs: $e');
DebugLogger.error('Failed to save server configs', e);
rethrow;
}
}
@@ -221,7 +221,7 @@ class SecureCredentialStorage {
return await _decryptData(encryptedConfigs);
} catch (e) {
debugPrint('ERROR: Failed to retrieve server configs: $e');
DebugLogger.error('Failed to retrieve server configs', e);
return null;
}
}
@@ -239,7 +239,7 @@ class SecureCredentialStorage {
return result == testValue;
} catch (e) {
debugPrint('WARNING: Secure storage not available: $e');
DebugLogger.warning('Secure storage not available: $e');
return false;
}
}
@@ -248,9 +248,9 @@ class SecureCredentialStorage {
Future<void> clearAll() async {
try {
await _secureStorage.deleteAll();
debugPrint('DEBUG: All secure data cleared');
DebugLogger.storage('All secure data cleared');
} catch (e) {
debugPrint('ERROR: Failed to clear secure data: $e');
DebugLogger.error('Failed to clear secure data', e);
}
}
@@ -261,7 +261,7 @@ class SecureCredentialStorage {
// In a more advanced implementation, you could add an additional layer of AES encryption
return data;
} catch (e) {
debugPrint('ERROR: Failed to encrypt data: $e');
DebugLogger.error('Failed to encrypt data', e);
rethrow;
}
}
@@ -273,7 +273,7 @@ class SecureCredentialStorage {
// This matches the encryption method above
return encryptedData;
} catch (e) {
debugPrint('ERROR: Failed to decrypt data: $e');
DebugLogger.error('Failed to decrypt data', e);
rethrow;
}
}
@@ -297,7 +297,7 @@ class SecureCredentialStorage {
return digest.toString();
} catch (e) {
debugPrint('WARNING: Failed to generate device fingerprint: $e');
DebugLogger.warning('Failed to generate device fingerprint: $e');
// Return a consistent fallback fingerprint
return 'stable_fallback_device_id';
}
@@ -315,11 +315,11 @@ class SecureCredentialStorage {
username: oldCredentials['username'] ?? '',
password: oldCredentials['password'] ?? '',
);
debugPrint(
'DEBUG: Successfully migrated credentials to new secure format',
DebugLogger.storage(
'Successfully migrated credentials to new secure format',
);
} catch (e) {
debugPrint('ERROR: Failed to migrate credentials: $e');
DebugLogger.error('Failed to migrate credentials', e);
}
}
}

View File

@@ -0,0 +1,73 @@
import 'package:flutter/foundation.dart';
/// Centralized debug logging utility for the entire app
class DebugLogger {
static const bool _enabled = kDebugMode;
/// Log debug information
static void log(String message) {
if (_enabled) {
debugPrint('🔍 $message');
}
}
/// Log errors
static void error(String message, [Object? error]) {
if (_enabled) {
if (error != null) {
debugPrint('$message: $error');
} else {
debugPrint('$message');
}
}
}
/// Log warnings
static void warning(String message) {
if (_enabled) {
debugPrint('⚠️ $message');
}
}
/// Log success/info messages
static void info(String message) {
if (_enabled) {
debugPrint(' $message');
}
}
/// Log navigation events
static void navigation(String message) {
if (_enabled) {
debugPrint('🧭 $message');
}
}
/// Log authentication events
static void auth(String message) {
if (_enabled) {
debugPrint('🔐 $message');
}
}
/// Log streaming events
static void stream(String message) {
if (_enabled) {
debugPrint('📡 $message');
}
}
/// Log validation events
static void validation(String message) {
if (_enabled) {
debugPrint('$message');
}
}
/// Log storage events
static void storage(String message) {
if (_enabled) {
debugPrint('💾 $message');
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter/foundation.dart';
import 'debug_logger.dart';
/// Utility class for parsing and extracting reasoning/thinking content from messages
class ReasoningParser {
@@ -7,21 +8,21 @@ class ReasoningParser {
if (content.isEmpty) return null;
if (kDebugMode) {
debugPrint(
'DEBUG: Parsing content: ${content.substring(0, content.length > 200 ? 200 : content.length)}...',
DebugLogger.log(
'Parsing content: ${content.substring(0, content.length > 200 ? 200 : content.length)}...',
);
}
// Check if content contains reasoning
if (!content.contains('<details type="reasoning"')) {
if (kDebugMode) {
debugPrint('DEBUG: No reasoning content found in text');
DebugLogger.log('No reasoning content found in text');
}
return null;
}
if (kDebugMode) {
debugPrint('DEBUG: Found reasoning tags in content');
DebugLogger.log('Found reasoning tags in content');
}
// Match the <details> tag with type="reasoning"
@@ -45,18 +46,18 @@ class ReasoningParser {
final flexMatch = flexRegex.firstMatch(content);
if (flexMatch != null) {
if (kDebugMode) {
debugPrint('DEBUG: Found flexible match: ${flexMatch.group(0)}');
DebugLogger.log('Found flexible match: ${flexMatch.group(0)}');
}
} else {
if (kDebugMode) {
debugPrint('DEBUG: No flexible match found either');
DebugLogger.log('No flexible match found either');
}
}
return null;
}
if (kDebugMode) {
debugPrint('DEBUG: Regex matched successfully');
DebugLogger.log('Regex matched successfully');
}
final isDone = match.group(1) == 'true';
@@ -65,10 +66,10 @@ class ReasoningParser {
final reasoning = match.group(4)?.trim() ?? '';
if (kDebugMode) {
debugPrint(
'DEBUG: Parsed values - isDone: $isDone, duration: $duration, summary: $summary',
DebugLogger.log(
'Parsed values - isDone: $isDone, duration: $duration, summary: $summary',
);
debugPrint('DEBUG: Reasoning content length: ${reasoning.length}');
DebugLogger.log('Reasoning content length: ${reasoning.length}');
}
// Remove the reasoning section from the main content

View File

@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'schema_registry.dart';
import 'validation_result.dart';
import 'field_mapper.dart';
import '../utils/debug_logger.dart';
/// Comprehensive API request and response validator
/// Validates against OpenAPI specification schemas
@@ -24,9 +24,9 @@ class ApiValidator {
try {
await _schemaRegistry.loadSchemas();
_initialized = true;
debugPrint('ApiValidator: Successfully initialized with schemas');
DebugLogger.validation('Successfully initialized with schemas');
} catch (e) {
debugPrint('ApiValidator: Failed to initialize: $e');
DebugLogger.error('Failed to initialize', e);
// Continue without validation if schemas can't be loaded
}
}

View File

@@ -1,4 +1,4 @@
import 'package:flutter/foundation.dart';
import '../utils/debug_logger.dart';
/// Handles field name transformations between API and client formats
/// Converts between snake_case (API) and camelCase (client)
@@ -224,7 +224,7 @@ class FieldMapper {
void clearCache() {
_toCamelCaseCache.clear();
_toSnakeCaseCache.clear();
debugPrint('FieldMapper: Cleared transformation caches');
DebugLogger.validation('Cleared transformation caches');
}
/// Add custom field mapping
@@ -236,7 +236,7 @@ class FieldMapper {
_toCamelCaseCache.remove(apiField);
_toSnakeCaseCache.remove(clientField);
debugPrint('FieldMapper: Added custom mapping: $apiField <-> $clientField');
DebugLogger.validation('Added custom mapping: $apiField <-> $clientField');
}
/// Validate that field transformations are reversible
@@ -266,14 +266,14 @@ class FieldMapper {
}
if (errors.isNotEmpty) {
debugPrint('FieldMapper: Transformation validation errors:');
DebugLogger.error('Transformation validation errors:');
for (final error in errors) {
debugPrint(' $error');
DebugLogger.error(' $error');
}
return false;
}
debugPrint('FieldMapper: All transformations validated successfully');
DebugLogger.validation('All transformations validated successfully');
return true;
}
}

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import '../utils/debug_logger.dart';
/// Registry for OpenAPI schemas
/// Loads and provides access to request/response schemas for validation
@@ -19,15 +19,15 @@ class SchemaRegistry {
/// Load schemas from OpenAPI specification
Future<void> loadSchemas() async {
try {
debugPrint('SchemaRegistry: Loading OpenAPI specification...');
DebugLogger.validation('Loading OpenAPI specification...');
// Try to load from assets first, then from file system as fallback
String openApiContent;
try {
openApiContent = await rootBundle.loadString('assets/openapi.json');
} catch (e) {
debugPrint(
'SchemaRegistry: Could not load from assets, trying file system...',
DebugLogger.warning(
'Could not load from assets, trying file system...',
);
// Fallback - in a real app you might load from network or local file
throw Exception('OpenAPI specification not found in assets');
@@ -35,14 +35,14 @@ class SchemaRegistry {
_openApiSpec = jsonDecode(openApiContent) as Map<String, dynamic>;
debugPrint(
'SchemaRegistry: Successfully loaded OpenAPI spec with ${_getPaths().length} paths',
DebugLogger.validation(
'Successfully loaded OpenAPI spec with ${_getPaths().length} paths',
);
// Pre-process and cache commonly used schemas
await _buildSchemaCache();
} catch (e) {
debugPrint('SchemaRegistry: Failed to load schemas: $e');
DebugLogger.error('Failed to load schemas', e);
rethrow;
}
}
@@ -86,8 +86,9 @@ class SchemaRegistry {
return schema;
} catch (e) {
debugPrint(
'SchemaRegistry: Error getting request schema for $method $endpoint: $e',
DebugLogger.error(
'Error getting request schema for $method $endpoint',
e,
);
return null;
}
@@ -146,8 +147,9 @@ class SchemaRegistry {
return schema;
} catch (e) {
debugPrint(
'SchemaRegistry: Error getting response schema for $method $endpoint ($code): $e',
DebugLogger.error(
'Error getting response schema for $method $endpoint ($code)',
e,
);
return null;
}
@@ -240,7 +242,7 @@ class SchemaRegistry {
/// Resolve $ref reference
Map<String, dynamic>? _resolveReference(String ref) {
if (!ref.startsWith('#/')) {
debugPrint('SchemaRegistry: External references not supported: $ref');
DebugLogger.warning('External references not supported: $ref');
return null;
}
@@ -251,7 +253,7 @@ class SchemaRegistry {
if (current is Map<String, dynamic> && current.containsKey(segment)) {
current = current[segment];
} else {
debugPrint('SchemaRegistry: Could not resolve reference: $ref');
DebugLogger.warning('Could not resolve reference: $ref');
return null;
}
}
@@ -329,9 +331,7 @@ class SchemaRegistry {
}
}
debugPrint(
'SchemaRegistry: Pre-cached schemas for $cachedCount operations',
);
DebugLogger.validation('Pre-cached schemas for $cachedCount operations');
}
/// Get all available endpoints

View File

@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'api_validator.dart';
import 'validation_result.dart';
import '../utils/debug_logger.dart';
/// Dio interceptor for automatic API validation
/// Validates requests and responses against OpenAPI schemas
@@ -61,7 +62,7 @@ class ValidationInterceptor extends Interceptor {
);
return;
} else {
debugPrint('ValidationInterceptor: Request validation error: $e');
DebugLogger.error('Request validation error', e);
}
}
}
@@ -120,7 +121,7 @@ class ValidationInterceptor extends Interceptor {
);
return;
} else {
debugPrint('ValidationInterceptor: Response validation error: $e');
DebugLogger.error('Response validation error', e);
}
}
}
@@ -165,9 +166,7 @@ class ValidationInterceptor extends Interceptor {
err.response!.extra['validationResult'] = result;
}
} catch (e) {
debugPrint(
'ValidationInterceptor: Error response validation failed: $e',
);
DebugLogger.error('Error response validation failed', e);
}
}
@@ -187,21 +186,21 @@ class ValidationInterceptor extends Interceptor {
final statusText = statusCode != null ? ' ($statusCode)' : '';
final icon = result.isValid ? '' : '';
debugPrint(
DebugLogger.validation(
'$icon Validation $type: ${method.toUpperCase()} $path$statusText - ${result.status.name}',
);
if (result.hasErrors) {
debugPrint(' Errors: ${result.errors.join(', ')}');
DebugLogger.error(' Errors: ${result.errors.join(', ')}');
}
if (result.hasWarnings) {
debugPrint(' Warnings: ${result.warnings.join(', ')}');
DebugLogger.warning(' Warnings: ${result.warnings.join(', ')}');
}
if (result.message.isNotEmpty &&
result.status != ValidationStatus.success) {
debugPrint(' Message: ${result.message}');
DebugLogger.info(' Message: ${result.message}');
}
}