|
|
|
|
|
|
| const AppState = {
|
| currentTab: 'dashboard',
|
| data: {},
|
| charts: {}
|
| };
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
| initTabs();
|
| checkAPIStatus();
|
| loadDashboard();
|
|
|
|
|
| setInterval(() => {
|
| if (AppState.currentTab === 'dashboard') {
|
| loadDashboard();
|
| }
|
| }, 30000);
|
| });
|
|
|
|
|
| function initTabs() {
|
| const tabButtons = document.querySelectorAll('.tab-btn');
|
| const tabContents = document.querySelectorAll('.tab-content');
|
|
|
| tabButtons.forEach(btn => {
|
| btn.addEventListener('click', () => {
|
| const tabId = btn.dataset.tab;
|
|
|
|
|
| tabButtons.forEach(b => b.classList.remove('active'));
|
| btn.classList.add('active');
|
|
|
|
|
| tabContents.forEach(c => c.classList.remove('active'));
|
| document.getElementById(`tab-${tabId}`).classList.add('active');
|
|
|
| AppState.currentTab = tabId;
|
|
|
|
|
| loadTabData(tabId);
|
| });
|
| });
|
| }
|
|
|
|
|
| function loadTabData(tabId) {
|
| switch(tabId) {
|
| case 'dashboard':
|
| case 'market':
|
| loadMarketData();
|
| break;
|
| case 'monitor':
|
| loadMonitorData();
|
| break;
|
| case 'advanced':
|
| loadAdvancedData();
|
| break;
|
| case 'admin':
|
| loadAdminData();
|
| break;
|
| case 'hf':
|
| loadHFHealth();
|
| loadModels();
|
| break;
|
| case 'pools':
|
| loadPools();
|
| break;
|
| case 'logs':
|
| loadLogs();
|
| break;
|
| case 'resources':
|
| loadResources();
|
| loadAPIRegistry();
|
| break;
|
| case 'reports':
|
| loadReports();
|
| break;
|
|
|
| case 'models':
|
| loadModels();
|
| break;
|
| case 'sentiment':
|
| loadSentimentModels();
|
| loadSentimentHistory();
|
| break;
|
| case 'news':
|
| loadNews();
|
| break;
|
| case 'providers':
|
| loadProviders();
|
| break;
|
| case 'diagnostics':
|
| loadDiagnostics();
|
| break;
|
| case 'api-explorer':
|
| loadAPIEndpoints();
|
| break;
|
| }
|
| }
|
|
|
|
|
| function loadAPIEndpoints() {
|
| const endpointSelect = document.getElementById('api-endpoint');
|
| if (!endpointSelect) return;
|
|
|
|
|
| const endpoints = [
|
| { value: '/api/health', text: 'GET /api/health - Health Check' },
|
| { value: '/api/status', text: 'GET /api/status - System Status' },
|
| { value: '/api/stats', text: 'GET /api/stats - Statistics' },
|
| { value: '/api/market', text: 'GET /api/market - Market Data' },
|
| { value: '/api/trending', text: 'GET /api/trending - Trending Coins' },
|
| { value: '/api/sentiment', text: 'GET /api/sentiment - Fear & Greed Index' },
|
| { value: '/api/news', text: 'GET /api/news - Latest News' },
|
| { value: '/api/news/latest', text: 'GET /api/news/latest - Latest News (Alt)' },
|
| { value: '/api/resources', text: 'GET /api/resources - Resources Summary' },
|
| { value: '/api/providers', text: 'GET /api/providers - List Providers' },
|
| { value: '/api/models/list', text: 'GET /api/models/list - List Models' },
|
| { value: '/api/models/status', text: 'GET /api/models/status - Models Status' },
|
| { value: '/api/models/data/stats', text: 'GET /api/models/data/stats - Models Statistics' },
|
| { value: '/api/logs/recent', text: 'GET /api/logs/recent - Recent Logs' },
|
| { value: '/api/logs/errors', text: 'GET /api/logs/errors - Error Logs' },
|
| { value: '/api/diagnostics/last', text: 'GET /api/diagnostics/last - Last Diagnostics' },
|
| { value: '/api/hf/models', text: 'GET /api/hf/models - HF Models' },
|
| { value: '/api/hf/health', text: 'GET /api/hf/health - HF Health' }
|
| ];
|
|
|
|
|
| endpointSelect.innerHTML = '<option value="">انتخاب Endpoint...</option>';
|
| endpoints.forEach(ep => {
|
| const option = document.createElement('option');
|
| option.value = ep.value;
|
| option.textContent = ep.text;
|
| endpointSelect.appendChild(option);
|
| });
|
| }
|
|
|
|
|
| async function checkAPIStatus() {
|
| try {
|
| const response = await fetch('/health');
|
| const data = await response.json();
|
|
|
| const statusBadge = document.getElementById('api-status');
|
| if (data.status === 'healthy') {
|
| statusBadge.className = 'status-badge';
|
| statusBadge.innerHTML = '<span class="status-dot"></span><span>✅ سیستم فعال</span>';
|
| } else {
|
| statusBadge.className = 'status-badge error';
|
| statusBadge.innerHTML = '<span class="status-dot"></span><span>❌ خطا</span>';
|
| }
|
| } catch (error) {
|
| const statusBadge = document.getElementById('api-status');
|
| statusBadge.className = 'status-badge error';
|
| statusBadge.innerHTML = '<span class="status-dot"></span><span>❌ اتصال برقرار نشد</span>';
|
| }
|
| }
|
|
|
|
|
| async function loadDashboard() {
|
| try {
|
|
|
| const resourcesRes = await fetch('/api/resources');
|
| const resourcesData = await resourcesRes.json();
|
|
|
| if (resourcesData.success && resourcesData.summary) {
|
| document.getElementById('stat-total-resources').textContent = resourcesData.summary.total_resources || 0;
|
| document.getElementById('stat-free-resources').textContent = resourcesData.summary.free_resources || 0;
|
| document.getElementById('stat-models').textContent = resourcesData.summary.models_available || 0;
|
| }
|
|
|
|
|
| try {
|
| const statusRes = await fetch('/api/status');
|
| const statusData = await statusRes.json();
|
|
|
| document.getElementById('stat-providers').textContent = statusData.total_apis || statusData.total_providers || 0;
|
|
|
|
|
| const systemStatusDiv = document.getElementById('system-status');
|
| const healthStatus = statusData.system_health || 'unknown';
|
| const healthClass = healthStatus === 'healthy' ? 'alert-success' :
|
| healthStatus === 'degraded' ? 'alert-warning' : 'alert-error';
|
|
|
| systemStatusDiv.innerHTML = `
|
| <div class="alert ${healthClass}">
|
| <strong>وضعیت سیستم:</strong> ${healthStatus}<br>
|
| <strong>APIهای آنلاین:</strong> ${statusData.online || 0}<br>
|
| <strong>APIهای تخریب شده:</strong> ${statusData.degraded || 0}<br>
|
| <strong>APIهای آفلاین:</strong> ${statusData.offline || 0}<br>
|
| <strong>میانگین زمان پاسخ:</strong> ${statusData.avg_response_time_ms || 0}ms<br>
|
| <strong>آخرین بهروزرسانی:</strong> ${new Date(statusData.last_update || Date.now()).toLocaleString('fa-IR')}
|
| </div>
|
| `;
|
| } catch (statusError) {
|
| console.warn('Status endpoint not available:', statusError);
|
| document.getElementById('stat-providers').textContent = '-';
|
| }
|
|
|
|
|
| if (resourcesData.success && resourcesData.summary.categories) {
|
| createCategoriesChart(resourcesData.summary.categories);
|
| }
|
| } catch (error) {
|
| console.error('Error loading dashboard:', error);
|
| showError('خطا در بارگذاری داشبورد');
|
| }
|
| }
|
|
|
|
|
| function createCategoriesChart(categories) {
|
| const ctx = document.getElementById('categories-chart');
|
| if (!ctx) return;
|
|
|
| if (AppState.charts.categories) {
|
| AppState.charts.categories.destroy();
|
| }
|
|
|
| AppState.charts.categories = new Chart(ctx, {
|
| type: 'bar',
|
| data: {
|
| labels: Object.keys(categories),
|
| datasets: [{
|
| label: 'تعداد منابع',
|
| data: Object.values(categories),
|
| backgroundColor: 'rgba(102, 126, 234, 0.6)',
|
| borderColor: 'rgba(102, 126, 234, 1)',
|
| borderWidth: 2
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| plugins: {
|
| legend: { display: false }
|
| },
|
| scales: {
|
| y: { beginAtZero: true }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| async function loadMarketData() {
|
| try {
|
| const response = await fetch('/api/market');
|
| const data = await response.json();
|
|
|
| if (data.cryptocurrencies && data.cryptocurrencies.length > 0) {
|
| const marketDiv = document.getElementById('market-data');
|
| marketDiv.innerHTML = `
|
| <div style="overflow-x: auto;">
|
| <table>
|
| <thead>
|
| <tr>
|
| <th>#</th>
|
| <th>نام</th>
|
| <th>قیمت (USD)</th>
|
| <th>تغییر 24h</th>
|
| <th>حجم 24h</th>
|
| <th>مارکت کپ</th>
|
| </tr>
|
| </thead>
|
| <tbody>
|
| ${data.cryptocurrencies.map(coin => `
|
| <tr>
|
| <td>${coin.rank || '-'}</td>
|
| <td>
|
| ${coin.image ? `<img src="${coin.image}" style="width: 24px; height: 24px; margin-left: 8px; vertical-align: middle;" />` : ''}
|
| <strong>${coin.symbol}</strong> ${coin.name}
|
| </td>
|
| <td>$${formatNumber(coin.price)}</td>
|
| <td style="color: ${coin.change_24h >= 0 ? 'var(--success)' : 'var(--danger)'}; font-weight: 600;">
|
| ${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h || 0).toFixed(2)}%
|
| </td>
|
| <td>$${formatNumber(coin.volume_24h)}</td>
|
| <td>$${formatNumber(coin.market_cap)}</td>
|
| </tr>
|
| `).join('')}
|
| </tbody>
|
| </table>
|
| </div>
|
| ${data.total_market_cap ? `<div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
|
| <strong>کل مارکت کپ:</strong> $${formatNumber(data.total_market_cap)} |
|
| <strong>BTC Dominance:</strong> ${(data.btc_dominance || 0).toFixed(2)}%
|
| </div>` : ''}
|
| `;
|
| } else {
|
| document.getElementById('market-data').innerHTML = '<div class="alert alert-warning">دادهای یافت نشد</div>';
|
| }
|
|
|
|
|
| try {
|
| const trendingRes = await fetch('/api/trending');
|
| const trendingData = await trendingRes.json();
|
|
|
| if (trendingData.trending && trendingData.trending.length > 0) {
|
| const trendingDiv = document.getElementById('trending-coins');
|
| trendingDiv.innerHTML = `
|
| <div style="display: grid; gap: 10px;">
|
| ${trendingData.trending.map((coin, index) => `
|
| <div style="padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 10px; display: flex; justify-content: space-between; align-items: center; border-left: 4px solid var(--primary);">
|
| <div style="display: flex; align-items: center; gap: 10px;">
|
| <span style="font-size: 18px; font-weight: 800; color: var(--primary);">#${index + 1}</span>
|
| <div>
|
| <strong>${coin.symbol || coin.id}</strong> - ${coin.name || 'Unknown'}
|
| ${coin.market_cap_rank ? `<div style="font-size: 12px; color: var(--text-secondary);">رتبه مارکت کپ: ${coin.market_cap_rank}</div>` : ''}
|
| </div>
|
| </div>
|
| <div style="font-size: 20px; font-weight: 700; color: var(--success);">${coin.score ? coin.score.toFixed(2) : 'N/A'}</div>
|
| </div>
|
| `).join('')}
|
| </div>
|
| `;
|
| } else {
|
| document.getElementById('trending-coins').innerHTML = '<div class="alert alert-warning">دادهای یافت نشد</div>';
|
| }
|
| } catch (trendingError) {
|
| console.warn('Trending endpoint error:', trendingError);
|
| document.getElementById('trending-coins').innerHTML = '<div class="alert alert-error">خطا در بارگذاری ارزهای ترند</div>';
|
| }
|
|
|
|
|
| try {
|
| const sentimentRes = await fetch('/api/sentiment');
|
| const sentimentData = await sentimentRes.json();
|
|
|
| if (sentimentData.fear_greed_index !== undefined) {
|
| const fgDiv = document.getElementById('fear-greed');
|
| const fgValue = sentimentData.fear_greed_index;
|
| const fgLabel = sentimentData.fear_greed_label || 'Unknown';
|
|
|
|
|
| let fgColor = 'var(--warning)';
|
| if (fgValue >= 75) fgColor = 'var(--success)';
|
| else if (fgValue >= 50) fgColor = 'var(--info)';
|
| else if (fgValue >= 25) fgColor = 'var(--warning)';
|
| else fgColor = 'var(--danger)';
|
|
|
| fgDiv.innerHTML = `
|
| <div style="text-align: center; padding: 30px;">
|
| <div style="font-size: 72px; font-weight: 800; margin-bottom: 10px; color: ${fgColor};">
|
| ${fgValue}
|
| </div>
|
| <div style="font-size: 24px; font-weight: 600; color: var(--text-primary); margin-bottom: 10px;">
|
| ${fgLabel}
|
| </div>
|
| <div style="font-size: 14px; color: var(--text-secondary);">
|
| شاخص ترس و طمع بازار
|
| </div>
|
| ${sentimentData.timestamp ? `<div style="font-size: 12px; color: var(--text-secondary); margin-top: 10px;">
|
| آخرین بهروزرسانی: ${new Date(sentimentData.timestamp).toLocaleString('fa-IR')}
|
| </div>` : ''}
|
| </div>
|
| `;
|
| } else {
|
| document.getElementById('fear-greed').innerHTML = '<div class="alert alert-warning">دادهای یافت نشد</div>';
|
| }
|
| } catch (sentimentError) {
|
| console.warn('Sentiment endpoint error:', sentimentError);
|
| document.getElementById('fear-greed').innerHTML = '<div class="alert alert-error">خطا در بارگذاری شاخص ترس و طمع</div>';
|
| }
|
| } catch (error) {
|
| console.error('Error loading market data:', error);
|
| showError('خطا در بارگذاری دادههای بازار');
|
| }
|
| }
|
|
|
|
|
| function formatNumber(num) {
|
| if (!num) return '0';
|
| if (num >= 1e12) return (num / 1e12).toFixed(2) + 'T';
|
| if (num >= 1e9) return (num / 1e9).toFixed(2) + 'B';
|
| if (num >= 1e6) return (num / 1e6).toFixed(2) + 'M';
|
| if (num >= 1e3) return (num / 1e3).toFixed(2) + 'K';
|
| return num.toLocaleString('en-US', { maximumFractionDigits: 2 });
|
| }
|
|
|
|
|
| async function loadModels() {
|
| try {
|
| const response = await fetch('/api/models/list');
|
| const data = await response.json();
|
|
|
| const models = data.models || data || [];
|
|
|
| if (models.length > 0) {
|
| const modelsListDiv = document.getElementById('models-list');
|
| modelsListDiv.innerHTML = `
|
| <div style="display: grid; gap: 15px;">
|
| ${models.map(model => {
|
| const status = model.status || 'unknown';
|
| const isAvailable = status === 'available' || status === 'loaded';
|
| const statusColor = isAvailable ? 'var(--success)' : 'var(--danger)';
|
| const statusBg = isAvailable ? 'rgba(16, 185, 129, 0.2)' : 'rgba(239, 68, 68, 0.2)';
|
|
|
| return `
|
| <div style="padding: 20px; background: rgba(31, 41, 55, 0.6); border-radius: 12px; border-left: 4px solid ${statusColor};">
|
| <div style="display: flex; justify-content: space-between; align-items: start; flex-wrap: wrap; gap: 10px;">
|
| <div style="flex: 1;">
|
| <h4 style="margin-bottom: 5px; color: var(--text-primary);">${model.model_id || model.name || 'Unknown'}</h4>
|
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">
|
| ${model.task || model.category || 'N/A'}
|
| </div>
|
| ${model.category ? `<div style="font-size: 11px; color: var(--text-secondary);">دسته: ${model.category}</div>` : ''}
|
| ${model.requires_auth !== undefined ? `<div style="font-size: 11px; color: var(--text-secondary);">
|
| ${model.requires_auth ? '🔐 نیاز به احراز هویت' : '🔓 بدون احراز هویت'}
|
| </div>` : ''}
|
| </div>
|
| <span style="background: ${statusBg}; color: ${statusColor}; padding: 5px 10px; border-radius: 5px; font-size: 12px; font-weight: 600;">
|
| ${isAvailable ? '✅ در دسترس' : '❌ غیرفعال'}
|
| </span>
|
| </div>
|
| ${model.key ? `<div style="margin-top: 10px; font-size: 11px; color: var(--text-secondary); font-family: monospace;">
|
| Key: ${model.key}
|
| </div>` : ''}
|
| </div>
|
| `;
|
| }).join('')}
|
| </div>
|
| `;
|
| } else {
|
| document.getElementById('models-list').innerHTML = '<div class="alert alert-warning">هیچ مدلی یافت نشد</div>';
|
| }
|
|
|
|
|
| try {
|
| const statusRes = await fetch('/api/models/status');
|
| const statusData = await statusRes.json();
|
|
|
| const statusDiv = document.getElementById('models-status');
|
| const status = statusData.status || statusData.ok ? 'فعال' : 'غیرفعال';
|
| const statusClass = statusData.status === 'ok' || statusData.ok ? 'alert-success' : 'alert-warning';
|
|
|
| statusDiv.innerHTML = `
|
| <div class="alert ${statusClass}">
|
| <strong>وضعیت:</strong> ${status}<br>
|
| <strong>مدلهای بارگذاری شده:</strong> ${statusData.models_loaded || statusData.pipelines_loaded || 0}<br>
|
| <strong>مدلهای در دسترس:</strong> ${statusData.models_available || statusData.available_models?.length || 0}<br>
|
| ${statusData.mode ? `<strong>حالت:</strong> ${statusData.mode}<br>` : ''}
|
| ${statusData.transformers_available !== undefined ? `<strong>Transformers:</strong> ${statusData.transformers_available ? '✅' : '❌'}<br>` : ''}
|
| </div>
|
| `;
|
| } catch (statusError) {
|
| console.warn('Models status endpoint error:', statusError);
|
| }
|
|
|
|
|
| try {
|
| const statsRes = await fetch('/api/models/data/stats');
|
| const statsData = await statsRes.json();
|
|
|
| if (statsData.success && statsData.statistics) {
|
| const statsDiv = document.getElementById('models-stats');
|
| statsDiv.innerHTML = `
|
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">
|
| <div style="padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
|
| <div style="font-size: 28px; font-weight: 800; color: var(--primary);">${statsData.statistics.total_analyses || 0}</div>
|
| <div style="font-size: 14px; color: var(--text-secondary);">کل تحلیلها</div>
|
| </div>
|
| <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px;">
|
| <div style="font-size: 28px; font-weight: 800; color: var(--success);">${statsData.statistics.unique_symbols || 0}</div>
|
| <div style="font-size: 14px; color: var(--text-secondary);">نمادهای منحصر به فرد</div>
|
| </div>
|
| ${statsData.statistics.most_used_model ? `
|
| <div style="padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 10px;">
|
| <div style="font-size: 18px; font-weight: 800; color: var(--warning);">${statsData.statistics.most_used_model}</div>
|
| <div style="font-size: 14px; color: var(--text-secondary);">پراستفادهترین مدل</div>
|
| </div>
|
| ` : ''}
|
| </div>
|
| `;
|
| }
|
| } catch (statsError) {
|
| console.warn('Models stats endpoint error:', statsError);
|
| }
|
| } catch (error) {
|
| console.error('Error loading models:', error);
|
| showError('خطا در بارگذاری مدلها');
|
| }
|
| }
|
|
|
|
|
| async function initializeModels() {
|
| try {
|
| const response = await fetch('/api/models/initialize', { method: 'POST' });
|
| const data = await response.json();
|
|
|
| if (data.success) {
|
| showSuccess('مدلها با موفقیت بارگذاری شدند');
|
| loadModels();
|
| } else {
|
| showError(data.error || 'خطا در بارگذاری مدلها');
|
| }
|
| } catch (error) {
|
| showError('خطا در بارگذاری مدلها: ' + error.message);
|
| }
|
| }
|
|
|
|
|
| async function loadSentimentModels() {
|
| try {
|
| const response = await fetch('/api/models/list');
|
| const data = await response.json();
|
|
|
| const models = data.models || data || [];
|
| const select = document.getElementById('sentiment-model');
|
| select.innerHTML = '<option value="">انتخاب مدل...</option>';
|
|
|
| models.filter(m => {
|
| const status = m.status || 'unknown';
|
| return status === 'available' || status === 'loaded' || !m.status;
|
| }).forEach(model => {
|
| const option = document.createElement('option');
|
| const modelId = model.model_id || model.name || model.key || 'unknown';
|
| const task = model.task || model.category || '';
|
| option.value = model.key || modelId;
|
| option.textContent = `${modelId}${task ? ` (${task})` : ''}`;
|
| select.appendChild(option);
|
| });
|
|
|
|
|
| if (select.options.length === 1) {
|
| const option = document.createElement('option');
|
| option.value = '';
|
| option.textContent = 'هیچ مدلی در دسترس نیست';
|
| option.disabled = true;
|
| select.appendChild(option);
|
| }
|
| } catch (error) {
|
| console.error('Error loading sentiment models:', error);
|
| const select = document.getElementById('sentiment-model');
|
| select.innerHTML = '<option value="">خطا در بارگذاری مدلها</option>';
|
| }
|
| }
|
|
|
|
|
| async function analyzeGlobalSentiment() {
|
| const resultDiv = document.getElementById('global-sentiment-result');
|
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل احساسات بازار...</div>';
|
|
|
| try {
|
|
|
| const marketText = "Cryptocurrency market analysis: Bitcoin, Ethereum, and major altcoins showing mixed signals. Market sentiment analysis required.";
|
|
|
| const response = await fetch('/api/sentiment/analyze', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text: marketText, mode: 'crypto' })
|
| });
|
|
|
| const data = await response.json();
|
|
|
| if (!data.available) {
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-warning">
|
| <strong>⚠️ مدلها در دسترس نیستند:</strong> ${data.error || 'مدلهای AI در حال حاضر در دسترس نیستند'}
|
| </div>
|
| `;
|
| return;
|
| }
|
|
|
| const sentiment = data.sentiment || 'neutral';
|
| const confidence = data.confidence || 0;
|
| const sentimentEmoji = sentiment === 'bullish' ? '📈' : sentiment === 'bearish' ? '📉' : '➡️';
|
| const sentimentColor = sentiment === 'bullish' ? 'var(--success)' : sentiment === 'bearish' ? 'var(--danger)' : 'var(--text-secondary)';
|
|
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
|
| <h4 style="margin-bottom: 15px;">احساسات کلی بازار</h4>
|
| <div style="display: grid; gap: 10px;">
|
| <div style="text-align: center; padding: 20px;">
|
| <div style="font-size: 48px; margin-bottom: 10px;">${sentimentEmoji}</div>
|
| <div style="font-size: 24px; font-weight: 700; color: ${sentimentColor}; margin-bottom: 5px;">
|
| ${sentiment === 'bullish' ? 'صعودی' : sentiment === 'bearish' ? 'نزولی' : 'خنثی'}
|
| </div>
|
| <div style="color: var(--text-secondary);">
|
| اعتماد: ${(confidence * 100).toFixed(1)}%
|
| </div>
|
| </div>
|
| <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
|
| <strong>جزئیات:</strong>
|
| <div style="margin-top: 5px; font-size: 13px; color: var(--text-secondary);">
|
| این تحلیل بر اساس مدلهای AI انجام شده است.
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| `;
|
| } catch (error) {
|
| console.error('Global sentiment analysis error:', error);
|
| resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
|
| showError('خطا در تحلیل احساسات بازار');
|
| }
|
| }
|
|
|
|
|
| async function analyzeAssetSentiment() {
|
| const symbol = document.getElementById('asset-symbol').value.trim().toUpperCase();
|
| const text = document.getElementById('asset-sentiment-text').value.trim();
|
|
|
| if (!symbol) {
|
| showError('لطفاً نماد ارز را وارد کنید');
|
| return;
|
| }
|
|
|
| const resultDiv = document.getElementById('asset-sentiment-result');
|
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل...</div>';
|
|
|
| try {
|
|
|
| const analysisText = text || `${symbol} market analysis and sentiment`;
|
|
|
| const response = await fetch('/api/sentiment/analyze', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text: analysisText, mode: 'crypto', symbol: symbol })
|
| });
|
|
|
| const data = await response.json();
|
|
|
| if (!data.available) {
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-warning">
|
| <strong>⚠️ مدلها در دسترس نیستند:</strong> ${data.error || 'مدلهای AI در حال حاضر در دسترس نیستند'}
|
| </div>
|
| `;
|
| return;
|
| }
|
|
|
| const sentiment = data.sentiment || 'neutral';
|
| const confidence = data.confidence || 0;
|
| const sentimentEmoji = sentiment === 'bullish' ? '📈' : sentiment === 'bearish' ? '📉' : '➡️';
|
| const sentimentColor = sentiment === 'bullish' ? 'var(--success)' : sentiment === 'bearish' ? 'var(--danger)' : 'var(--text-secondary)';
|
|
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
|
| <h4 style="margin-bottom: 15px;">نتیجه تحلیل احساسات ${symbol}</h4>
|
| <div style="display: grid; gap: 10px;">
|
| <div>
|
| <strong>احساسات:</strong>
|
| <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
|
| ${sentimentEmoji} ${sentiment === 'bullish' ? 'صعودی' : sentiment === 'bearish' ? 'نزولی' : 'خنثی'}
|
| </span>
|
| </div>
|
| <div>
|
| <strong>اعتماد:</strong>
|
| <span style="color: var(--primary); font-weight: 600;">
|
| ${(confidence * 100).toFixed(2)}%
|
| </span>
|
| </div>
|
| ${text ? `
|
| <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
|
| <strong>متن تحلیل شده:</strong>
|
| <div style="margin-top: 5px; padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 5px; font-size: 13px; color: var(--text-secondary);">
|
| "${text.substring(0, 200)}${text.length > 200 ? '...' : ''}"
|
| </div>
|
| </div>
|
| ` : ''}
|
| </div>
|
| </div>
|
| `;
|
| } catch (error) {
|
| console.error('Asset sentiment analysis error:', error);
|
| resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
|
| showError('خطا در تحلیل احساسات ارز');
|
| }
|
| }
|
|
|
|
|
| async function analyzeNewsSentiment() {
|
| const title = document.getElementById('news-title').value.trim();
|
| const content = document.getElementById('news-content').value.trim();
|
|
|
| if (!title && !content) {
|
| showError('لطفاً عنوان یا محتوای خبر را وارد کنید');
|
| return;
|
| }
|
|
|
| const resultDiv = document.getElementById('news-sentiment-result');
|
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل...</div>';
|
|
|
| try {
|
| const response = await fetch('/api/news/analyze', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ title: title, content: content, description: content })
|
| });
|
|
|
| const data = await response.json();
|
|
|
| if (!data.available) {
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-warning">
|
| <strong>⚠️ مدلها در دسترس نیستند:</strong> ${data.news?.error || data.error || 'مدلهای AI در حال حاضر در دسترس نیستند'}
|
| </div>
|
| `;
|
| return;
|
| }
|
|
|
| const newsData = data.news || {};
|
| const sentiment = newsData.sentiment || 'neutral';
|
| const confidence = newsData.confidence || 0;
|
| const sentimentEmoji = sentiment === 'bullish' || sentiment === 'positive' ? '📈' :
|
| sentiment === 'bearish' || sentiment === 'negative' ? '📉' : '➡️';
|
| const sentimentColor = sentiment === 'bullish' || sentiment === 'positive' ? 'var(--success)' :
|
| sentiment === 'bearish' || sentiment === 'negative' ? 'var(--danger)' : 'var(--text-secondary)';
|
|
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-success" style="border-left: 4px solid ${sentimentColor};">
|
| <h4 style="margin-bottom: 15px;">نتیجه تحلیل خبر</h4>
|
| <div style="display: grid; gap: 10px;">
|
| <div>
|
| <strong>عنوان:</strong>
|
| <span style="color: var(--text-primary);">${title || 'بدون عنوان'}</span>
|
| </div>
|
| <div>
|
| <strong>احساسات:</strong>
|
| <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
|
| ${sentimentEmoji} ${sentiment === 'bullish' || sentiment === 'positive' ? 'مثبت' :
|
| sentiment === 'bearish' || sentiment === 'negative' ? 'منفی' : 'خنثی'}
|
| </span>
|
| </div>
|
| <div>
|
| <strong>اعتماد:</strong>
|
| <span style="color: var(--primary); font-weight: 600;">
|
| ${(confidence * 100).toFixed(2)}%
|
| </span>
|
| </div>
|
| </div>
|
| </div>
|
| `;
|
| } catch (error) {
|
| console.error('News sentiment analysis error:', error);
|
| resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
|
| showError('خطا در تحلیل خبر');
|
| }
|
| }
|
|
|
|
|
| async function analyzeSentiment() {
|
| const text = document.getElementById('sentiment-text').value;
|
| const mode = document.getElementById('sentiment-mode').value;
|
| const modelKey = document.getElementById('sentiment-model').value;
|
|
|
| if (!text.trim()) {
|
| showError('لطفاً متنی وارد کنید');
|
| return;
|
| }
|
|
|
| const resultDiv = document.getElementById('sentiment-result');
|
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال تحلیل...</div>';
|
|
|
| try {
|
| let response;
|
|
|
|
|
| response = await fetch('/api/sentiment/analyze', {
|
| method: 'POST',
|
| headers: { 'Content-Type': 'application/json' },
|
| body: JSON.stringify({ text: text, mode: mode })
|
| });
|
|
|
| const data = await response.json();
|
|
|
| if (!data.available) {
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-warning">
|
| <strong>⚠️ مدلها در دسترس نیستند:</strong> ${data.error || 'مدلهای AI در حال حاضر در دسترس نیستند'}
|
| </div>
|
| `;
|
| return;
|
| }
|
|
|
| const label = data.sentiment || 'neutral';
|
| const confidence = data.confidence || 0;
|
| const result = data.result || {};
|
|
|
|
|
| const sentimentEmoji = label === 'bullish' || label === 'positive' ? '📈' :
|
| label === 'bearish' || label === 'negative' ? '📉' : '➡️';
|
| const sentimentColor = label === 'bullish' || label === 'positive' ? 'var(--success)' :
|
| label === 'bearish' || label === 'negative' ? 'var(--danger)' : 'var(--text-secondary)';
|
|
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-success" style="margin-top: 20px; border-left: 4px solid ${sentimentColor};">
|
| <h4 style="margin-bottom: 15px;">نتیجه تحلیل احساسات</h4>
|
| <div style="display: grid; gap: 10px;">
|
| <div>
|
| <strong>احساسات:</strong>
|
| <span style="color: ${sentimentColor}; font-weight: 700; font-size: 18px;">
|
| ${sentimentEmoji} ${label === 'bullish' || label === 'positive' ? 'صعودی/مثبت' :
|
| label === 'bearish' || label === 'negative' ? 'نزولی/منفی' : 'خنثی'}
|
| </span>
|
| </div>
|
| <div>
|
| <strong>اعتماد:</strong>
|
| <span style="color: var(--primary); font-weight: 600;">
|
| ${(confidence * 100).toFixed(2)}%
|
| </span>
|
| </div>
|
| <div>
|
| <strong>نوع تحلیل:</strong>
|
| <span style="color: var(--text-secondary);">${mode}</span>
|
| </div>
|
| <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);">
|
| <strong>متن تحلیل شده:</strong>
|
| <div style="margin-top: 5px; padding: 10px; background: rgba(31, 41, 55, 0.6); border-radius: 5px; font-size: 13px; color: var(--text-secondary);">
|
| "${text.substring(0, 200)}${text.length > 200 ? '...' : ''}"
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| `;
|
|
|
|
|
| saveSentimentToHistory({
|
| text: text.substring(0, 100),
|
| label: label,
|
| confidence: confidence,
|
| model: mode,
|
| timestamp: new Date().toISOString()
|
| });
|
|
|
|
|
| loadSentimentHistory();
|
|
|
| } catch (error) {
|
| console.error('Sentiment analysis error:', error);
|
| resultDiv.innerHTML = `<div class="alert alert-error">خطا در تحلیل: ${error.message}</div>`;
|
| showError('خطا در تحلیل احساسات');
|
| }
|
| }
|
|
|
|
|
| function saveSentimentToHistory(analysis) {
|
| try {
|
| const history = JSON.parse(localStorage.getItem('sentiment_history') || '[]');
|
| history.unshift(analysis);
|
|
|
| if (history.length > 50) history = history.slice(0, 50);
|
| localStorage.setItem('sentiment_history', JSON.stringify(history));
|
| } catch (e) {
|
| console.warn('Could not save to history:', e);
|
| }
|
| }
|
|
|
|
|
| function loadSentimentHistory() {
|
| try {
|
| const history = JSON.parse(localStorage.getItem('sentiment_history') || '[]');
|
| const historyDiv = document.getElementById('sentiment-history');
|
|
|
| if (history.length === 0) {
|
| historyDiv.innerHTML = '<div class="alert alert-warning">هیچ تاریخچهای وجود ندارد</div>';
|
| return;
|
| }
|
|
|
| historyDiv.innerHTML = `
|
| <div style="display: grid; gap: 10px; max-height: 400px; overflow-y: auto;">
|
| ${history.slice(0, 20).map(item => {
|
| const sentimentEmoji = item.label.toUpperCase().includes('POSITIVE') || item.label.toUpperCase().includes('BULLISH') ? '📈' :
|
| item.label.toUpperCase().includes('NEGATIVE') || item.label.toUpperCase().includes('BEARISH') ? '📉' : '➡️';
|
| return `
|
| <div style="padding: 12px; background: rgba(31, 41, 55, 0.6); border-radius: 8px; border-left: 3px solid var(--primary);">
|
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 5px;">
|
| <span style="font-weight: 600;">${sentimentEmoji} ${item.label}</span>
|
| <span style="font-size: 11px; color: var(--text-secondary);">${new Date(item.timestamp).toLocaleString('fa-IR')}</span>
|
| </div>
|
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">${item.text}</div>
|
| <div style="font-size: 11px; color: var(--text-secondary);">
|
| اعتماد: ${(item.confidence * 100).toFixed(0)}% | مدل: ${item.model}
|
| </div>
|
| </div>
|
| `;
|
| }).join('')}
|
| </div>
|
| `;
|
| } catch (e) {
|
| console.warn('Could not load history:', e);
|
| }
|
| }
|
|
|
|
|
| async function loadNews() {
|
| try {
|
|
|
| let response;
|
| try {
|
| response = await fetch('/api/news/latest');
|
| } catch {
|
| response = await fetch('/api/news?limit=20');
|
| }
|
|
|
| const data = await response.json();
|
|
|
| const newsItems = data.news || data.data || [];
|
|
|
| if (newsItems.length > 0) {
|
| const newsDiv = document.getElementById('news-list');
|
| newsDiv.innerHTML = `
|
| <div style="display: grid; gap: 15px;">
|
| ${newsItems.map(item => {
|
| const sentiment = item.sentiment_label || item.sentiment;
|
| const sentimentColor = sentiment === 'positive' || sentiment === 'POSITIVE' ? 'var(--success)' :
|
| sentiment === 'negative' || sentiment === 'NEGATIVE' ? 'var(--danger)' :
|
| 'var(--text-secondary)';
|
| const sentimentEmoji = sentiment === 'positive' || sentiment === 'POSITIVE' ? '📈' :
|
| sentiment === 'negative' || sentiment === 'NEGATIVE' ? '📉' : '➡️';
|
|
|
| return `
|
| <div style="padding: 20px; background: rgba(31, 41, 55, 0.6); border-radius: 12px; border-left: 4px solid ${sentimentColor};">
|
| <h4 style="margin-bottom: 10px; color: var(--text-primary);">${item.title || 'بدون عنوان'}</h4>
|
| ${item.content || item.description ? `<p style="color: var(--text-secondary); margin-bottom: 10px; line-height: 1.6;">${(item.content || item.description).substring(0, 200)}...</p>` : ''}
|
| <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
| <div style="display: flex; gap: 15px; align-items: center;">
|
| <span style="font-size: 12px; color: var(--text-secondary);">
|
| ${item.source || 'Unknown'}
|
| </span>
|
| ${sentiment ? `<span style="font-size: 12px; color: ${sentimentColor};">
|
| ${sentimentEmoji} ${sentiment}
|
| ${item.sentiment_confidence ? ` (${(item.sentiment_confidence * 100).toFixed(0)}%)` : ''}
|
| </span>` : ''}
|
| ${item.published_date || item.published_at ? `<span style="font-size: 12px; color: var(--text-secondary);">
|
| ${new Date(item.published_date || item.published_at).toLocaleDateString('fa-IR')}
|
| </span>` : ''}
|
| </div>
|
| ${item.url ? `<a href="${item.url}" target="_blank" rel="noopener noreferrer" style="color: var(--primary); text-decoration: none; font-weight: 600;">مطالعه بیشتر →</a>` : ''}
|
| </div>
|
| </div>
|
| `;
|
| }).join('')}
|
| </div>
|
| `;
|
| } else {
|
| document.getElementById('news-list').innerHTML = '<div class="alert alert-warning">هیچ خبری یافت نشد</div>';
|
| }
|
| } catch (error) {
|
| console.error('Error loading news:', error);
|
| showError('خطا در بارگذاری اخبار');
|
| document.getElementById('news-list').innerHTML = '<div class="alert alert-error">خطا در بارگذاری اخبار</div>';
|
| }
|
| }
|
|
|
|
|
| async function loadProviders() {
|
| try {
|
|
|
| const [providersRes, healthRes] = await Promise.all([
|
| fetch('/api/providers'),
|
| fetch('/api/providers/health-summary').catch(() => null)
|
| ]);
|
|
|
| const providersData = await providersRes.json();
|
| const providers = providersData.providers || providersData || [];
|
|
|
|
|
| const providersDiv = document.getElementById('providers-list');
|
| if (providersDiv) {
|
| if (providers.length > 0) {
|
| providersDiv.innerHTML = `
|
| <div style="overflow-x: auto;">
|
| <table>
|
| <thead>
|
| <tr>
|
| <th>ID</th>
|
| <th>نام</th>
|
| <th>دسته</th>
|
| <th>نوع</th>
|
| <th>وضعیت</th>
|
| <th>جزئیات</th>
|
| </tr>
|
| </thead>
|
| <tbody>
|
| ${providers.map(provider => {
|
| const status = provider.status || 'unknown';
|
| const statusConfig = {
|
| 'VALID': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ معتبر' },
|
| 'validated': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ معتبر' },
|
| 'available': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ در دسترس' },
|
| 'online': { color: 'var(--success)', bg: 'rgba(16, 185, 129, 0.2)', text: '✅ آنلاین' },
|
| 'CONDITIONALLY_AVAILABLE': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ شرطی' },
|
| 'INVALID': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ نامعتبر' },
|
| 'unvalidated': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ نامعتبر' },
|
| 'not_loaded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ بارگذاری نشده' },
|
| 'offline': { color: 'var(--danger)', bg: 'rgba(239, 68, 68, 0.2)', text: '❌ آفلاین' },
|
| 'degraded': { color: 'var(--warning)', bg: 'rgba(245, 158, 11, 0.2)', text: '⚠️ تخریب شده' }
|
| };
|
| const statusInfo = statusConfig[status] || { color: 'var(--text-secondary)', bg: 'rgba(156, 163, 175, 0.2)', text: '❓ نامشخص' };
|
|
|
| return `
|
| <tr>
|
| <td>${provider.provider_id || provider.id || '-'}</td>
|
| <td><strong>${provider.name || 'Unknown'}</strong></td>
|
| <td>${provider.category || '-'}</td>
|
| <td>${provider.type || '-'}</td>
|
| <td>
|
| <span style="padding: 5px 10px; border-radius: 5px; background: ${statusInfo.bg}; color: ${statusInfo.color}; font-size: 12px;">
|
| ${statusInfo.text}
|
| </span>
|
| </td>
|
| <td>
|
| ${provider.response_time_ms ? `<span style="font-size: 12px; color: var(--text-secondary);">${provider.response_time_ms}ms</span>` : ''}
|
| ${provider.endpoint ? `<a href="${provider.endpoint}" target="_blank" style="color: var(--primary); font-size: 12px;">🔗</a>` : ''}
|
| ${provider.error_reason ? `<span style="font-size: 11px; color: var(--danger);" title="${provider.error_reason}">⚠️</span>` : ''}
|
| </td>
|
| </tr>
|
| `;
|
| }).join('')}
|
| </tbody>
|
| </table>
|
| </div>
|
| <div style="margin-top: 15px; padding: 15px; background: rgba(102, 126, 234, 0.1); border-radius: 10px;">
|
| <strong>کل Providerها:</strong> ${providersData.total || providers.length}
|
| </div>
|
| `;
|
| } else {
|
| providersDiv.innerHTML = '<div class="alert alert-warning">هیچ Providerی یافت نشد</div>';
|
| }
|
| }
|
|
|
|
|
| if (healthRes) {
|
| try {
|
| const healthData = await healthRes.json();
|
| const healthSummaryDiv = document.getElementById('providers-health-summary');
|
| if (healthSummaryDiv && healthData.ok && healthData.summary) {
|
| const summary = healthData.summary;
|
| healthSummaryDiv.innerHTML = `
|
| <div class="card">
|
| <h3>Provider Health Summary</h3>
|
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-top: 15px;">
|
| <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border-left: 4px solid var(--success);">
|
| <div style="font-size: 24px; font-weight: bold; color: var(--success);">${summary.total_active_providers || 0}</div>
|
| <div style="font-size: 12px; color: var(--text-secondary);">Total Active</div>
|
| </div>
|
| <div style="padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border-left: 4px solid var(--success);">
|
| <div style="font-size: 24px; font-weight: bold; color: var(--success);">${summary.http_valid || 0}</div>
|
| <div style="font-size: 12px; color: var(--text-secondary);">HTTP Valid</div>
|
| </div>
|
| <div style="padding: 15px; background: rgba(239, 68, 68, 0.1); border-radius: 10px; border-left: 4px solid var(--danger);">
|
| <div style="font-size: 24px; font-weight: bold; color: var(--danger);">${summary.http_invalid || 0}</div>
|
| <div style="font-size: 12px; color: var(--text-secondary);">HTTP Invalid</div>
|
| </div>
|
| <div style="padding: 15px; background: rgba(245, 158, 11, 0.1); border-radius: 10px; border-left: 4px solid var(--warning);">
|
| <div style="font-size: 24px; font-weight: bold; color: var(--warning);">${summary.http_conditional || 0}</div>
|
| <div style="font-size: 12px; color: var(--text-secondary);">Conditional</div>
|
| </div>
|
| </div>
|
| </div>
|
| `;
|
| }
|
| } catch (e) {
|
| console.warn('Could not load health summary:', e);
|
| }
|
| }
|
|
|
| } catch (error) {
|
| console.error('Error loading providers:', error);
|
| showError('خطا در بارگذاری Providerها');
|
| const providersDiv = document.getElementById('providers-list');
|
| if (providersDiv) {
|
| providersDiv.innerHTML = '<div class="alert alert-error">خطا در بارگذاری Providerها</div>';
|
| }
|
| }
|
| }
|
|
|
|
|
| async function searchResources() {
|
| const query = document.getElementById('search-resources').value;
|
| if (!query.trim()) {
|
| showError('لطفاً عبارتی برای جستجو وارد کنید');
|
| return;
|
| }
|
|
|
| const resultsDiv = document.getElementById('search-results');
|
| resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال جستجو...</div>';
|
|
|
| try {
|
| const response = await fetch(`/api/resources/search?q=${encodeURIComponent(query)}`);
|
| const data = await response.json();
|
|
|
| if (data.success && data.resources && data.resources.length > 0) {
|
| resultsDiv.innerHTML = `
|
| <div style="margin-top: 15px;">
|
| <div style="margin-bottom: 10px; color: var(--text-secondary);">
|
| ${data.count || data.resources.length} نتیجه یافت شد
|
| </div>
|
| <div style="display: grid; gap: 10px;">
|
| ${data.resources.map(resource => `
|
| <div style="padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 10px; border-left: 4px solid var(--primary);">
|
| <div style="display: flex; justify-content: space-between; align-items: start; flex-wrap: wrap; gap: 10px;">
|
| <div>
|
| <strong style="font-size: 16px;">${resource.name || 'Unknown'}</strong>
|
| <div style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;">
|
| دسته: ${resource.category || 'N/A'}
|
| </div>
|
| ${resource.base_url ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 3px; font-family: monospace;">
|
| ${resource.base_url}
|
| </div>` : ''}
|
| </div>
|
| ${resource.free !== undefined ? `
|
| <span style="padding: 5px 10px; border-radius: 5px; background: ${resource.free ? 'rgba(16, 185, 129, 0.2)' : 'rgba(245, 158, 11, 0.2)'}; color: ${resource.free ? 'var(--success)' : 'var(--warning)'}; font-size: 12px;">
|
| ${resource.free ? '🆓 رایگان' : '💰 پولی'}
|
| </span>
|
| ` : ''}
|
| </div>
|
| </div>
|
| `).join('')}
|
| </div>
|
| </div>
|
| `;
|
| } else {
|
| resultsDiv.innerHTML = '<div class="alert alert-warning" style="margin-top: 15px;">نتیجهای یافت نشد</div>';
|
| }
|
| } catch (error) {
|
| console.error('Search error:', error);
|
| resultsDiv.innerHTML = '<div class="alert alert-error" style="margin-top: 15px;">خطا در جستجو</div>';
|
| showError('خطا در جستجو');
|
| }
|
| }
|
|
|
|
|
| async function loadDiagnostics() {
|
| try {
|
|
|
| try {
|
| const statusRes = await fetch('/api/status');
|
| const statusData = await statusRes.json();
|
|
|
| const statusDiv = document.getElementById('diagnostics-status');
|
| const health = statusData.system_health || 'unknown';
|
| const healthClass = health === 'healthy' ? 'alert-success' :
|
| health === 'degraded' ? 'alert-warning' : 'alert-error';
|
|
|
| statusDiv.innerHTML = `
|
| <div class="alert ${healthClass}">
|
| <h4 style="margin-bottom: 10px;">وضعیت سیستم</h4>
|
| <div style="display: grid; gap: 5px;">
|
| <div><strong>وضعیت کلی:</strong> ${health}</div>
|
| <div><strong>APIهای کل:</strong> ${statusData.total_apis || 0}</div>
|
| <div><strong>آنلاین:</strong> ${statusData.online || 0}</div>
|
| <div><strong>تخریب شده:</strong> ${statusData.degraded || 0}</div>
|
| <div><strong>آفلاین:</strong> ${statusData.offline || 0}</div>
|
| <div><strong>میانگین زمان پاسخ:</strong> ${statusData.avg_response_time_ms || 0}ms</div>
|
| ${statusData.last_update ? `<div><strong>آخرین بهروزرسانی:</strong> ${new Date(statusData.last_update).toLocaleString('fa-IR')}</div>` : ''}
|
| </div>
|
| </div>
|
| `;
|
| } catch (statusError) {
|
| document.getElementById('diagnostics-status').innerHTML = '<div class="alert alert-error">خطا در بارگذاری وضعیت سیستم</div>';
|
| }
|
|
|
|
|
| try {
|
| const errorsRes = await fetch('/api/logs/errors');
|
| const errorsData = await errorsRes.json();
|
|
|
| const errors = errorsData.errors || errorsData.error_logs || [];
|
| const errorsDiv = document.getElementById('error-logs');
|
|
|
| if (errors.length > 0) {
|
| errorsDiv.innerHTML = `
|
| <div style="display: grid; gap: 10px;">
|
| ${errors.slice(0, 10).map(error => `
|
| <div style="padding: 15px; background: rgba(239, 68, 68, 0.1); border-left: 4px solid var(--danger); border-radius: 5px;">
|
| <div style="font-weight: 600; color: var(--danger); margin-bottom: 5px;">
|
| ${error.message || error.error_message || error.type || 'خطا'}
|
| </div>
|
| ${error.error_type ? `<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 3px;">نوع: ${error.error_type}</div>` : ''}
|
| ${error.provider ? `<div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 3px;">Provider: ${error.provider}</div>` : ''}
|
| <div style="font-size: 11px; color: var(--text-secondary); margin-top: 5px;">
|
| ${error.timestamp ? new Date(error.timestamp).toLocaleString('fa-IR') : ''}
|
| </div>
|
| </div>
|
| `).join('')}
|
| </div>
|
| ${errors.length > 10 ? `<div style="margin-top: 10px; text-align: center; color: var(--text-secondary); font-size: 12px;">
|
| نمایش ${Math.min(10, errors.length)} از ${errors.length} خطا
|
| </div>` : ''}
|
| `;
|
| } else {
|
| errorsDiv.innerHTML = '<div class="alert alert-success">هیچ خطایی یافت نشد ✅</div>';
|
| }
|
| } catch (errorsError) {
|
| document.getElementById('error-logs').innerHTML = '<div class="alert alert-warning">خطا در بارگذاری گزارش خطاها</div>';
|
| }
|
|
|
|
|
| try {
|
| const logsRes = await fetch('/api/logs/recent');
|
| const logsData = await logsRes.json();
|
|
|
| const logs = logsData.logs || logsData.recent || [];
|
| const logsDiv = document.getElementById('recent-logs');
|
|
|
| if (logs.length > 0) {
|
| logsDiv.innerHTML = `
|
| <div style="display: grid; gap: 10px; max-height: 400px; overflow-y: auto;">
|
| ${logs.slice(0, 20).map(log => {
|
| const level = log.level || log.status || 'info';
|
| const levelColor = level === 'ERROR' ? 'var(--danger)' :
|
| level === 'WARNING' ? 'var(--warning)' :
|
| 'var(--text-secondary)';
|
|
|
| return `
|
| <div style="padding: 12px; background: rgba(31, 41, 55, 0.6); border-left: 3px solid ${levelColor}; border-radius: 5px;">
|
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 5px;">
|
| <div style="font-size: 12px; font-weight: 600; color: ${levelColor};">
|
| ${level}
|
| </div>
|
| <div style="font-size: 11px; color: var(--text-secondary);">
|
| ${log.timestamp ? new Date(log.timestamp).toLocaleString('fa-IR') : ''}
|
| </div>
|
| </div>
|
| <div style="font-size: 13px; color: var(--text-primary);">
|
| ${log.message || log.content || JSON.stringify(log)}
|
| </div>
|
| ${log.provider ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 3px;">Provider: ${log.provider}</div>` : ''}
|
| </div>
|
| `;
|
| }).join('')}
|
| </div>
|
| `;
|
| } else {
|
| logsDiv.innerHTML = '<div class="alert alert-warning">هیچ لاگی یافت نشد</div>';
|
| }
|
| } catch (logsError) {
|
| document.getElementById('recent-logs').innerHTML = '<div class="alert alert-warning">خطا در بارگذاری لاگها</div>';
|
| }
|
| } catch (error) {
|
| console.error('Error loading diagnostics:', error);
|
| showError('خطا در بارگذاری خطایابی');
|
| }
|
| }
|
|
|
|
|
| async function runDiagnostics() {
|
| try {
|
| const response = await fetch('/api/diagnostics/run', { method: 'POST' });
|
| const data = await response.json();
|
|
|
| if (data.success) {
|
| showSuccess('تشخیص با موفقیت اجرا شد');
|
| setTimeout(loadDiagnostics, 1000);
|
| } else {
|
| showError(data.error || 'خطا در اجرای تشخیص');
|
| }
|
| } catch (error) {
|
| showError('خطا در اجرای تشخیص: ' + error.message);
|
| }
|
| }
|
|
|
|
|
| async function testAPI() {
|
| const endpoint = document.getElementById('api-endpoint').value;
|
| const method = document.getElementById('api-method').value;
|
| const bodyText = document.getElementById('api-body').value;
|
|
|
| if (!endpoint) {
|
| showError('لطفاً یک endpoint انتخاب کنید');
|
| return;
|
| }
|
|
|
| const resultDiv = document.getElementById('api-result');
|
| resultDiv.innerHTML = '<div class="loading"><div class="spinner"></div> در حال ارسال درخواست...</div>';
|
|
|
| try {
|
| const options = { method };
|
|
|
|
|
| let body = null;
|
| if (method === 'POST' && bodyText) {
|
| try {
|
| body = JSON.parse(bodyText);
|
| options.headers = { 'Content-Type': 'application/json' };
|
| } catch (e) {
|
| showError('JSON نامعتبر در body');
|
| resultDiv.innerHTML = '<div class="alert alert-error">خطا در پارس JSON</div>';
|
| return;
|
| }
|
| }
|
|
|
| if (body) {
|
| options.body = JSON.stringify(body);
|
| }
|
|
|
| const startTime = Date.now();
|
| const response = await fetch(endpoint, options);
|
| const responseTime = Date.now() - startTime;
|
|
|
| let data;
|
| const contentType = response.headers.get('content-type');
|
|
|
| if (contentType && contentType.includes('application/json')) {
|
| data = await response.json();
|
| } else {
|
| data = { text: await response.text() };
|
| }
|
|
|
| const statusClass = response.ok ? 'alert-success' : 'alert-error';
|
| const statusEmoji = response.ok ? '✅' : '❌';
|
|
|
| resultDiv.innerHTML = `
|
| <div style="margin-top: 20px;">
|
| <div class="alert ${statusClass}" style="margin-bottom: 15px;">
|
| <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;">
|
| <div>
|
| <strong>${statusEmoji} وضعیت:</strong> ${response.status} ${response.statusText}
|
| </div>
|
| <div style="font-size: 12px; color: var(--text-secondary);">
|
| زمان پاسخ: ${responseTime}ms
|
| </div>
|
| </div>
|
| </div>
|
| <div style="padding: 15px; background: rgba(31, 41, 55, 0.6); border-radius: 10px;">
|
| <h4 style="margin-bottom: 10px;">پاسخ:</h4>
|
| <pre style="background: rgba(0, 0, 0, 0.3); padding: 15px; border-radius: 5px; overflow-x: auto; margin-top: 10px; font-size: 12px; max-height: 500px; overflow-y: auto;">${JSON.stringify(data, null, 2)}</pre>
|
| </div>
|
| <div style="margin-top: 10px; padding: 10px; background: rgba(102, 126, 234, 0.1); border-radius: 5px; font-size: 12px; color: var(--text-secondary);">
|
| <strong>Endpoint:</strong> ${method} ${endpoint}
|
| </div>
|
| </div>
|
| `;
|
| } catch (error) {
|
| resultDiv.innerHTML = `
|
| <div class="alert alert-error" style="margin-top: 20px;">
|
| <h4>خطا:</h4>
|
| <p>${error.message}</p>
|
| </div>
|
| `;
|
| showError('خطا در تست API: ' + error.message);
|
| }
|
| }
|
|
|
|
|
| function showError(message) {
|
| const alert = document.createElement('div');
|
| alert.className = 'alert alert-error';
|
| alert.textContent = message;
|
| document.body.appendChild(alert);
|
| setTimeout(() => alert.remove(), 5000);
|
| }
|
|
|
| function showSuccess(message) {
|
| const alert = document.createElement('div');
|
| alert.className = 'alert alert-success';
|
| alert.textContent = message;
|
| document.body.appendChild(alert);
|
| setTimeout(() => alert.remove(), 5000);
|
| }
|
|
|
|
|
| async function loadMonitorData() {
|
|
|
| try {
|
| const response = await fetch('/api/status');
|
| const data = await response.json();
|
| const monitorContainer = document.getElementById('monitor-content');
|
| if (monitorContainer) {
|
| monitorContainer.innerHTML = `
|
| <div class="card">
|
| <h3>API Status</h3>
|
| <pre>${JSON.stringify(data, null, 2)}</pre>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading monitor data:', error);
|
| }
|
| }
|
|
|
| async function loadAdvancedData() {
|
|
|
| loadAPIEndpoints();
|
| loadDiagnostics();
|
| }
|
|
|
| async function loadAdminData() {
|
|
|
| try {
|
| const [providersRes, modelsRes] = await Promise.all([
|
| fetch('/api/providers'),
|
| fetch('/api/models/status')
|
| ]);
|
| const providers = await providersRes.json();
|
| const models = await modelsRes.json();
|
|
|
| const adminContainer = document.getElementById('admin-content');
|
| if (adminContainer) {
|
| adminContainer.innerHTML = `
|
| <div class="card">
|
| <h3>System Status</h3>
|
| <p>Providers: ${providers.total || 0}</p>
|
| <p>Models: ${models.models_loaded || 0} loaded</p>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading admin data:', error);
|
| }
|
| }
|
|
|
| async function loadHFHealth() {
|
|
|
| try {
|
| const response = await fetch('/api/models/status');
|
| const data = await response.json();
|
| const hfContainer = document.getElementById('hf-status');
|
| if (hfContainer) {
|
| hfContainer.innerHTML = `
|
| <div class="card">
|
| <h3>HF Models Status</h3>
|
| <p>Mode: ${data.hf_mode || 'unknown'}</p>
|
| <p>Loaded: ${data.models_loaded || 0}</p>
|
| <p>Failed: ${data.failed_count || 0}</p>
|
| <p>Status: ${data.status || 'unknown'}</p>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading HF health:', error);
|
| }
|
| }
|
|
|
| async function loadPools() {
|
|
|
| try {
|
| const response = await fetch('/api/pools');
|
| const data = await response.json();
|
| const poolsContainer = document.getElementById('pools-content');
|
| if (poolsContainer) {
|
| poolsContainer.innerHTML = `
|
| <div class="card">
|
| <h3>Provider Pools</h3>
|
| <p>${data.message || 'No pools available'}</p>
|
| <pre>${JSON.stringify(data, null, 2)}</pre>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading pools:', error);
|
| }
|
| }
|
|
|
| async function loadLogs() {
|
|
|
| try {
|
| const response = await fetch('/api/logs/recent');
|
| const data = await response.json();
|
| const logsContainer = document.getElementById('logs-content');
|
| if (logsContainer) {
|
| const logsHtml = data.logs && data.logs.length > 0
|
| ? data.logs.map(log => `<div class="log-entry">${JSON.stringify(log)}</div>`).join('')
|
| : '<p>No logs available</p>';
|
| logsContainer.innerHTML = `<div class="card"><h3>Recent Logs</h3>${logsHtml}</div>`;
|
| }
|
| } catch (error) {
|
| console.error('Error loading logs:', error);
|
| }
|
| }
|
|
|
| async function loadReports() {
|
|
|
| try {
|
| const response = await fetch('/api/providers/health-summary');
|
| const data = await response.json();
|
| const reportsContainer = document.getElementById('reports-content');
|
| if (reportsContainer) {
|
| reportsContainer.innerHTML = `
|
| <div class="card">
|
| <h3>Provider Health Report</h3>
|
| <pre>${JSON.stringify(data, null, 2)}</pre>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading reports:', error);
|
| }
|
| }
|
|
|
| async function loadResources() {
|
|
|
| try {
|
| const response = await fetch('/api/resources');
|
| const data = await response.json();
|
| const resourcesContainer = document.getElementById('resources-summary');
|
| if (resourcesContainer) {
|
| const summary = data.summary || {};
|
| resourcesContainer.innerHTML = `
|
| <div class="card">
|
| <h3>Resources Summary</h3>
|
| <p>Total: ${summary.total_resources || 0}</p>
|
| <p>Free: ${summary.free_resources || 0}</p>
|
| <p>Models: ${summary.models_available || 0}</p>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading resources:', error);
|
| }
|
| }
|
|
|
| async function loadAPIRegistry() {
|
|
|
| try {
|
| const response = await fetch('/api/resources/apis');
|
| const data = await response.json();
|
|
|
| if (!data.ok) {
|
| console.warn('API registry not available:', data.error);
|
| return;
|
| }
|
|
|
| const registryContainer = document.getElementById('api-registry-section');
|
| if (registryContainer) {
|
| const metadata = data.metadata || {};
|
| const categories = data.categories || [];
|
|
|
| registryContainer.innerHTML = `
|
| <div class="card">
|
| <h3>API Registry: ${metadata.name || 'Unknown'}</h3>
|
| <p>Version: ${metadata.version || 'N/A'}</p>
|
| <p>Description: ${metadata.description || 'N/A'}</p>
|
| <h4>Categories:</h4>
|
| <ul>
|
| ${categories.map(cat => `<li>${cat}</li>`).join('')}
|
| </ul>
|
| <p>Total Files: ${data.total_raw_files || 0}</p>
|
| </div>
|
| `;
|
| }
|
|
|
|
|
| const metadataContainer = document.getElementById('api-registry-metadata');
|
| if (metadataContainer) {
|
| metadataContainer.innerHTML = `
|
| <div class="card">
|
| <h4>Metadata</h4>
|
| <pre>${JSON.stringify(metadata, null, 2)}</pre>
|
| </div>
|
| `;
|
| }
|
| } catch (error) {
|
| console.error('Error loading API registry:', error);
|
| }
|
| }
|
|
|
|
|