/** * Status Drawer - Slide-out panel from right side * Shows ONLY: Resources, Endpoints, Providers status * Real-time updates, NO CPU/Memory stats */ class StatusDrawer { constructor(options = {}) { this.options = { apiEndpoint: options.apiEndpoint || '/api/system/status', updateInterval: options.updateInterval || 3000, // 3 seconds ...options }; this.isOpen = false; this.pollTimer = null; this.lastData = null; this.drawerElement = null; this.buttonElement = null; this.createDrawer(); this.createFloatingButton(); if (this.options.showApiHelpButton) { this.createApiHelpButton(); } } createFloatingButton() { const wrap = document.createElement('div'); wrap.id = 'fab-stack'; wrap.className = 'fab-stack'; const button = document.createElement('button'); button.id = 'status-drawer-btn'; button.className = 'status-drawer-floating-btn'; button.title = 'وضعیت سیستم | System Status'; button.setAttribute('aria-label', 'Open status panel'); button.innerHTML = ` `; button.addEventListener('click', () => this.toggle()); wrap.appendChild(button); document.body.appendChild(wrap); this.buttonElement = button; this.fabStack = wrap; } createApiHelpButton() { const helpBtn = document.createElement('button'); helpBtn.id = 'api-help-fab'; helpBtn.className = 'status-drawer-floating-btn api-help-fab'; helpBtn.title = 'راهنمای API | API Help'; helpBtn.setAttribute('aria-label', 'Open API help guide'); helpBtn.innerHTML = ` `; helpBtn.addEventListener('click', async () => { try { const { ConfigHelperModal } = await import('/static/shared/components/config-helper-modal.js?v=4.1'); if (!window._configHelperModal) window._configHelperModal = new ConfigHelperModal(); window._configHelperModal.show(); } catch (e) { console.error('API help failed:', e); } }); (this.fabStack || document.body).appendChild(helpBtn); this.helpButtonElement = helpBtn; } /** * Create drawer panel - ENHANCED with detailed provider metrics */ createDrawer() { const drawer = document.createElement('div'); drawer.id = 'status-drawer'; drawer.className = 'status-drawer status-drawer-enhanced'; drawer.innerHTML = `

System Status وضعیت سیستم

All Providers
Loading...
AI Models
Loading...
API Keys
Loading...
Rotation Pools
Loading...
Infrastructure
Loading...
Resource Breakdown
Loading...
Recent Errors
Performance
Loading...
`; document.body.appendChild(drawer); this.drawerElement = drawer; // Close button drawer.querySelector('.drawer-close').addEventListener('click', () => this.close()); // Refresh button drawer.querySelector('#refresh-status').addEventListener('click', () => this.fetchStatus()); // Collapsible sections drawer.querySelectorAll('.section-title.collapsible').forEach(title => { title.addEventListener('click', (e) => { const target = title.dataset.target; const content = document.getElementById(target); if (content) { content.classList.toggle('collapsed'); title.classList.toggle('collapsed'); } }); }); } /** * Toggle drawer */ toggle() { if (this.isOpen) { this.close(); } else { this.open(); } } /** * Open drawer */ open() { if (this.isOpen) return; this.isOpen = true; this.drawerElement.classList.add('open'); this.fabStack?.classList.add('hidden'); this.buttonElement?.classList.add('hidden'); // Start polling this.startPolling(); } /** * Close drawer */ close() { if (!this.isOpen) return; this.isOpen = false; this.drawerElement.classList.remove('open'); this.fabStack?.classList.remove('hidden'); this.buttonElement?.classList.remove('hidden'); // Stop polling this.stopPolling(); } /** * Start polling (only when open) */ startPolling() { if (!this.isOpen) return; this.fetchStatus(); this.pollTimer = setTimeout(() => this.startPolling(), this.options.updateInterval); } /** * Stop polling */ stopPolling() { if (this.pollTimer) { clearTimeout(this.pollTimer); this.pollTimer = null; } } /** * Fetch status from API */ async fetchStatus() { if (!this.isOpen) return; try { const overviewUrl = this.options.overviewEndpoint || '/api/dashboard/overview'; const [statusRes, overviewRes] = await Promise.allSettled([ fetch(this.options.apiEndpoint), fetch(overviewUrl), ]); if (statusRes.status !== 'fulfilled' || !statusRes.value.ok) { throw new Error(`HTTP ${statusRes.value?.status || 'error'}`); } const data = await statusRes.value.json(); if (overviewRes.status === 'fulfilled' && overviewRes.value.ok) { data._overview = await overviewRes.value.json(); } this.updateUI(data); } catch (error) { console.error('Status Drawer: Failed to fetch:', error); this.showError(); } } /** * Update UI with data - ENHANCED */ updateUI(data) { this.lastData = data; // Update all providers with detailed metrics this.updateProvidersDetailed(data.providers_detailed || data.services || []); // Update AI models this.updateAIModels(data.ai_models || {}); // Update infrastructure this.updateInfrastructure(data.infrastructure || {}); // Update resource breakdown this.updateResourceBreakdown(data.resource_breakdown || {}); // Update error details this.updateErrorDetails(data.error_details || []); // Update performance this.updatePerformance(data.performance || {}); this.updateApiKeys(data.api_keys || {}); this.updateRotationSummary(data.rotation || data._overview?.rotation || {}); // Update timestamp this.updateTimestamp(data.timestamp); } updateApiKeys(apiKeys) { const container = document.getElementById('api-keys-list'); if (!container) return; const env = apiKeys.env_configured || []; const display = apiKeys.display || `${apiKeys.env_secrets || 0} secrets + ${apiKeys.file_keys || 0} file`; container.innerHTML = `
Ready: 🟢 ${apiKeys.configured_total || 0} total
Breakdown: ${display}
Env secrets: ${env.length ? env.join(', ') : 'none'}
API: GET /api/resources/rotation/health
`; } updateRotationSummary(rotation) { const container = document.getElementById('rotation-summary'); if (!container) return; container.innerHTML = `
Pools: 🟢 ${rotation.pools_healthy || 0} / ${rotation.pools_total || 0}
Providers: ${rotation.providers_available || 0} / ${rotation.providers_total || 0}
`; } /** * Update providers with detailed metrics */ updateProvidersDetailed(providers) { const container = document.getElementById('providers-list'); if (!container) return; if (!providers.length) { container.innerHTML = '
No providers configured
'; return; } container.innerHTML = providers.map(provider => { const isOnline = provider.status === 'online' || provider.status === 'active'; const statusEmoji = isOnline ? '🟢' : provider.status === 'rate_limited' ? '🔴' : provider.status === 'degraded' ? '🟡' : '⚫'; let statusText = ''; if (provider.resource_count) { statusText = `${provider.resource_count} providers | ${provider.success_rate || 100}% avail`; } else if (isOnline) { statusText = `${provider.response_time_ms || 0}ms | Success: ${provider.success_rate || 100}%`; if (provider.last_check) { const elapsed = Math.floor((Date.now() / 1000) - new Date(provider.last_check).getTime() / 1000); statusText += ` | Last: ${elapsed}s ago`; } } else if (provider.status === 'rate_limited') { statusText = `Rate Limited (${provider.status_code || 429})`; if (provider.cached_until) { statusText += ` | Cached ${provider.cached_until}`; } } else if (provider.status === 'degraded') { statusText = provider.error || 'Degraded performance'; } else { statusText = provider.error || 'Offline'; } const resourceInfo = provider.resource_count ? ` | ${provider.resource_count} resources` : ''; return `
${statusEmoji} ${provider.name}
${statusText}${resourceInfo}
`; }).join(''); } /** * Update AI models section */ updateAIModels(aiModels) { const container = document.getElementById('ai-models-list'); if (!container) return; const overview = this.lastData?._overview?.cards || {}; const v2 = overview.models_loaded_v2 ?? aiModels.models_loaded_v2 ?? aiModels.sentiment_models ?? 0; const v4 = overview.models_loaded_v4 ?? aiModels.models_loaded_v4 ?? 0; const total = overview.models_total ?? aiModels.models_total ?? (v2 + v4); const catV2 = aiModels.catalog_v2 || overview.models_total_v2 || 45; const catV4 = aiModels.catalog_v4 || overview.models_total_v4 || 18; const hfMode = overview.hf_mode || aiModels.hf_mode || 'unknown'; const v4Mode = overview.v4_hf_mode || aiModels.v4_hf_mode || 'complement'; container.innerHTML = `
v2 Models: 🟢 ${v2} loaded
v4 Complement: 🟢 ${v4} loaded (${v4Mode})
Catalog: ${catV2} v2 + ${catV4} v4 = ${catV2 + catV4}
Loaded: ${v2 + v4} / ${total}
HF Mode: ${hfMode}
API: GET /api/models/summary
`; } /** * Update infrastructure section */ updateInfrastructure(infrastructure) { const container = document.getElementById('infrastructure-list'); if (!container) return; const dbStatus = infrastructure.database_status || 'unknown'; const dbEntries = infrastructure.database_entries || 0; const workerStatus = infrastructure.background_worker || 'unknown'; const workerNextRun = infrastructure.worker_next_run || 'N/A'; const wsStatus = infrastructure.websocket_active ? '🟢 Active' : '⚫ Inactive'; container.innerHTML = `
Database: ${dbStatus === 'online' ? '🟢' : '🔴'} SQLite (${dbEntries} cached)
Background Worker: ${workerStatus === 'active' ? '🟢' : '⚫'} ${workerNextRun}
WebSocket: ${wsStatus}
`; } /** * Update resource breakdown section */ updateResourceBreakdown(breakdown) { const container = document.getElementById('resources-breakdown'); if (!container) return; const total = breakdown.total || 0; const bySource = breakdown.by_source || {}; const byCategory = breakdown.by_category || {}; let sourceHTML = ''; for (const [source, count] of Object.entries(bySource)) { sourceHTML += `
${source}: ${count}
`; } let categoryHTML = ''; for (const [category, count] of Object.entries(byCategory)) { categoryHTML += `
${category}: ${count} online
`; } container.innerHTML = `
Total: ${total}+ resources
${sourceHTML}
By Category:
${categoryHTML}
`; } /** * Update error details section */ updateErrorDetails(errors) { const container = document.getElementById('error-list'); if (!container) return; if (!errors || errors.length === 0) { container.innerHTML = '
No recent errors
'; return; } container.innerHTML = errors.map(error => `
${error.provider || 'Unknown'}: ${error.count || 1}x ${error.type || 'error'}
${error.message || 'Unknown error'}
${error.action ? `
Action: ${error.action}
` : ''}
`).join(''); } /** * Update performance section */ updatePerformance(performance) { const container = document.getElementById('performance-metrics'); if (!container) return; const avgResponse = performance.avg_response_ms || 0; const fastest = performance.fastest_provider || 'N/A'; const fastestTime = performance.fastest_time_ms || 0; const cacheHit = performance.cache_hit_rate || 0; container.innerHTML = `
Avg Response: ${avgResponse}ms
Fastest: ${fastest} (${fastestTime}ms)
Cache Hit: ${cacheHit}%
`; } /** * Update endpoints */ updateEndpoints(endpoints) { const container = document.getElementById('endpoints-status'); if (!container) return; if (!endpoints.length) { container.innerHTML = '
No endpoints
'; return; } container.innerHTML = endpoints.map(endpoint => { const statusClass = endpoint.status === 'online' ? 'status-online' : 'status-offline'; return `
${endpoint.path}
${endpoint.avg_response_ms ? `${endpoint.avg_response_ms.toFixed(0)}ms` : '--'} • ${endpoint.success_rate ? `${endpoint.success_rate.toFixed(1)}%` : '--'}
`; }).join(''); } /** * Update providers */ updateProviders(services) { const container = document.getElementById('providers-status'); if (!container) return; if (!services.length) { container.innerHTML = '
No providers
'; return; } container.innerHTML = services.map(service => { const statusClass = service.status === 'online' ? 'status-online' : 'status-offline'; return `
${service.name}
${service.response_time_ms ? `${service.response_time_ms.toFixed(0)}ms` : 'Offline'}
`; }).join(''); } /** * Update coins */ updateCoins(coins) { const container = document.getElementById('coins-status'); if (!container) return; if (!coins.length) { container.innerHTML = '
No coins
'; return; } container.innerHTML = coins.map(coin => { const statusClass = coin.status === 'online' ? 'status-online' : 'status-offline'; return `
${coin.symbol}
${coin.price ? `$${coin.price.toLocaleString()}` : 'Unavailable'}
`; }).join(''); } /** * Update timestamp */ updateTimestamp(timestamp) { const element = document.getElementById('last-update-time'); if (element) { const date = new Date(timestamp * 1000); element.textContent = date.toLocaleTimeString(); } } /** * Show error state */ showError() { const sections = ['resources-summary', 'endpoints-status', 'providers-status', 'coins-status']; sections.forEach(id => { const element = document.getElementById(id); if (element) { element.innerHTML = '
Failed to load
'; } }); } /** * Destroy drawer */ destroy() { this.stopPolling(); if (this.drawerElement) this.drawerElement.remove(); if (this.buttonElement) this.buttonElement.remove(); } } // Export if (typeof window !== 'undefined') { window.StatusDrawer = StatusDrawer; }