mirror of
https://github.com/coleam00/Archon.git
synced 2026-01-01 20:28:43 -05:00
Add Supabase key validation and simplify frontend state management
- Add backend validation to detect and warn about anon vs service keys - Prevent startup with incorrect Supabase key configuration - Consolidate frontend state management following KISS principles - Remove duplicate state tracking and sessionStorage polling - Add clear error display when backend fails to start - Improve .env.example documentation with detailed key selection guide - Add comprehensive test coverage for validation logic - Remove unused test results checking to eliminate 404 errors The implementation now warns users about key misconfiguration while maintaining backward compatibility. Frontend state is simplified with MainLayout as the single source of truth for backend status.
This commit is contained in:
74
archon-ui-main/src/components/BackendStartupError.tsx
Normal file
74
archon-ui-main/src/components/BackendStartupError.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user