Merge pull request #232 from coleam00/fix/supabase-key-validation-and-state-consolidation

Fix Supabase key validation and consolidate frontend state management
This commit is contained in:
Wirasm
2025-08-18 21:19:27 +03:00
committed by GitHub
19 changed files with 854 additions and 317 deletions

View File

@@ -22,4 +22,4 @@ COPY . .
EXPOSE 5173
# Start Vite dev server with host binding for Docker
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]

View File

@@ -0,0 +1,74 @@
import React from 'react';
import { AlertCircle, Terminal, RefreshCw } from 'lucide-react';
export const BackendStartupError: React.FC = () => {
const handleRetry = () => {
// Reload the page to retry
window.location.reload();
};
return (
<div className="fixed inset-0 z-[10000] bg-black/90 backdrop-blur-sm flex items-center justify-center p-8">
<div className="max-w-2xl w-full">
<div className="bg-red-950/50 border-2 border-red-500/50 rounded-lg p-8 shadow-2xl backdrop-blur-md">
<div className="flex items-start gap-4">
<AlertCircle className="w-8 h-8 text-red-500 flex-shrink-0 mt-1" />
<div className="space-y-4 flex-1">
<h2 className="text-2xl font-bold text-red-100">
Backend Service Startup Failure
</h2>
<p className="text-red-200">
The Archon backend service failed to start. This is typically due to a configuration issue.
</p>
<div className="bg-black/50 rounded-lg p-4 border border-red-900/50">
<div className="flex items-center gap-2 mb-3 text-red-300">
<Terminal className="w-5 h-5" />
<span className="font-semibold">Check Docker Logs</span>
</div>
<p className="text-red-100 font-mono text-sm mb-3">
Check the <span className="text-red-400 font-bold">Archon-Server</span> logs in Docker Desktop for detailed error information.
</p>
<div className="space-y-2 text-xs text-red-300">
<p>1. Open Docker Desktop</p>
<p>2. Go to Containers tab</p>
<p>3. Click on <span className="text-red-400 font-semibold">Archon-Server</span></p>
<p>4. View the logs for the specific error message</p>
</div>
</div>
<div className="bg-yellow-950/30 border border-yellow-700/30 rounded-lg p-3">
<p className="text-yellow-200 text-sm">
<strong>Common issue:</strong> Using an ANON key instead of SERVICE key in your .env file
</p>
</div>
<div className="pt-4 border-t border-red-900/30">
<p className="text-red-300 text-sm">
After fixing the issue in your .env file, recreate the Docker containers:
</p>
<code className="block mt-2 p-2 bg-black/70 rounded text-red-100 font-mono text-sm">
docker compose down && docker compose up -d
</code>
<p className="text-red-300 text-xs mt-2">
Note: Use 'down' and 'up', not 'restart' - containers need to be recreated to load new environment variables
</p>
</div>
<div className="flex justify-center pt-4">
<button
onClick={handleRetry}
className="flex items-center gap-2 px-4 py-2 bg-red-600/20 hover:bg-red-600/30 border border-red-500/50 rounded-lg text-red-100 transition-colors"
>
<RefreshCw className="w-4 h-4" />
Retry Connection
</button>
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View File

@@ -6,6 +6,7 @@ import { X } from 'lucide-react';
import { useToast } from '../../contexts/ToastContext';
import { credentialsService } from '../../services/credentialsService';
import { isLmConfigured } from '../../utils/onboarding';
import { BackendStartupError } from '../BackendStartupError';
/**
* Props for the MainLayout component
*/
@@ -29,13 +30,14 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
const navigate = useNavigate();
const location = useLocation();
const [backendReady, setBackendReady] = useState(false);
const [backendStartupFailed, setBackendStartupFailed] = useState(false);
// Check backend readiness
useEffect(() => {
const checkBackendHealth = async (retryCount = 0) => {
const maxRetries = 10; // Increased retries for initialization
const retryDelay = 1000;
const maxRetries = 3; // 3 retries total
const retryDelay = 1500; // 1.5 seconds between retries
try {
// Create AbortController for proper timeout handling
@@ -58,6 +60,7 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
if (healthData.ready === true) {
console.log('✅ Backend is fully initialized');
setBackendReady(true);
setBackendStartupFailed(false);
} else {
// Backend is starting up but not ready yet
console.log(`🔄 Backend initializing... (attempt ${retryCount + 1}/${maxRetries}):`, healthData.message || 'Loading credentials...');
@@ -66,9 +69,10 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
if (retryCount < maxRetries) {
setTimeout(() => {
checkBackendHealth(retryCount + 1);
}, retryDelay); // Constant 1s retry during initialization
}, retryDelay); // Constant 1.5s retry during initialization
} else {
console.warn('Backend initialization taking too long - skipping credential check');
console.warn('Backend initialization taking too long - proceeding anyway');
// Don't mark as failed yet, just not fully ready
setBackendReady(false);
}
}
@@ -80,7 +84,10 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
const errorMessage = error instanceof Error
? (error.name === 'AbortError' ? 'Request timeout (5s)' : error.message)
: 'Unknown error';
console.log(`Backend not ready yet (attempt ${retryCount + 1}/${maxRetries}):`, errorMessage);
// Only log after first attempt to reduce noise during normal startup
if (retryCount > 0) {
console.log(`Backend not ready yet (attempt ${retryCount + 1}/${maxRetries}):`, errorMessage);
}
// Retry if we haven't exceeded max retries
if (retryCount < maxRetries) {
@@ -88,8 +95,9 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
checkBackendHealth(retryCount + 1);
}, retryDelay * Math.pow(1.5, retryCount)); // Exponential backoff for connection errors
} else {
console.warn('Backend not ready after maximum retries - skipping credential check');
console.error('Backend startup failed after maximum retries - showing error message');
setBackendReady(false);
setBackendStartupFailed(true);
}
}
};
@@ -99,11 +107,16 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
setTimeout(() => {
checkBackendHealth();
}, 1000); // Wait 1 second for initial app startup
}, [showToast, navigate]); // Removed backendReady from dependencies to prevent double execution
}, []); // Empty deps - only run once on mount
// Check for onboarding redirect after backend is ready
useEffect(() => {
const checkOnboarding = async () => {
// Skip if backend failed to start
if (backendStartupFailed) {
return;
}
// Skip if not ready, already on onboarding, or already dismissed
if (!backendReady || location.pathname === '/onboarding') {
return;
@@ -152,9 +165,12 @@ export const MainLayout: React.FC<MainLayoutProps> = ({
};
checkOnboarding();
}, [backendReady, location.pathname, navigate, showToast]);
}, [backendReady, backendStartupFailed, location.pathname, navigate, showToast]);
return <div className="relative min-h-screen bg-white dark:bg-black overflow-hidden">
{/* Show backend startup error if backend failed to start */}
{backendStartupFailed && <BackendStartupError />}
{/* Fixed full-page background grid that doesn't scroll */}
<div className="fixed inset-0 neon-grid pointer-events-none z-0"></div>
{/* Floating Navigation */}

View File

@@ -1,10 +1,10 @@
import { useState } from 'react';
import { Key, ExternalLink, Save, Loader } from 'lucide-react';
import { Input } from '../ui/Input';
import { Button } from '../ui/Button';
import { Select } from '../ui/Select';
import { useToast } from '../../contexts/ToastContext';
import { credentialsService } from '../../services/credentialsService';
import { useState } from "react";
import { Key, ExternalLink, Save, Loader } from "lucide-react";
import { Input } from "../ui/Input";
import { Button } from "../ui/Button";
import { Select } from "../ui/Select";
import { useToast } from "../../contexts/ToastContext";
import { credentialsService } from "../../services/credentialsService";
interface ProviderStepProps {
onSaved: () => void;
@@ -12,14 +12,14 @@ interface ProviderStepProps {
}
export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
const [provider, setProvider] = useState('openai');
const [apiKey, setApiKey] = useState('');
const [provider, setProvider] = useState("openai");
const [apiKey, setApiKey] = useState("");
const [saving, setSaving] = useState(false);
const { showToast } = useToast();
const handleSave = async () => {
if (!apiKey.trim()) {
showToast('Please enter an API key', 'error');
showToast("Please enter an API key", "error");
return;
}
@@ -27,60 +27,50 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
try {
// Save the API key
await credentialsService.createCredential({
key: 'OPENAI_API_KEY',
key: "OPENAI_API_KEY",
value: apiKey,
is_encrypted: true,
category: 'api_keys'
category: "api_keys",
});
// Update the provider setting if needed
await credentialsService.updateCredential({
key: 'LLM_PROVIDER',
value: 'openai',
key: "LLM_PROVIDER",
value: "openai",
is_encrypted: false,
category: 'rag_strategy'
category: "rag_strategy",
});
showToast('API key saved successfully!', 'success');
showToast("API key saved successfully!", "success");
// Mark onboarding as dismissed when API key is saved
localStorage.setItem('onboardingDismissed', 'true');
localStorage.setItem("onboardingDismissed", "true");
onSaved();
} catch (error) {
// Detailed error handling for critical configuration per alpha principles
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
const errorDetails = {
context: 'API key configuration',
operation: 'save_openai_key',
provider: 'openai',
error: errorMessage,
timestamp: new Date().toISOString()
};
// Log with full context and stack trace
console.error('API_KEY_SAVE_FAILED:', errorDetails, error);
// Log error for debugging per alpha principles
const errorMessage =
error instanceof Error ? error.message : "Unknown error";
console.error("Failed to save API key:", error);
// Show specific error details to help user resolve the issue
if (errorMessage.includes('duplicate') || errorMessage.includes('already exists')) {
if (
errorMessage.includes("duplicate") ||
errorMessage.includes("already exists")
) {
showToast(
'API key already exists. Please update it in Settings if you want to change it.',
'warning'
"API key already exists. Please update it in Settings if you want to change it.",
"warning",
);
} else if (errorMessage.includes('network') || errorMessage.includes('fetch')) {
} else if (
errorMessage.includes("network") ||
errorMessage.includes("fetch")
) {
showToast(
`Network error while saving API key: ${errorMessage}. Please check your connection.`,
'error'
);
} else if (errorMessage.includes('unauthorized') || errorMessage.includes('forbidden')) {
showToast(
`Permission error: ${errorMessage}. Please check backend configuration.`,
'error'
"error",
);
} else {
// Show the actual error for unknown issues
showToast(
`Failed to save API key: ${errorMessage}`,
'error'
);
showToast(`Failed to save API key: ${errorMessage}`, "error");
}
} finally {
setSaving(false);
@@ -88,9 +78,9 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
};
const handleSkip = () => {
showToast('You can configure your provider in Settings', 'info');
showToast("You can configure your provider in Settings", "info");
// Mark onboarding as dismissed when skipping
localStorage.setItem('onboardingDismissed', 'true');
localStorage.setItem("onboardingDismissed", "true");
onSkip();
};
@@ -103,21 +93,24 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
value={provider}
onChange={(e) => setProvider(e.target.value)}
options={[
{ value: 'openai', label: 'OpenAI' },
{ value: 'google', label: 'Google Gemini' },
{ value: 'ollama', label: 'Ollama (Local)' },
{ value: "openai", label: "OpenAI" },
{ value: "google", label: "Google Gemini" },
{ value: "ollama", label: "Ollama (Local)" },
]}
accentColor="green"
/>
<p className="mt-2 text-sm text-gray-600 dark:text-zinc-400">
{provider === 'openai' && 'OpenAI provides powerful models like GPT-4. You\'ll need an API key from OpenAI.'}
{provider === 'google' && 'Google Gemini offers advanced AI capabilities. Configure in Settings after setup.'}
{provider === 'ollama' && 'Ollama runs models locally on your machine. Configure in Settings after setup.'}
{provider === "openai" &&
"OpenAI provides powerful models like GPT-4. You'll need an API key from OpenAI."}
{provider === "google" &&
"Google Gemini offers advanced AI capabilities. Configure in Settings after setup."}
{provider === "ollama" &&
"Ollama runs models locally on your machine. Configure in Settings after setup."}
</p>
</div>
{/* OpenAI API Key Input */}
{provider === 'openai' && (
{provider === "openai" && (
<>
<div>
<Input
@@ -152,10 +145,16 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
size="lg"
onClick={handleSave}
disabled={saving || !apiKey.trim()}
icon={saving ? <Loader className="w-4 h-4 animate-spin" /> : <Save className="w-4 h-4" />}
icon={
saving ? (
<Loader className="w-4 h-4 animate-spin" />
) : (
<Save className="w-4 h-4" />
)
}
className="flex-1"
>
{saving ? 'Saving...' : 'Save & Continue'}
{saving ? "Saving..." : "Save & Continue"}
</Button>
<Button
variant="outline"
@@ -171,15 +170,17 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
)}
{/* Non-OpenAI Provider Message */}
{provider !== 'openai' && (
{provider !== "openai" && (
<div className="space-y-4">
<div className="p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg">
<p className="text-sm text-blue-800 dark:text-blue-200">
{provider === 'google' && 'Google Gemini configuration will be available in Settings after setup.'}
{provider === 'ollama' && 'Ollama configuration will be available in Settings after setup. Make sure Ollama is running locally.'}
{provider === "google" &&
"Google Gemini configuration will be available in Settings after setup."}
{provider === "ollama" &&
"Ollama configuration will be available in Settings after setup. Make sure Ollama is running locally."}
</p>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
@@ -188,27 +189,30 @@ export const ProviderStep = ({ onSaved, onSkip }: ProviderStepProps) => {
// Save the provider selection for non-OpenAI providers
try {
await credentialsService.updateCredential({
key: 'LLM_PROVIDER',
key: "LLM_PROVIDER",
value: provider,
is_encrypted: false,
category: 'rag_strategy'
category: "rag_strategy",
});
showToast(`${provider === 'google' ? 'Google Gemini' : 'Ollama'} selected as provider`, 'success');
showToast(
`${provider === "google" ? "Google Gemini" : "Ollama"} selected as provider`,
"success",
);
// Mark onboarding as dismissed
localStorage.setItem('onboardingDismissed', 'true');
localStorage.setItem("onboardingDismissed", "true");
onSaved();
} catch (error) {
console.error('Failed to save provider selection:', error);
showToast('Failed to save provider selection', 'error');
console.error("Failed to save provider selection:", error);
showToast("Failed to save provider selection", "error");
}
}}
className="flex-1"
>
Continue with {provider === 'google' ? 'Gemini' : 'Ollama'}
Continue with {provider === "google" ? "Gemini" : "Ollama"}
</Button>
</div>
</div>
)}
</div>
);
};
};

View File

@@ -70,23 +70,15 @@ export const TestStatus = () => {
};
}, []);
// Check for test results availability
// Test results availability - not implemented yet
useEffect(() => {
const checkResults = async () => {
const hasTestResults = await testService.hasTestResults();
setHasResults(hasTestResults);
};
checkResults();
setHasResults(false);
}, []);
// Check for results when UI tests complete
useEffect(() => {
if (!uiTest.isRunning && uiTest.exitCode === 0) {
// Small delay to ensure files are written
setTimeout(async () => {
const hasTestResults = await testService.hasTestResults();
setHasResults(hasTestResults);
}, 2000);
setHasResults(false);
}
}, [uiTest.isRunning, uiTest.exitCode]);

View File

@@ -1,19 +1,35 @@
import { useState, useEffect } from 'react';
import { Loader, Settings, ChevronDown, ChevronUp, Palette, Key, Brain, Code, Activity, FileCode, Bug } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { useToast } from '../contexts/ToastContext';
import { useSettings } from '../contexts/SettingsContext';
import { useStaggeredEntrance } from '../hooks/useStaggeredEntrance';
import { FeaturesSection } from '../components/settings/FeaturesSection';
import { APIKeysSection } from '../components/settings/APIKeysSection';
import { RAGSettings } from '../components/settings/RAGSettings';
import { CodeExtractionSettings } from '../components/settings/CodeExtractionSettings';
import { TestStatus } from '../components/settings/TestStatus';
import { IDEGlobalRules } from '../components/settings/IDEGlobalRules';
import { ButtonPlayground } from '../components/settings/ButtonPlayground';
import { CollapsibleSettingsCard } from '../components/ui/CollapsibleSettingsCard';
import { BugReportButton } from '../components/bug-report/BugReportButton';
import { credentialsService, RagSettings, CodeExtractionSettings as CodeExtractionSettingsType } from '../services/credentialsService';
import { useState, useEffect } from "react";
import {
Loader,
Settings,
ChevronDown,
ChevronUp,
Palette,
Key,
Brain,
Code,
Activity,
FileCode,
Bug,
} from "lucide-react";
import { motion, AnimatePresence } from "framer-motion";
import { useToast } from "../contexts/ToastContext";
import { useSettings } from "../contexts/SettingsContext";
import { useStaggeredEntrance } from "../hooks/useStaggeredEntrance";
import { FeaturesSection } from "../components/settings/FeaturesSection";
import { APIKeysSection } from "../components/settings/APIKeysSection";
import { RAGSettings } from "../components/settings/RAGSettings";
import { CodeExtractionSettings } from "../components/settings/CodeExtractionSettings";
import { TestStatus } from "../components/settings/TestStatus";
import { IDEGlobalRules } from "../components/settings/IDEGlobalRules";
import { ButtonPlayground } from "../components/settings/ButtonPlayground";
import { CollapsibleSettingsCard } from "../components/ui/CollapsibleSettingsCard";
import { BugReportButton } from "../components/bug-report/BugReportButton";
import {
credentialsService,
RagSettings,
CodeExtractionSettings as CodeExtractionSettingsType,
} from "../services/credentialsService";
export const SettingsPage = () => {
const [ragSettings, setRagSettings] = useState<RagSettings>({
@@ -22,56 +38,56 @@ export const SettingsPage = () => {
USE_HYBRID_SEARCH: true,
USE_AGENTIC_RAG: true,
USE_RERANKING: true,
MODEL_CHOICE: 'gpt-4.1-nano'
});
const [codeExtractionSettings, setCodeExtractionSettings] = useState<CodeExtractionSettingsType>({
MIN_CODE_BLOCK_LENGTH: 250,
MAX_CODE_BLOCK_LENGTH: 5000,
ENABLE_COMPLETE_BLOCK_DETECTION: true,
ENABLE_LANGUAGE_SPECIFIC_PATTERNS: true,
ENABLE_PROSE_FILTERING: true,
MAX_PROSE_RATIO: 0.15,
MIN_CODE_INDICATORS: 3,
ENABLE_DIAGRAM_FILTERING: true,
ENABLE_CONTEXTUAL_LENGTH: true,
CODE_EXTRACTION_MAX_WORKERS: 3,
CONTEXT_WINDOW_SIZE: 1000,
ENABLE_CODE_SUMMARIES: true
MODEL_CHOICE: "gpt-4.1-nano",
});
const [codeExtractionSettings, setCodeExtractionSettings] =
useState<CodeExtractionSettingsType>({
MIN_CODE_BLOCK_LENGTH: 250,
MAX_CODE_BLOCK_LENGTH: 5000,
ENABLE_COMPLETE_BLOCK_DETECTION: true,
ENABLE_LANGUAGE_SPECIFIC_PATTERNS: true,
ENABLE_PROSE_FILTERING: true,
MAX_PROSE_RATIO: 0.15,
MIN_CODE_INDICATORS: 3,
ENABLE_DIAGRAM_FILTERING: true,
ENABLE_CONTEXTUAL_LENGTH: true,
CODE_EXTRACTION_MAX_WORKERS: 3,
CONTEXT_WINDOW_SIZE: 1000,
ENABLE_CODE_SUMMARIES: true,
});
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [showButtonPlayground, setShowButtonPlayground] = useState(false);
const { showToast } = useToast();
const { projectsEnabled } = useSettings();
// Use staggered entrance animation
const { isVisible, containerVariants, itemVariants, titleVariants } = useStaggeredEntrance(
[1, 2, 3, 4],
0.15
);
const { isVisible, containerVariants, itemVariants, titleVariants } =
useStaggeredEntrance([1, 2, 3, 4], 0.15);
// Load settings on mount
useEffect(() => {
loadSettings();
}, []);
const loadSettings = async () => {
const loadSettings = async (isRetry = false) => {
try {
setLoading(true);
setError(null);
// Load RAG settings
const ragSettingsData = await credentialsService.getRagSettings();
setRagSettings(ragSettingsData);
// Load Code Extraction settings
const codeExtractionSettingsData = await credentialsService.getCodeExtractionSettings();
const codeExtractionSettingsData =
await credentialsService.getCodeExtractionSettings();
setCodeExtractionSettings(codeExtractionSettingsData);
} catch (err) {
setError('Failed to load settings');
setError("Failed to load settings");
console.error(err);
showToast('Failed to load settings', 'error');
showToast("Failed to load settings", "error");
} finally {
setLoading(false);
}
@@ -88,12 +104,15 @@ export const SettingsPage = () => {
return (
<motion.div
initial="hidden"
animate={isVisible ? 'visible' : 'hidden'}
animate={isVisible ? "visible" : "hidden"}
variants={containerVariants}
className="w-full"
>
{/* Header */}
<motion.div className="flex justify-between items-center mb-8" variants={itemVariants}>
<motion.div
className="flex justify-between items-center mb-8"
variants={itemVariants}
>
<motion.h1
className="text-3xl font-bold text-gray-800 dark:text-white flex items-center gap-3"
variants={titleVariants}
@@ -103,6 +122,7 @@ export const SettingsPage = () => {
</motion.h1>
</motion.div>
{/* Main content with two-column layout */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Left Column */}
@@ -165,7 +185,10 @@ export const SettingsPage = () => {
storageKey="rag-settings"
defaultExpanded={true}
>
<RAGSettings ragSettings={ragSettings} setRagSettings={setRagSettings} />
<RAGSettings
ragSettings={ragSettings}
setRagSettings={setRagSettings}
/>
</CollapsibleSettingsCard>
</motion.div>
<motion.div variants={itemVariants}>
@@ -176,9 +199,9 @@ export const SettingsPage = () => {
storageKey="code-extraction"
defaultExpanded={true}
>
<CodeExtractionSettings
codeExtractionSettings={codeExtractionSettings}
setCodeExtractionSettings={setCodeExtractionSettings}
<CodeExtractionSettings
codeExtractionSettings={codeExtractionSettings}
setCodeExtractionSettings={setCodeExtractionSettings}
/>
</CollapsibleSettingsCard>
</motion.div>
@@ -194,7 +217,8 @@ export const SettingsPage = () => {
>
<div className="space-y-4">
<p className="text-sm text-gray-600 dark:text-gray-400">
Found a bug or issue? Report it to help improve Archon V2 Alpha.
Found a bug or issue? Report it to help improve Archon V2
Alpha.
</p>
<div className="flex justify-start">
<BugReportButton variant="secondary" size="md">
@@ -234,7 +258,7 @@ export const SettingsPage = () => {
{showButtonPlayground && (
<motion.div
initial={{ opacity: 0, height: 0 }}
animate={{ opacity: 1, height: 'auto' }}
animate={{ opacity: 1, height: "auto" }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3 }}
className="overflow-hidden"
@@ -257,4 +281,4 @@ export const SettingsPage = () => {
)}
</motion.div>
);
};
};

View File

@@ -53,56 +53,81 @@ export interface CodeExtractionSettings {
ENABLE_CODE_SUMMARIES: boolean;
}
import { getApiUrl } from '../config/api';
import { getApiUrl } from "../config/api";
class CredentialsService {
private baseUrl = getApiUrl();
private handleCredentialError(error: any, context: string): Error {
const errorMessage = error instanceof Error ? error.message : String(error);
// Check for network errors
if (
errorMessage.toLowerCase().includes("network") ||
errorMessage.includes("fetch") ||
errorMessage.includes("Failed to fetch")
) {
return new Error(
`Network error while ${context.toLowerCase()}: ${errorMessage}. ` +
`Please check your connection and server status.`,
);
}
// Return original error with context
return new Error(`${context} failed: ${errorMessage}`);
}
async getAllCredentials(): Promise<Credential[]> {
const response = await fetch(`${this.baseUrl}/api/credentials`);
if (!response.ok) {
throw new Error('Failed to fetch credentials');
throw new Error("Failed to fetch credentials");
}
return response.json();
}
async getCredentialsByCategory(category: string): Promise<Credential[]> {
const response = await fetch(`${this.baseUrl}/api/credentials/categories/${category}`);
const response = await fetch(
`${this.baseUrl}/api/credentials/categories/${category}`,
);
if (!response.ok) {
throw new Error(`Failed to fetch credentials for category: ${category}`);
}
const result = await response.json();
// The API returns {credentials: {...}} where credentials is a dict
// Convert to array format expected by frontend
if (result.credentials && typeof result.credentials === 'object') {
return Object.entries(result.credentials).map(([key, value]: [string, any]) => {
if (value && typeof value === 'object' && value.is_encrypted) {
return {
key,
value: undefined,
encrypted_value: value.encrypted_value,
is_encrypted: true,
category,
description: value.description
};
} else {
return {
key,
value: value,
encrypted_value: undefined,
is_encrypted: false,
category,
description: ''
};
}
});
if (result.credentials && typeof result.credentials === "object") {
return Object.entries(result.credentials).map(
([key, value]: [string, any]) => {
if (value && typeof value === "object" && value.is_encrypted) {
return {
key,
value: undefined,
encrypted_value: value.encrypted_value,
is_encrypted: true,
category,
description: value.description,
};
} else {
return {
key,
value: value,
encrypted_value: undefined,
is_encrypted: false,
category,
description: "",
};
}
},
);
}
return [];
}
async getCredential(key: string): Promise<{ key: string; value?: string; is_encrypted?: boolean }> {
async getCredential(
key: string,
): Promise<{ key: string; value?: string; is_encrypted?: boolean }> {
const response = await fetch(`${this.baseUrl}/api/credentials/${key}`);
if (!response.ok) {
if (response.status === 404) {
@@ -115,24 +140,24 @@ class CredentialsService {
}
async getRagSettings(): Promise<RagSettings> {
const ragCredentials = await this.getCredentialsByCategory('rag_strategy');
const apiKeysCredentials = await this.getCredentialsByCategory('api_keys');
const ragCredentials = await this.getCredentialsByCategory("rag_strategy");
const apiKeysCredentials = await this.getCredentialsByCategory("api_keys");
const settings: RagSettings = {
USE_CONTEXTUAL_EMBEDDINGS: false,
CONTEXTUAL_EMBEDDINGS_MAX_WORKERS: 3,
USE_HYBRID_SEARCH: true,
USE_AGENTIC_RAG: true,
USE_RERANKING: true,
MODEL_CHOICE: 'gpt-4.1-nano',
LLM_PROVIDER: 'openai',
LLM_BASE_URL: '',
EMBEDDING_MODEL: '',
MODEL_CHOICE: "gpt-4.1-nano",
LLM_PROVIDER: "openai",
LLM_BASE_URL: "",
EMBEDDING_MODEL: "",
// Crawling Performance Settings defaults
CRAWL_BATCH_SIZE: 50,
CRAWL_MAX_CONCURRENT: 10,
CRAWL_WAIT_STRATEGY: 'domcontentloaded',
CRAWL_PAGE_TIMEOUT: 60000, // Increased from 30s to 60s for documentation sites
CRAWL_WAIT_STRATEGY: "domcontentloaded",
CRAWL_PAGE_TIMEOUT: 60000, // Increased from 30s to 60s for documentation sites
CRAWL_DELAY_BEFORE_HTML: 0.5,
// Storage Performance Settings defaults
DOCUMENT_STORAGE_BATCH_SIZE: 50,
@@ -143,30 +168,50 @@ class CredentialsService {
MEMORY_THRESHOLD_PERCENT: 80,
DISPATCHER_CHECK_INTERVAL: 30,
CODE_EXTRACTION_BATCH_SIZE: 50,
CODE_SUMMARY_MAX_WORKERS: 3
CODE_SUMMARY_MAX_WORKERS: 3,
};
// Map credentials to settings
[...ragCredentials, ...apiKeysCredentials].forEach(cred => {
[...ragCredentials, ...apiKeysCredentials].forEach((cred) => {
if (cred.key in settings) {
// String fields
if (['MODEL_CHOICE', 'LLM_PROVIDER', 'LLM_BASE_URL', 'EMBEDDING_MODEL', 'CRAWL_WAIT_STRATEGY'].includes(cred.key)) {
(settings as any)[cred.key] = cred.value || '';
}
if (
[
"MODEL_CHOICE",
"LLM_PROVIDER",
"LLM_BASE_URL",
"EMBEDDING_MODEL",
"CRAWL_WAIT_STRATEGY",
].includes(cred.key)
) {
(settings as any)[cred.key] = cred.value || "";
}
// Number fields
else if (['CONTEXTUAL_EMBEDDINGS_MAX_WORKERS', 'CRAWL_BATCH_SIZE', 'CRAWL_MAX_CONCURRENT',
'CRAWL_PAGE_TIMEOUT', 'DOCUMENT_STORAGE_BATCH_SIZE', 'EMBEDDING_BATCH_SIZE',
'DELETE_BATCH_SIZE', 'MEMORY_THRESHOLD_PERCENT', 'DISPATCHER_CHECK_INTERVAL',
'CODE_EXTRACTION_BATCH_SIZE', 'CODE_SUMMARY_MAX_WORKERS'].includes(cred.key)) {
(settings as any)[cred.key] = parseInt(cred.value || '0', 10) || (settings as any)[cred.key];
else if (
[
"CONTEXTUAL_EMBEDDINGS_MAX_WORKERS",
"CRAWL_BATCH_SIZE",
"CRAWL_MAX_CONCURRENT",
"CRAWL_PAGE_TIMEOUT",
"DOCUMENT_STORAGE_BATCH_SIZE",
"EMBEDDING_BATCH_SIZE",
"DELETE_BATCH_SIZE",
"MEMORY_THRESHOLD_PERCENT",
"DISPATCHER_CHECK_INTERVAL",
"CODE_EXTRACTION_BATCH_SIZE",
"CODE_SUMMARY_MAX_WORKERS",
].includes(cred.key)
) {
(settings as any)[cred.key] =
parseInt(cred.value || "0", 10) || (settings as any)[cred.key];
}
// Float fields
else if (cred.key === 'CRAWL_DELAY_BEFORE_HTML') {
settings[cred.key] = parseFloat(cred.value || '0.5') || 0.5;
else if (cred.key === "CRAWL_DELAY_BEFORE_HTML") {
settings[cred.key] = parseFloat(cred.value || "0.5") || 0.5;
}
// Boolean fields
else {
(settings as any)[cred.key] = cred.value === 'true';
(settings as any)[cred.key] = cred.value === "true";
}
}
});
@@ -175,71 +220,96 @@ class CredentialsService {
}
async updateCredential(credential: Credential): Promise<Credential> {
const response = await fetch(`${this.baseUrl}/api/credentials/${credential.key}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credential),
});
if (!response.ok) {
throw new Error('Failed to update credential');
try {
const response = await fetch(
`${this.baseUrl}/api/credentials/${credential.key}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credential),
},
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
return response.json();
} catch (error) {
throw this.handleCredentialError(
error,
`Updating credential '${credential.key}'`,
);
}
return response.json();
}
async createCredential(credential: Credential): Promise<Credential> {
const response = await fetch(`${this.baseUrl}/api/credentials`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credential),
});
if (!response.ok) {
throw new Error('Failed to create credential');
try {
const response = await fetch(`${this.baseUrl}/api/credentials`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(credential),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
return response.json();
} catch (error) {
throw this.handleCredentialError(
error,
`Creating credential '${credential.key}'`,
);
}
return response.json();
}
async deleteCredential(key: string): Promise<void> {
const response = await fetch(`${this.baseUrl}/api/credentials/${key}`, {
method: 'DELETE',
});
if (!response.ok) {
throw new Error('Failed to delete credential');
try {
const response = await fetch(`${this.baseUrl}/api/credentials/${key}`, {
method: "DELETE",
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(`HTTP ${response.status}: ${errorText}`);
}
} catch (error) {
throw this.handleCredentialError(error, `Deleting credential '${key}'`);
}
}
async updateRagSettings(settings: RagSettings): Promise<void> {
const promises = [];
// Update all RAG strategy settings
for (const [key, value] of Object.entries(settings)) {
// Skip undefined values
if (value === undefined) continue;
promises.push(
this.updateCredential({
key,
value: value.toString(),
is_encrypted: false,
category: 'rag_strategy',
})
category: "rag_strategy",
}),
);
}
await Promise.all(promises);
}
async getCodeExtractionSettings(): Promise<CodeExtractionSettings> {
const codeExtractionCredentials = await this.getCredentialsByCategory('code_extraction');
const codeExtractionCredentials =
await this.getCredentialsByCategory("code_extraction");
const settings: CodeExtractionSettings = {
MIN_CODE_BLOCK_LENGTH: 250,
MAX_CODE_BLOCK_LENGTH: 5000,
@@ -252,21 +322,24 @@ class CredentialsService {
ENABLE_CONTEXTUAL_LENGTH: true,
CODE_EXTRACTION_MAX_WORKERS: 3,
CONTEXT_WINDOW_SIZE: 1000,
ENABLE_CODE_SUMMARIES: true
ENABLE_CODE_SUMMARIES: true,
};
// Map credentials to settings
codeExtractionCredentials.forEach(cred => {
codeExtractionCredentials.forEach((cred) => {
if (cred.key in settings) {
const key = cred.key as keyof CodeExtractionSettings;
if (typeof settings[key] === 'number') {
if (key === 'MAX_PROSE_RATIO') {
settings[key] = parseFloat(cred.value || '0.15');
if (typeof settings[key] === "number") {
if (key === "MAX_PROSE_RATIO") {
settings[key] = parseFloat(cred.value || "0.15");
} else {
settings[key] = parseInt(cred.value || settings[key].toString(), 10);
settings[key] = parseInt(
cred.value || settings[key].toString(),
10,
);
}
} else if (typeof settings[key] === 'boolean') {
settings[key] = cred.value === 'true';
} else if (typeof settings[key] === "boolean") {
settings[key] = cred.value === "true";
}
}
});
@@ -274,9 +347,11 @@ class CredentialsService {
return settings;
}
async updateCodeExtractionSettings(settings: CodeExtractionSettings): Promise<void> {
async updateCodeExtractionSettings(
settings: CodeExtractionSettings,
): Promise<void> {
const promises = [];
// Update all code extraction settings
for (const [key, value] of Object.entries(settings)) {
promises.push(
@@ -284,13 +359,13 @@ class CredentialsService {
key,
value: value.toString(),
is_encrypted: false,
category: 'code_extraction',
})
category: "code_extraction",
}),
);
}
await Promise.all(promises);
}
}
export const credentialsService = new CredentialsService();
export const credentialsService = new CredentialsService();

View File

@@ -242,18 +242,6 @@ class TestService {
}
}
/**
* Check if test results are available
*/
async hasTestResults(): Promise<boolean> {
try {
// Check for latest test results via API
const response = await fetch(`${API_BASE_URL}/api/tests/latest-results`);
return response.ok;
} catch {
return false;
}
}
/**
* Get coverage data for Test Results Modal from new API endpoints with fallback

View File

@@ -1,6 +1,7 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, test, expect, vi } from 'vitest'
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest'
import React from 'react'
import { credentialsService } from '../src/services/credentialsService'
describe('Error Handling Tests', () => {
test('api error simulation', () => {
@@ -196,4 +197,40 @@ describe('Error Handling Tests', () => {
fireEvent.click(screen.getByText('500 Error'))
expect(screen.getByRole('alert')).toHaveTextContent('Something went wrong on our end')
})
})
describe('CredentialsService Error Handling', () => {
const originalFetch = global.fetch
beforeEach(() => {
global.fetch = vi.fn() as any
})
afterEach(() => {
global.fetch = originalFetch
})
test('should handle network errors with context', async () => {
const mockError = new Error('Network request failed')
;(global.fetch as any).mockRejectedValueOnce(mockError)
await expect(credentialsService.createCredential({
key: 'TEST_KEY',
value: 'test',
is_encrypted: false,
category: 'test'
})).rejects.toThrow(/Network error while creating credential 'test_key'/)
})
test('should preserve context in error messages', async () => {
const mockError = new Error('database error')
;(global.fetch as any).mockRejectedValueOnce(mockError)
await expect(credentialsService.updateCredential({
key: 'OPENAI_API_KEY',
value: 'sk-test',
is_encrypted: true,
category: 'api_keys'
})).rejects.toThrow(/Updating credential 'OPENAI_API_KEY' failed/)
})
})

View File

@@ -74,7 +74,7 @@ describe('Onboarding Detection Tests', () => {
{ key: 'LLM_PROVIDER', value: 'openai', category: 'rag_strategy' }
]
const apiKeyCreds: NormalizedCredential[] = [
{ key: 'OPENAI_API_KEY', is_encrypted: true, category: 'api_keys' }
{ key: 'OPENAI_API_KEY', is_encrypted: true, encrypted_value: 'encrypted_sk-test123', category: 'api_keys' }
]
expect(isLmConfigured(ragCreds, apiKeyCreds)).toBe(true)

View File

@@ -2,6 +2,9 @@ import { expect, afterEach, vi } from 'vitest'
import { cleanup } from '@testing-library/react'
import '@testing-library/jest-dom/vitest'
// Set required environment variables for tests
process.env.ARCHON_SERVER_PORT = '8181'
// Clean up after each test
afterEach(() => {
cleanup()
@@ -15,7 +18,7 @@ global.fetch = vi.fn(() =>
text: () => Promise.resolve(''),
status: 200,
} as Response)
)
) as any
// Mock WebSocket
class MockWebSocket {