/** * Swapper Service Integration for Node #1 and Node #2 Admin Consoles * React/TypeScript component example */ import React, { useEffect, useState } from 'react'; import { AlertCircle } from 'lucide-react'; // Types interface SwapperStatus { service: string; status: string; mode: string; active_model: { name: string; uptime_hours: number; request_count: number; loaded_at: string | null; } | null; total_models: number; available_models: string[]; loaded_models: string[]; models: Array<{ name: string; ollama_name: string; type: string; size_gb: number; priority: string; status: string; is_active: boolean; uptime_hours: number; request_count: number; total_uptime_seconds: number; }>; timestamp: string; } interface SwapperMetrics { summary: { total_models: number; active_models: number; available_models: number; total_uptime_hours: number; total_requests: number; }; most_used_model: { name: string; uptime_hours: number; request_count: number; } | null; active_model: { name: string; uptime_hours: number | null; } | null; timestamp: string; } // API Service - визначається по ноді const getSwapperUrl = (nodeId?: string): string => { // Визначаємо URL Swapper Service на основі nodeId if (!nodeId) { return import.meta.env.VITE_SWAPPER_URL || 'http://localhost:8890'; } // НОДА1: node-1, node-1-hetzner-gex44, або будь-який ID що містить 'node-1' if (nodeId === 'node-1' || nodeId === 'node-1-hetzner-gex44' || nodeId.includes('node-1')) { return import.meta.env.VITE_SWAPPER_NODE1_URL || 'http://144.76.224.179:8890'; } // НОДА2: node-2 або будь-який ID що містить 'node-2' if (nodeId === 'node-2' || nodeId.includes('node-2')) { return import.meta.env.VITE_SWAPPER_NODE2_URL || 'http://192.168.1.244:8890'; } // За замовчуванням return import.meta.env.VITE_SWAPPER_URL || 'http://localhost:8890'; }; const SWAPPER_API_BASE = getSwapperUrl(); export const swapperService = { async getStatus(): Promise { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); // 5 second timeout try { const response = await fetch(`${SWAPPER_API_BASE}/api/cabinet/swapper/status`, { signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) throw new Error('Failed to fetch Swapper status'); return response.json(); } catch (error) { clearTimeout(timeoutId); if (error instanceof Error && error.name === 'AbortError') { throw new Error('Swapper Service не відповідає (таймаут)'); } throw error; } }, async getMetrics(): Promise { const response = await fetch(`${SWAPPER_API_BASE}/api/cabinet/swapper/metrics/summary`); if (!response.ok) throw new Error('Failed to fetch Swapper metrics'); return response.json(); }, async loadModel(modelName: string): Promise { const response = await fetch(`${SWAPPER_API_BASE}/models/${modelName}/load`, { method: 'POST', }); if (!response.ok) throw new Error(`Failed to load model: ${modelName}`); }, async unloadModel(modelName: string): Promise { const response = await fetch(`${SWAPPER_API_BASE}/models/${modelName}/unload`, { method: 'POST', }); if (!response.ok) throw new Error(`Failed to unload model: ${modelName}`); }, }; // Main Swapper Status Component export const SwapperStatusCard: React.FC<{ nodeId?: string }> = ({ nodeId }) => { const [status, setStatus] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const swapperUrl = getSwapperUrl(nodeId); const fetchStatus = async () => { try { setLoading(true); const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); // Збільшено до 10 секунд // Спочатку перевіряємо health endpoint const healthResponse = await fetch(`${swapperUrl}/health`, { signal: controller.signal, mode: 'cors', }); clearTimeout(timeoutId); if (!healthResponse.ok) { throw new Error(`Swapper Service health check failed: ${healthResponse.status}`); } // Потім отримуємо статус - спочатку пробуємо cabinet API, потім базовий endpoint const controller2 = new AbortController(); const timeoutId2 = setTimeout(() => controller2.abort(), 10000); let response; let data; // Спробуємо cabinet API try { response = await fetch(`${swapperUrl}/api/cabinet/swapper/status`, { signal: controller2.signal, mode: 'cors', }); if (response.ok) { data = await response.json(); setStatus(data); setError(null); clearTimeout(timeoutId2); return; } } catch (cabinetError) { console.warn('Cabinet API not available, trying basic endpoint:', cabinetError); } // Fallback на базовий endpoint clearTimeout(timeoutId2); const controller3 = new AbortController(); const timeoutId3 = setTimeout(() => controller3.abort(), 10000); response = await fetch(`${swapperUrl}/status`, { signal: controller3.signal, mode: 'cors', }); clearTimeout(timeoutId3); if (!response.ok) { throw new Error(`Failed to fetch Swapper status: ${response.status} ${response.statusText}`); } const basicStatus = await response.json(); // Конвертуємо базовий статус у формат cabinet API data = { service: 'swapper-service', status: 'healthy', mode: basicStatus.mode || 'single-active', active_model: basicStatus.active_model ? { name: basicStatus.active_model, uptime_hours: 0, request_count: 0, loaded_at: null, } : null, total_models: basicStatus.total_models || 0, available_models: basicStatus.available_models || [], loaded_models: basicStatus.loaded_models || [], models: [], timestamp: new Date().toISOString(), }; setStatus(data); setError(null); } catch (err) { if (err instanceof Error && err.name === 'AbortError') { setError(`Swapper Service не відповідає (таймаут 10 секунд). URL: ${swapperUrl}`); } else if (err instanceof Error) { setError(`${err.message}. URL: ${swapperUrl}`); } else { setError(`Невідома помилка. URL: ${swapperUrl}`); } console.error('Error fetching Swapper status:', err); } finally { setLoading(false); } }; useEffect(() => { fetchStatus(); const interval = setInterval(fetchStatus, 30000); // Update every 30 seconds return () => clearInterval(interval); }, [nodeId, swapperUrl]); // Додано залежності для правильного оновлення if (loading) return
Завантаження статусу Swapper...
; if (error) { // Показуємо детальну інформацію про помилку return (

Swapper Service недоступний

{error}

URL: {swapperUrl}

Node ID: {nodeId || 'N/A'}

Можливі причини:

  • Swapper Service не запущений на сервері
  • Проблеми з мережею або файрволом
  • Неправильний URL або порт
  • CORS обмеження

Як перевірити:

ssh root@144.76.224.179 "curl http://localhost:8890/health"
); } if (!status) return
Немає даних про статус
; return (

🔄 Swapper Service

{status.status}
Mode: {status.mode}
Total Models: {status.total_models}
Loaded Models: {status.loaded_models.length}
{status.active_model && (

✨ Active Model

{status.active_model.name}
Uptime: {status.active_model.uptime_hours.toFixed(2)}h
Requests: {status.active_model.request_count}
{status.active_model.loaded_at && (
Loaded: {new Date(status.active_model.loaded_at).toLocaleString()}
)}
)}

Available Models

{status.models.map((model) => ( ))}
Name Type Size (GB) Status Uptime (h) Actions
{model.name} {model.type} {model.size_gb.toFixed(1)} {model.status} {model.uptime_hours.toFixed(2)} {model.status === 'unloaded' && ( )} {model.status === 'loaded' && !model.is_active && ( )} {model.is_active && ( ● Active )}
Last updated: {new Date(status.timestamp).toLocaleString()}
); }; // Metrics Summary Component export const SwapperMetricsSummary: React.FC<{ nodeId?: string }> = ({ nodeId }) => { const swapperUrl = getSwapperUrl(nodeId); const fetchMetrics = async (): Promise => { try { // Спробуємо cabinet API let response = await fetch(`${swapperUrl}/api/cabinet/swapper/metrics/summary`, { mode: 'cors', }); if (response.ok) { return await response.json(); } // Fallback - створюємо базові метрики зі статусу const statusResponse = await fetch(`${swapperUrl}/status`, { mode: 'cors', }); if (!statusResponse.ok) { throw new Error('Failed to fetch Swapper status'); } const status = await statusResponse.json(); // Створюємо базові метрики return { summary: { total_models: status.total_models || 0, active_models: status.active_model ? 1 : 0, available_models: status.available_models?.length || 0, total_uptime_hours: 0, total_requests: 0, }, most_used_model: status.active_model ? { name: status.active_model, uptime_hours: 0, request_count: 0, } : null, active_model: status.active_model ? { name: status.active_model, uptime_hours: null, } : null, timestamp: new Date().toISOString(), }; } catch (err) { console.error('Error fetching Swapper metrics:', err); return null; } }; const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const loadMetrics = async () => { const data = await fetchMetrics(); setMetrics(data); setLoading(false); }; loadMetrics(); const interval = setInterval(loadMetrics, 30000); // Update every 30 seconds return () => clearInterval(interval); }, [nodeId, swapperUrl]); // Додано залежності для правильного оновлення if (loading) return
Завантаження метрик...
; if (!metrics) return
Метрики недоступні
; return (

📊 Метрики

Всього моделей: {metrics.summary.total_models}
Активних: {metrics.summary.active_models}
Доступних: {metrics.summary.available_models}
Uptime (год): {metrics.summary.total_uptime_hours.toFixed(1)}
Запитів: {metrics.summary.total_requests}
{metrics.most_used_model && (

Найбільш використовувана модель:

{metrics.most_used_model.name}

{metrics.most_used_model.uptime_hours.toFixed(1)} год | {metrics.most_used_model.request_count} запитів

)}
); }; // Legacy export for backward compatibility export const SwapperMetricsSummaryLegacy: React.FC = () => { const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const fetchMetrics = async () => { try { const data = await swapperService.getMetrics(); setMetrics(data); } catch (err) { console.error('Error fetching metrics:', err); } finally { setLoading(false); } }; fetchMetrics(); const interval = setInterval(fetchMetrics, 60000); // Update every minute return () => clearInterval(interval); }, []); if (loading || !metrics) return
Loading metrics...
; return (

📊 Metrics Summary

Total Models
{metrics.summary.total_models}
Active Models
{metrics.summary.active_models}
Total Uptime
{metrics.summary.total_uptime_hours.toFixed(2)}h
Total Requests
{metrics.summary.total_requests}
{metrics.most_used_model && (
Most Used Model
{metrics.most_used_model.name} {metrics.most_used_model.uptime_hours.toFixed(2)}h
)}
); }; // Main Swapper Page Component export const SwapperPage: React.FC = () => { return (

Swapper Service

Dynamic model loading and management

); }; export default SwapperPage;