diff --git a/apps/ui/src/components/views/login-view.tsx b/apps/ui/src/components/views/login-view.tsx index c619f1f2..0bcfbece 100644 --- a/apps/ui/src/components/views/login-view.tsx +++ b/apps/ui/src/components/views/login-view.tsx @@ -3,17 +3,26 @@ * * Prompts user to enter the API key shown in server console. * On successful login, sets an HTTP-only session cookie. + * + * On mount, verifies if an existing session is valid using exponential backoff. + * This handles cases where server live reloads kick users back to login + * even though their session is still valid. */ -import { useState } from 'react'; +import { useState, useEffect, useRef } from 'react'; import { useNavigate } from '@tanstack/react-router'; -import { login } from '@/lib/http-api-client'; +import { login, verifySession } from '@/lib/http-api-client'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { KeyRound, AlertCircle, Loader2 } from 'lucide-react'; import { useAuthStore } from '@/store/auth-store'; import { useSetupStore } from '@/store/setup-store'; +/** + * Delay helper for exponential backoff + */ +const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + export function LoginView() { const navigate = useNavigate(); const setAuthState = useAuthStore((s) => s.setAuthState); @@ -21,6 +30,45 @@ export function LoginView() { const [apiKey, setApiKey] = useState(''); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [isCheckingSession, setIsCheckingSession] = useState(true); + const sessionCheckRef = useRef(false); + + // Check for existing valid session on mount with exponential backoff + useEffect(() => { + // Prevent duplicate checks in strict mode + if (sessionCheckRef.current) return; + sessionCheckRef.current = true; + + const checkExistingSession = async () => { + const maxRetries = 5; + const baseDelay = 500; // Start with 500ms + + for (let attempt = 0; attempt < maxRetries; attempt++) { + try { + const isValid = await verifySession(); + if (isValid) { + // Session is valid, redirect to the main app + setAuthState({ isAuthenticated: true, authChecked: true }); + navigate({ to: setupComplete ? '/' : '/setup' }); + return; + } + // Session is invalid, no need to retry - show login form + break; + } catch { + // Network error or server not ready, retry with exponential backoff + if (attempt < maxRetries - 1) { + const waitTime = baseDelay * Math.pow(2, attempt); // 500, 1000, 2000, 4000, 8000ms + await delay(waitTime); + } + } + } + + // Session check complete (either invalid or all retries exhausted) + setIsCheckingSession(false); + }; + + checkExistingSession(); + }, [navigate, setAuthState, setupComplete]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -45,6 +93,18 @@ export function LoginView() { } }; + // Show loading state while checking existing session + if (isCheckingSession) { + return ( +
+
+ +

Checking session...

+
+
+ ); + } + return (
diff --git a/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx b/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx index e0261e97..e6ab828e 100644 --- a/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx +++ b/apps/ui/src/components/views/settings-view/api-keys/api-keys-section.tsx @@ -1,7 +1,7 @@ import { useAppStore } from '@/store/app-store'; import { useSetupStore } from '@/store/setup-store'; import { Button } from '@/components/ui/button'; -import { Key, CheckCircle2, Settings, Trash2, Loader2 } from 'lucide-react'; +import { Key, CheckCircle2, Trash2, Loader2 } from 'lucide-react'; import { ApiKeyField } from './api-key-field'; import { buildProviderConfigs } from '@/config/api-providers'; import { SecurityNotice } from './security-notice'; @@ -10,13 +10,11 @@ import { cn } from '@/lib/utils'; import { useState, useCallback } from 'react'; import { getElectronAPI } from '@/lib/electron'; import { toast } from 'sonner'; -import { useNavigate } from '@tanstack/react-router'; export function ApiKeysSection() { const { apiKeys, setApiKeys } = useAppStore(); - const { claudeAuthStatus, setClaudeAuthStatus, setSetupComplete } = useSetupStore(); + const { claudeAuthStatus, setClaudeAuthStatus } = useSetupStore(); const [isDeletingAnthropicKey, setIsDeletingAnthropicKey] = useState(false); - const navigate = useNavigate(); const { providerConfigParams, handleSave, saved } = useApiKeyManagement(); @@ -51,12 +49,6 @@ export function ApiKeysSection() { } }, [apiKeys, setApiKeys, claudeAuthStatus, setClaudeAuthStatus]); - // Open setup wizard - const openSetupWizard = useCallback(() => { - setSetupComplete(false); - navigate({ to: '/setup' }); - }, [setSetupComplete, navigate]); - return (
- - {apiKeys.anthropic && (