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