| |
| |
| |
| |
| |
|
|
| class StatusDrawer { |
| constructor(options = {}) { |
| this.options = { |
| apiEndpoint: options.apiEndpoint || '/api/system/status', |
| updateInterval: options.updateInterval || 3000, |
| ...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 = ` |
| <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"/> |
| <path d="M12 6v6l4 2"/> |
| </svg> |
| `; |
| 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 = ` |
| <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"/> |
| <path d="M9.09 9a3 3 0 1 1 5.82 1c0 2-3 2-3 4"/> |
| <line x1="12" y1="17" x2="12" y2="17"/> |
| </svg> |
| `; |
| 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; |
| } |
| |
| |
| |
| |
| createDrawer() { |
| const drawer = document.createElement('div'); |
| drawer.id = 'status-drawer'; |
| drawer.className = 'status-drawer status-drawer-enhanced'; |
| drawer.innerHTML = ` |
| <div class="status-drawer-header"> |
| <h3>System Status <small style="font-weight:400;opacity:.75">وضعیت سیستم</small></h3> |
| <div class="header-actions"> |
| <button class="refresh-btn" id="refresh-status" aria-label="Refresh"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="23 4 23 10 17 10"></polyline> |
| <polyline points="1 20 1 14 7 14"></polyline> |
| <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path> |
| </svg> |
| </button> |
| <button class="drawer-close" aria-label="Close"> |
| <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M9 18l6-6-6-6"/> |
| </svg> |
| </button> |
| </div> |
| </div> |
| |
| <div class="status-drawer-body"> |
| <!-- ALL PROVIDER STATUS --> |
| <div class="status-section providers-detailed"> |
| <div class="section-title collapsible" data-target="providers-list"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <rect x="2" y="3" width="20" height="14" rx="2"/> |
| <line x1="8" y1="21" x2="16" y2="21"/> |
| <line x1="12" y1="17" x2="12" y2="21"/> |
| </svg> |
| <span>All Providers</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="providers-list"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- AI MODELS --> |
| <div class="status-section ai-models"> |
| <div class="section-title collapsible" data-target="ai-models-list"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/> |
| </svg> |
| <span>AI Models</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="ai-models-list"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- API KEYS --> |
| <div class="status-section api-keys"> |
| <div class="section-title collapsible" data-target="api-keys-list"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0 3 3L22 7l-3-3m-3.5 3.5L19 4"/> |
| </svg> |
| <span>API Keys</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="api-keys-list"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- ROTATION --> |
| <div class="status-section rotation"> |
| <div class="section-title collapsible" data-target="rotation-summary"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="23 4 23 10 17 10"></polyline> |
| <polyline points="1 20 1 14 7 14"></polyline> |
| <path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"></path> |
| </svg> |
| <span>Rotation Pools</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="rotation-summary"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- INFRASTRUCTURE --> |
| <div class="status-section infrastructure"> |
| <div class="section-title collapsible" data-target="infrastructure-list"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <rect x="2" y="2" width="20" height="8" rx="2" ry="2"></rect> |
| <rect x="2" y="14" width="20" height="8" rx="2" ry="2"></rect> |
| <line x1="6" y1="6" x2="6" y2="6"></line> |
| <line x1="6" y1="18" x2="6" y2="18"></line> |
| </svg> |
| <span>Infrastructure</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="infrastructure-list"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- RESOURCE BREAKDOWN --> |
| <div class="status-section resource-breakdown"> |
| <div class="section-title collapsible" data-target="resources-breakdown"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <line x1="8" y1="6" x2="21" y2="6"></line> |
| <line x1="8" y1="12" x2="21" y2="12"></line> |
| <line x1="8" y1="18" x2="21" y2="18"></line> |
| <line x1="3" y1="6" x2="3" y2="6"></line> |
| <line x1="3" y1="12" x2="3" y2="12"></line> |
| <line x1="3" y1="18" x2="3" y2="18"></line> |
| </svg> |
| <span>Resource Breakdown</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="resources-breakdown"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- ERROR DETAILS --> |
| <div class="status-section error-details"> |
| <div class="section-title collapsible" data-target="error-list"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <circle cx="12" cy="12" r="10"></circle> |
| <line x1="12" y1="8" x2="12" y2="12"></line> |
| <line x1="12" y1="16" x2="12" y2="16"></line> |
| </svg> |
| <span>Recent Errors</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content collapsed" id="error-list"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- PERFORMANCE --> |
| <div class="status-section performance"> |
| <div class="section-title collapsible" data-target="performance-metrics"> |
| <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline> |
| </svg> |
| <span>Performance</span> |
| <svg class="chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
| <polyline points="6 9 12 15 18 9"></polyline> |
| </svg> |
| </div> |
| <div class="collapsible-content" id="performance-metrics"> |
| <div class="summary-loading">Loading...</div> |
| </div> |
| </div> |
| |
| <!-- Last Update --> |
| <div class="drawer-footer"> |
| <span class="last-update-label">Last update:</span> |
| <span class="last-update-time" id="last-update-time">--</span> |
| </div> |
| </div> |
| `; |
| |
| document.body.appendChild(drawer); |
| this.drawerElement = drawer; |
| |
| |
| drawer.querySelector('.drawer-close').addEventListener('click', () => this.close()); |
| |
| |
| drawer.querySelector('#refresh-status').addEventListener('click', () => this.fetchStatus()); |
| |
| |
| 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() { |
| if (this.isOpen) { |
| this.close(); |
| } else { |
| this.open(); |
| } |
| } |
| |
| |
| |
| |
| open() { |
| if (this.isOpen) return; |
| |
| this.isOpen = true; |
| this.drawerElement.classList.add('open'); |
| this.fabStack?.classList.add('hidden'); |
| this.buttonElement?.classList.add('hidden'); |
| |
| |
| this.startPolling(); |
| } |
| |
| |
| |
| |
| close() { |
| if (!this.isOpen) return; |
| |
| this.isOpen = false; |
| this.drawerElement.classList.remove('open'); |
| this.fabStack?.classList.remove('hidden'); |
| this.buttonElement?.classList.remove('hidden'); |
| |
| |
| this.stopPolling(); |
| } |
| |
| |
| |
| |
| startPolling() { |
| if (!this.isOpen) return; |
| |
| this.fetchStatus(); |
| this.pollTimer = setTimeout(() => this.startPolling(), this.options.updateInterval); |
| } |
| |
| |
| |
| |
| stopPolling() { |
| if (this.pollTimer) { |
| clearTimeout(this.pollTimer); |
| this.pollTimer = null; |
| } |
| } |
| |
| |
| |
| |
| 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(); |
| } |
| } |
| |
| |
| |
| |
| updateUI(data) { |
| this.lastData = data; |
| |
| |
| this.updateProvidersDetailed(data.providers_detailed || data.services || []); |
| |
| |
| this.updateAIModels(data.ai_models || {}); |
| |
| |
| this.updateInfrastructure(data.infrastructure || {}); |
| |
| |
| this.updateResourceBreakdown(data.resource_breakdown || {}); |
| |
| |
| this.updateErrorDetails(data.error_details || []); |
| |
| |
| this.updatePerformance(data.performance || {}); |
|
|
| this.updateApiKeys(data.api_keys || {}); |
| this.updateRotationSummary(data.rotation || data._overview?.rotation || {}); |
| |
| |
| 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 = ` |
| <div class="metric-item"> |
| <span class="metric-label">Ready:</span> |
| <span class="metric-value">🟢 ${apiKeys.configured_total || 0} total</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Breakdown:</span> |
| <span class="metric-value">${display}</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Env secrets:</span> |
| <span class="metric-value" style="font-size:11px">${env.length ? env.join(', ') : 'none'}</span> |
| </div> |
| <div class="metric-hint">API: <code>GET /api/resources/rotation/health</code></div> |
| `; |
| } |
|
|
| updateRotationSummary(rotation) { |
| const container = document.getElementById('rotation-summary'); |
| if (!container) return; |
| container.innerHTML = ` |
| <div class="metric-item"> |
| <span class="metric-label">Pools:</span> |
| <span class="metric-value">🟢 ${rotation.pools_healthy || 0} / ${rotation.pools_total || 0}</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Providers:</span> |
| <span class="metric-value">${rotation.providers_available || 0} / ${rotation.providers_total || 0}</span> |
| </div> |
| `; |
| } |
| |
| |
| |
| |
| updateProvidersDetailed(providers) { |
| const container = document.getElementById('providers-list'); |
| if (!container) return; |
| |
| if (!providers.length) { |
| container.innerHTML = '<div class="empty-state">No providers configured</div>'; |
| 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 ` |
| <div class="provider-item ${isOnline ? 'online' : 'offline'}"> |
| <div class="provider-status"> |
| <span class="status-emoji">${statusEmoji}</span> |
| <span class="provider-name">${provider.name}</span> |
| </div> |
| <div class="provider-metrics">${statusText}${resourceInfo}</div> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| |
| |
| |
| 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 = ` |
| <div class="metric-item"> |
| <span class="metric-label">v2 Models:</span> |
| <span class="metric-value">🟢 ${v2} loaded</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">v4 Complement:</span> |
| <span class="metric-value">🟢 ${v4} loaded (${v4Mode})</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Catalog:</span> |
| <span class="metric-value">${catV2} v2 + ${catV4} v4 = ${catV2 + catV4}</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Loaded:</span> |
| <span class="metric-value">${v2 + v4} / ${total}</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">HF Mode:</span> |
| <span class="metric-value">${hfMode}</span> |
| </div> |
| <div class="metric-hint">API: <code>GET /api/models/summary</code></div> |
| `; |
| } |
| |
| |
| |
| |
| 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 = ` |
| <div class="metric-item"> |
| <span class="metric-label">Database:</span> |
| <span class="metric-value">${dbStatus === 'online' ? '🟢' : '🔴'} SQLite (${dbEntries} cached)</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Background Worker:</span> |
| <span class="metric-value">${workerStatus === 'active' ? '🟢' : '⚫'} ${workerNextRun}</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">WebSocket:</span> |
| <span class="metric-value">${wsStatus}</span> |
| </div> |
| `; |
| } |
| |
| |
| |
| |
| 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 += ` |
| <div class="breakdown-item"> |
| <span class="breakdown-label">${source}:</span> |
| <span class="breakdown-value">${count}</span> |
| </div> |
| `; |
| } |
| |
| let categoryHTML = ''; |
| for (const [category, count] of Object.entries(byCategory)) { |
| categoryHTML += ` |
| <div class="breakdown-item"> |
| <span class="breakdown-label">${category}:</span> |
| <span class="breakdown-value">${count} online</span> |
| </div> |
| `; |
| } |
| |
| container.innerHTML = ` |
| <div class="breakdown-section"> |
| <div class="breakdown-title">Total: ${total}+ resources</div> |
| ${sourceHTML} |
| </div> |
| <div class="breakdown-section"> |
| <div class="breakdown-title">By Category:</div> |
| ${categoryHTML} |
| </div> |
| `; |
| } |
| |
| |
| |
| |
| updateErrorDetails(errors) { |
| const container = document.getElementById('error-list'); |
| if (!container) return; |
| |
| if (!errors || errors.length === 0) { |
| container.innerHTML = '<div class="empty-state">No recent errors</div>'; |
| return; |
| } |
| |
| container.innerHTML = errors.map(error => ` |
| <div class="error-item"> |
| <div class="error-provider">${error.provider || 'Unknown'}: ${error.count || 1}x ${error.type || 'error'}</div> |
| <div class="error-message">${error.message || 'Unknown error'}</div> |
| ${error.action ? `<div class="error-action">Action: ${error.action}</div>` : ''} |
| </div> |
| `).join(''); |
| } |
| |
| |
| |
| |
| 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 = ` |
| <div class="metric-item"> |
| <span class="metric-label">Avg Response:</span> |
| <span class="metric-value">${avgResponse}ms</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Fastest:</span> |
| <span class="metric-value">${fastest} (${fastestTime}ms)</span> |
| </div> |
| <div class="metric-item"> |
| <span class="metric-label">Cache Hit:</span> |
| <span class="metric-value">${cacheHit}%</span> |
| </div> |
| `; |
| } |
| |
| |
| |
| |
| updateEndpoints(endpoints) { |
| const container = document.getElementById('endpoints-status'); |
| if (!container) return; |
| |
| if (!endpoints.length) { |
| container.innerHTML = '<div class="empty-state">No endpoints</div>'; |
| return; |
| } |
| |
| container.innerHTML = endpoints.map(endpoint => { |
| const statusClass = endpoint.status === 'online' ? 'status-online' : 'status-offline'; |
| return ` |
| <div class="status-item ${statusClass}"> |
| <div class="status-dot"></div> |
| <div class="status-info"> |
| <div class="status-name">${endpoint.path}</div> |
| <div class="status-meta"> |
| ${endpoint.avg_response_ms ? `${endpoint.avg_response_ms.toFixed(0)}ms` : '--'} • |
| ${endpoint.success_rate ? `${endpoint.success_rate.toFixed(1)}%` : '--'} |
| </div> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| |
| |
| |
| updateProviders(services) { |
| const container = document.getElementById('providers-status'); |
| if (!container) return; |
| |
| if (!services.length) { |
| container.innerHTML = '<div class="empty-state">No providers</div>'; |
| return; |
| } |
| |
| container.innerHTML = services.map(service => { |
| const statusClass = service.status === 'online' ? 'status-online' : 'status-offline'; |
| return ` |
| <div class="status-item ${statusClass}"> |
| <div class="status-dot"></div> |
| <div class="status-info"> |
| <div class="status-name">${service.name}</div> |
| <div class="status-meta"> |
| ${service.response_time_ms ? `${service.response_time_ms.toFixed(0)}ms` : 'Offline'} |
| </div> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| |
| |
| |
| updateCoins(coins) { |
| const container = document.getElementById('coins-status'); |
| if (!container) return; |
| |
| if (!coins.length) { |
| container.innerHTML = '<div class="empty-state">No coins</div>'; |
| return; |
| } |
| |
| container.innerHTML = coins.map(coin => { |
| const statusClass = coin.status === 'online' ? 'status-online' : 'status-offline'; |
| return ` |
| <div class="status-item ${statusClass}"> |
| <div class="status-dot"></div> |
| <div class="status-info"> |
| <div class="status-name">${coin.symbol}</div> |
| <div class="status-meta"> |
| ${coin.price ? `$${coin.price.toLocaleString()}` : 'Unavailable'} |
| </div> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
| |
| |
| |
| |
| updateTimestamp(timestamp) { |
| const element = document.getElementById('last-update-time'); |
| if (element) { |
| const date = new Date(timestamp * 1000); |
| element.textContent = date.toLocaleTimeString(); |
| } |
| } |
| |
| |
| |
| |
| showError() { |
| const sections = ['resources-summary', 'endpoints-status', 'providers-status', 'coins-status']; |
| sections.forEach(id => { |
| const element = document.getElementById(id); |
| if (element) { |
| element.innerHTML = '<div class="error-state">Failed to load</div>'; |
| } |
| }); |
| } |
| |
| |
| |
| |
| destroy() { |
| this.stopPolling(); |
| if (this.drawerElement) this.drawerElement.remove(); |
| if (this.buttonElement) this.buttonElement.remove(); |
| } |
| } |
|
|
| |
| if (typeof window !== 'undefined') { |
| window.StatusDrawer = StatusDrawer; |
| } |
|
|