diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 01e9f4b..6196ca2 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -63,6 +63,7 @@ android { } getByName("debug") { // signingConfig = signingConfigs.getByName("debug") + applicationIdSuffix = ".debug" } } } diff --git a/lib/core/providers/app_providers.dart b/lib/core/providers/app_providers.dart index 45c3a20..e76437c 100644 --- a/lib/core/providers/app_providers.dart +++ b/lib/core/providers/app_providers.dart @@ -431,6 +431,12 @@ class _ConversationsCacheTimestampNotifier extends Notifier { // Conversation providers - Now using correct OpenWebUI API with caching final conversationsProvider = FutureProvider>((ref) async { + // Do not fetch protected data until authenticated + final authed = ref.read(isAuthenticatedProvider2); + if (!authed) { + DebugLogger.log('skip-unauthed', scope: 'conversations'); + return []; + } // Check if we have a recent cache (within 5 seconds) final lastFetch = ref.read(_conversationsCacheTimestamp); if (lastFetch != null && DateTime.now().difference(lastFetch).inSeconds < 5) { @@ -1228,6 +1234,11 @@ final webSearchAvailableProvider = Provider((ref) { // Folders provider final foldersProvider = FutureProvider>((ref) async { + // Protected: require authentication + if (!ref.read(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'folders'); + return []; + } final api = ref.watch(apiServiceProvider); if (api == null) { DebugLogger.warning('api-missing', scope: 'folders'); @@ -1253,6 +1264,11 @@ final foldersProvider = FutureProvider>((ref) async { // Files provider final userFilesProvider = FutureProvider>((ref) async { + // Protected: require authentication + if (!ref.read(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'files'); + return []; + } final api = ref.watch(apiServiceProvider); if (api == null) return []; @@ -1270,6 +1286,11 @@ final fileContentProvider = FutureProvider.family(( ref, fileId, ) async { + // Protected: require authentication + if (!ref.read(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'files/content'); + throw Exception('Not authenticated'); + } final api = ref.watch(apiServiceProvider); if (api == null) throw Exception('No API service available'); @@ -1288,6 +1309,11 @@ final fileContentProvider = FutureProvider.family(( // Knowledge Base providers final knowledgeBasesProvider = FutureProvider>((ref) async { + // Protected: require authentication + if (!ref.read(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'knowledge'); + return []; + } final api = ref.watch(apiServiceProvider); if (api == null) return []; @@ -1302,6 +1328,11 @@ final knowledgeBasesProvider = FutureProvider>((ref) async { final knowledgeBaseItemsProvider = FutureProvider.family, String>((ref, kbId) async { + // Protected: require authentication + if (!ref.read(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'knowledge/items'); + return []; + } final api = ref.watch(apiServiceProvider); if (api == null) return []; @@ -1322,6 +1353,11 @@ final knowledgeBaseItemsProvider = // Audio providers final availableVoicesProvider = FutureProvider>((ref) async { + // Protected: require authentication + if (!ref.read(isAuthenticatedProvider2)) { + DebugLogger.log('skip-unauthed', scope: 'voices'); + return []; + } final api = ref.watch(apiServiceProvider); if (api == null) return []; diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index ea68fda..a20858e 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -60,7 +60,9 @@ class RouterNotifier extends ChangeNotifier { } if (activeServerAsync.isLoading) { - // Avoid redirect loops by keeping splash during server loading + // Avoid redirect loops: do not override explicit auth routes while loading + if (_isAuthLocation(location)) return null; + // Keep splash during server loading otherwise return location == Routes.splash ? null : Routes.splash; } @@ -80,7 +82,9 @@ class RouterNotifier extends ChangeNotifier { final authState = ref.read(authNavigationStateProvider); switch (authState) { case AuthNavigationState.loading: - // Keep splash while establishing session + // Keep user on auth routes while loading to prevent bounce + if (_isAuthLocation(location)) return null; + // Otherwise keep splash during session establishment return location == Routes.splash ? null : Routes.splash; case AuthNavigationState.needsLogin: case AuthNavigationState.error: @@ -146,10 +150,9 @@ final goRouterProvider = Provider((ref) { name: RouteNames.authentication, builder: (context, state) { final config = state.extra; - if (config is! ServerConfig) { - return const ServerConnectionPage(); - } - return AuthenticationPage(serverConfig: config); + return AuthenticationPage( + serverConfig: config is ServerConfig ? config : null, + ); }, ), GoRoute( diff --git a/lib/core/services/connectivity_service.dart b/lib/core/services/connectivity_service.dart index 40910d0..976f2b3 100644 --- a/lib/core/services/connectivity_service.dart +++ b/lib/core/services/connectivity_service.dart @@ -33,14 +33,13 @@ class ConnectivityService { bool get isCurrentlyConnected => _lastStatus == ConnectivityStatus.online; void _startConnectivityMonitoring() { - // Initial check after a brief delay to avoid showing offline during startup - Timer(const Duration(milliseconds: 800), () { - _checkConnectivity(); - }); - - // Check periodically; interval adapts to recent failures - _connectivityTimer = Timer.periodic(_interval, (_) { - _checkConnectivity(); + // Initial check after a slightly longer delay to avoid competing with + // first-frame work. Start periodic checks only after the first probe. + Timer(const Duration(milliseconds: 1200), () async { + await _checkConnectivity(); + _connectivityTimer = Timer.periodic(_interval, (_) { + _checkConnectivity(); + }); }); } diff --git a/lib/features/auth/views/authentication_page.dart b/lib/features/auth/views/authentication_page.dart index 06aff32..ccf968a 100644 --- a/lib/features/auth/views/authentication_page.dart +++ b/lib/features/auth/views/authentication_page.dart @@ -20,9 +20,9 @@ import 'package:conduit/l10n/app_localizations.dart'; import '../providers/unified_auth_providers.dart'; class AuthenticationPage extends ConsumerStatefulWidget { - final ServerConfig serverConfig; + final ServerConfig? serverConfig; - const AuthenticationPage({super.key, required this.serverConfig}); + const AuthenticationPage({super.key, this.serverConfig}); @override ConsumerState createState() => _AuthenticationPageState(); @@ -234,6 +234,18 @@ class _AuthenticationPageState extends ConsumerState { } Widget _buildServerStatus() { + // Prefer route-provided config; otherwise fall back to active server + final activeServerAsync = ref.watch(activeServerProvider); + final cfg = + widget.serverConfig ?? + activeServerAsync.maybeWhen(data: (s) => s, orElse: () => null); + final hostText = () { + try { + final url = cfg?.url; + if (url != null && url.isNotEmpty) return Uri.parse(url).host; + } catch (_) {} + return 'Server'; + }(); return ConduitCard( isElevated: false, padding: const EdgeInsets.all(Spacing.lg), @@ -272,7 +284,7 @@ class _AuthenticationPageState extends ConsumerState { ), const SizedBox(height: Spacing.xs), Text( - Uri.parse(widget.serverConfig.url).host, + hostText, style: context.conduitTheme.bodySmall?.copyWith( color: context.conduitTheme.textSecondary, fontFamily: 'monospace',