diff --git "a/templates/index.html" "b/templates/index.html" --- "a/templates/index.html" +++ "b/templates/index.html" @@ -1070,15 +1070,834 @@ transform: translateX(400%); } } + + /* === Modern UI Enhancements === */ + + /* Ripple Effect for Buttons */ + .ripple { + position: relative; + overflow: hidden; + } + + .ripple::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.5); + transform: translate(-50%, -50%); + transition: width 0.6s, height 0.6s; + } + + .ripple:active::after { + width: 300px; + height: 300px; + } + + /* Enhanced Card Animations */ + .stat-card, + .market-section, + .chart-container { + animation: cardFadeIn 0.6s ease-out; + animation-fill-mode: both; + } + + .stat-card:nth-child(1) { animation-delay: 0.1s; } + .stat-card:nth-child(2) { animation-delay: 0.2s; } + .stat-card:nth-child(3) { animation-delay: 0.3s; } + .stat-card:nth-child(4) { animation-delay: 0.4s; } + .stat-card:nth-child(5) { animation-delay: 0.5s; } + + @keyframes cardFadeIn { + from { + opacity: 0; + transform: translateY(30px) scale(0.95); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } + } + + /* Number Counter Animation */ + .number-counter { + display: inline-block; + transition: all 0.3s ease; + } + + .number-counter.updated { + animation: numberPop 0.5s ease; + } + + @keyframes numberPop { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.15); color: var(--accent-blue); } + } + + /* Skeleton Loading */ + .skeleton-loader { + background: linear-gradient( + 90deg, + rgba(255, 255, 255, 0.05) 25%, + rgba(255, 255, 255, 0.15) 50%, + rgba(255, 255, 255, 0.05) 75% + ); + background-size: 200% 100%; + animation: skeleton-loading 1.5s ease-in-out infinite; + border-radius: 8px; + } + + @keyframes skeleton-loading { + 0% { background-position: 200% 0; } + 100% { background-position: -200% 0; } + } + + .skeleton-text { + height: 16px; + margin-bottom: 8px; + } + + .skeleton-title { + height: 24px; + width: 60%; + margin-bottom: 16px; + } + + .skeleton-avatar { + width: 40px; + height: 40px; + border-radius: 50%; + } + + /* Enhanced Table Row Animations */ + table tbody tr { + animation: rowSlideIn 0.4s ease-out; + animation-fill-mode: both; + } + + table tbody tr:nth-child(1) { animation-delay: 0.05s; } + table tbody tr:nth-child(2) { animation-delay: 0.1s; } + table tbody tr:nth-child(3) { animation-delay: 0.15s; } + table tbody tr:nth-child(4) { animation-delay: 0.2s; } + table tbody tr:nth-child(5) { animation-delay: 0.25s; } + table tbody tr:nth-child(n+6) { animation-delay: 0.3s; } + + @keyframes rowSlideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } + } + + /* Enhanced Hover Effects */ + tr { + transition: all 0.2s ease; + cursor: pointer; + } + + tr:hover { + background: rgba(59, 130, 246, 0.1) !important; + transform: translateX(5px); + box-shadow: -5px 0 0 var(--accent-blue); + } + + /* Search Bar */ + .search-container { + position: relative; + margin-bottom: 20px; + } + + .search-input { + width: 100%; + padding: 14px 20px 14px 50px; + background: rgba(17, 24, 39, 0.8); + border: 2px solid var(--border); + border-radius: 12px; + color: var(--text-primary); + font-size: 14px; + transition: all 0.3s ease; + } + + .search-input:focus { + outline: none; + border-color: var(--accent-blue); + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); + background: rgba(17, 24, 39, 0.95); + } + + .search-icon { + position: absolute; + left: 18px; + top: 50%; + transform: translateY(-50%); + color: var(--text-secondary); + font-size: 18px; + pointer-events: none; + } + + /* Filter Chips */ + .filter-chips { + display: flex; + gap: 10px; + flex-wrap: wrap; + margin-bottom: 20px; + } + + .filter-chip { + padding: 8px 16px; + background: rgba(17, 24, 39, 0.6); + border: 1px solid var(--border); + border-radius: 20px; + color: var(--text-secondary); + font-size: 13px; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + } + + .filter-chip:hover { + border-color: var(--accent-blue); + color: var(--accent-blue); + transform: translateY(-2px); + } + + .filter-chip.active { + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + border-color: transparent; + color: white; + box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4); + } + + /* Enhanced Toast Notifications */ + .toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 10000; + display: flex; + flex-direction: column; + gap: 12px; + max-width: 400px; + } + + .toast { + position: relative; + padding: 16px 20px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + gap: 12px; + min-width: 300px; + animation: toastSlideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + backdrop-filter: blur(20px); + } + + @keyframes toastSlideIn { + from { + opacity: 0; + transform: translateX(400px) scale(0.8); + } + to { + opacity: 1; + transform: translateX(0) scale(1); + } + } + + .toast-icon { + font-size: 24px; + flex-shrink: 0; + } + + .toast-content { + flex: 1; + } + + .toast-title { + font-weight: 700; + font-size: 14px; + margin-bottom: 4px; + } + + .toast-message { + font-size: 13px; + color: var(--text-secondary); + } + + .toast-close { + background: none; + border: none; + color: var(--text-secondary); + font-size: 20px; + cursor: pointer; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 6px; + transition: all 0.2s ease; + } + + .toast-close:hover { + background: rgba(255, 255, 255, 0.1); + color: var(--text-primary); + } + + /* Progress Indicator */ + .progress-indicator { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 3px; + background: rgba(255, 255, 255, 0.1); + z-index: 10001; + overflow: hidden; + } + + .progress-bar { + height: 100%; + background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink)); + width: 0%; + transition: width 0.3s ease; + animation: progress-shimmer 2s infinite; + } + + @keyframes progress-shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } + } + + /* Floating Action Button */ + .fab { + position: fixed; + bottom: 30px; + right: 30px; + width: 60px; + height: 60px; + border-radius: 50%; + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + border: none; + color: white; + font-size: 24px; + cursor: pointer; + box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4); + transition: all 0.3s ease; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + } + + .fab:hover { + transform: scale(1.1) rotate(90deg); + box-shadow: 0 15px 40px rgba(59, 130, 246, 0.6); + } + + .fab:active { + transform: scale(0.95); + } + + /* Success/Error Feedback */ + .feedback-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.7); + backdrop-filter: blur(5px); + z-index: 10002; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + } + + .feedback-overlay.show { + opacity: 1; + pointer-events: all; + } + + .feedback-card { + background: var(--bg-card); + border-radius: 20px; + padding: 40px; + text-align: center; + max-width: 400px; + border: 2px solid var(--border); + transform: scale(0.8); + transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + } + + .feedback-overlay.show .feedback-card { + transform: scale(1); + } + + .feedback-icon { + font-size: 64px; + margin-bottom: 20px; + animation: feedbackBounce 0.6s ease; + } + + @keyframes feedbackBounce { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.2); } + } + + .feedback-title { + font-size: 24px; + font-weight: 800; + margin-bottom: 10px; + } + + .feedback-message { + color: var(--text-secondary); + margin-bottom: 30px; + } + + /* Pulse Animation for Live Data */ + .pulse-data { + animation: pulseGlow 2s ease-in-out infinite; + } + + @keyframes pulseGlow { + 0%, 100% { + box-shadow: 0 0 5px rgba(59, 130, 246, 0.5); + } + 50% { + box-shadow: 0 0 20px rgba(59, 130, 246, 0.8), 0 0 30px rgba(59, 130, 246, 0.4); + } + } + + /* Smooth Scroll */ + html { + scroll-behavior: smooth; + } + + /* Enhanced Focus States */ + *:focus-visible { + outline: 2px solid var(--accent-blue); + outline-offset: 2px; + border-radius: 4px; + } + + /* Loading Overlay */ + .loading-overlay { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(10, 14, 26, 0.9); + backdrop-filter: blur(10px); + z-index: 10003; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + opacity: 0; + pointer-events: none; + transition: opacity 0.3s ease; + } + + .loading-overlay.show { + opacity: 1; + pointer-events: all; + } + + .loading-spinner-large { + width: 80px; + height: 80px; + border: 6px solid var(--border); + border-top-color: var(--accent-blue); + border-radius: 50%; + animation: spin 1s linear infinite; + } + + .loading-text { + font-size: 18px; + font-weight: 600; + color: var(--text-primary); + } + + /* Tooltip */ + .tooltip { + position: relative; + cursor: help; + } + + .tooltip::before { + content: attr(data-tooltip); + position: absolute; + bottom: 100%; + left: 50%; + transform: translateX(-50%) translateY(-10px); + padding: 8px 12px; + background: var(--bg-card); + border: 1px solid var(--border); + border-radius: 8px; + font-size: 12px; + white-space: nowrap; + opacity: 0; + pointer-events: none; + transition: all 0.3s ease; + z-index: 1000; + } + + .tooltip:hover::before { + opacity: 1; + transform: translateX(-50%) translateY(-5px); + } + + /* Gradient Text Animation */ + .gradient-text { + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple), var(--accent-pink)); + background-size: 200% 200%; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + animation: gradientShift 3s ease infinite; + } + + @keyframes gradientShift { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } + } + + /* Badge Pulse */ + .badge-pulse { + animation: badgePulse 2s ease-in-out infinite; + } + + @keyframes badgePulse { + 0%, 100% { transform: scale(1); } + 50% { transform: scale(1.1); } + } + + /* Smooth Transitions for All Interactive Elements */ + button, a, input, select, textarea { + transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); + } + + /* Enhanced Table Styling */ + table { + border-collapse: separate; + border-spacing: 0; + } + + thead th:first-child { + border-top-left-radius: 12px; + } + + thead th:last-child { + border-top-right-radius: 12px; + } + + tbody tr:last-child td:first-child { + border-bottom-left-radius: 12px; + } + + tbody tr:last-child td:last-child { + border-bottom-right-radius: 12px; + } + + /* SVG Icon Styles */ + .icon { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + + .icon-sm { + width: 16px; + height: 16px; + } + + .icon-md { + width: 24px; + height: 24px; + } + + .icon-lg { + width: 32px; + height: 32px; + } + + .icon-xl { + width: 48px; + height: 48px; + } + + .icon svg { + width: 100%; + height: 100%; + stroke: currentColor; + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + } + + .icon-filled svg { + fill: currentColor; + stroke: none; + } + + .icon-gradient { + background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; + background-clip: text; + } + + /* Tab Icons */ + .tab-icon { + width: 18px; + height: 18px; + margin-right: 8px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .tab-icon svg { + width: 100%; + height: 100%; + } + + /* Status Icons */ + .status-icon { + width: 20px; + height: 20px; + display: inline-flex; + align-items: center; + justify-content: center; + } + + .status-icon-success svg { + color: var(--accent-green); + } + + .status-icon-error svg { + color: var(--accent-red); + } + + .status-icon-warning svg { + color: var(--accent-yellow); + } + + .status-icon-info svg { + color: var(--accent-blue); + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +
+
+
در حال بارگذاری...
+
+ + +
+
+ + + + +
+
+ + + +
در حال اتصال... -
0
+
0
@@ -1093,7 +1912,10 @@
-
● LIVE
+
+ + LIVE +
All Systems Operational @@ -1103,15 +1925,42 @@
- - - - - - - - - + + + + + + + + +
@@ -1215,8 +2064,38 @@
-
💎 Live Market Data
- +
+ + Live Market Data +
+ +
+ + +
+ + +
+ + +
+ + + + +
@@ -1265,7 +2144,10 @@
-
🔥 Trending Now
+
+ + Trending Now +
@@ -1344,8 +2226,14 @@
-
📊 API Providers Status
- +
+ + API Providers Status +
+
@@ -1368,7 +2256,10 @@
-
🤗 HuggingFace Sentiment Analysis
+
+ + HuggingFace Sentiment Analysis +
-
📋 Current API Sources
+
+ + Current API Sources +
Loading...
-
⚙️ Settings
+
+ + Settings +
@@ -1528,7 +2428,10 @@ Market is bullish today
-
📊 Statistics
+
+ + Statistics +
0
@@ -1550,8 +2453,14 @@ Market is bullish today
-
📊 Health Status
- +
+ + Health Status +
+
Loading...
@@ -1596,7 +2505,10 @@ Market is bullish today
-
💭 Sentiment Analysis
+
+ + Sentiment Analysis +
- +
@@ -1617,17 +2532,30 @@ Crypto market is bullish today
-
📋 Log Management
+
+ + Log Management +
- - - - + + + +
@@ -1810,16 +2738,42 @@ Crypto market is bullish today
+ + +
-
🔍 اشکال‌یابی خودکار
+
+ + System Diagnostics +
- - - + + +
@@ -1833,8 +2787,14 @@ Crypto market is bullish today
-
🤖 گزارش Auto-Discovery Service
- +
+ + Auto-Discovery Service Report +
+
@@ -1845,8 +2805,14 @@ Crypto market is bullish today
-
🧠 گزارش وضعیت مدل‌های HuggingFace
- +
+ + HuggingFace Models Status Report +
+
@@ -2016,116 +2982,317 @@ Crypto market is bullish today // Market Data Functions async function loadMarketData() { try { + // Show loading state + const marketTableBody = document.getElementById('marketTableBody'); + if (marketTableBody) { + marketTableBody.innerHTML = '
'; + } + + showProgress(60); + + const [marketRes, statsRes, sentimentRes, trendingRes, defiRes] = await Promise.all([ + fetch('/api/market'), + fetch('/api/stats'), + fetch('/api/sentiment'), + fetch('/api/trending'), + fetch('/api/defi') + ]); + + showProgress(80); + + // Check if responses are OK + if (!marketRes.ok) throw new Error(`خطا در دریافت داده‌های بازار: ${marketRes.status}`); + if (!statsRes.ok) throw new Error(`خطا در دریافت آمار: ${statsRes.status}`); + if (!sentimentRes.ok) throw new Error(`خطا در دریافت احساسات: ${sentimentRes.status}`); + if (!trendingRes.ok) throw new Error(`خطا در دریافت ترندها: ${trendingRes.status}`); + if (!defiRes.ok) throw new Error(`خطا در دریافت DeFi: ${defiRes.status}`); + const [market, stats, sentiment, trending, defi] = await Promise.all([ - fetch('/api/market').then(r => r.json()), - fetch('/api/stats').then(r => r.json()), - fetch('/api/sentiment').then(r => r.json()), - fetch('/api/trending').then(r => r.json()), - fetch('/api/defi').then(r => r.json()) + marketRes.json(), + statsRes.json(), + sentimentRes.json(), + trendingRes.json(), + defiRes.json() ]); + // Validate data with more detailed checks + if (!market || !Array.isArray(market.cryptocurrencies)) { + throw new Error('داده‌های بازار نامعتبر است: cryptocurrencies array not found'); + } + if (!stats || typeof stats !== 'object' || stats === null) { + console.error('Invalid stats:', stats); + throw new Error('آمار نامعتبر است: stats object not found'); + } + // Check if stats.market exists and is an object + if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) { + console.error('Invalid stats.market:', stats.market); + console.error('Full stats object:', JSON.stringify(stats, null, 2)); + throw new Error('آمار نامعتبر است: stats.market object not found'); + } + if (!sentiment || typeof sentiment !== 'object' || sentiment === null) { + throw new Error('داده‌های احساسات نامعتبر است: sentiment object not found'); + } + // Note: sentiment can have different structures: + // - sentiment.fear_greed_index (from /api/sentiment) + // - sentiment.fear_greed_value (from /api/stats) + // So we don't validate the exact structure here + if (!trending || !Array.isArray(trending.trending)) { + throw new Error('داده‌های ترند نامعتبر است: trending array not found'); + } + if (!defi || typeof defi !== 'object' || defi === null) { + throw new Error('داده‌های DeFi نامعتبر است: defi object not found'); + } + + // Call updateStats with validated data - double check before calling + if (stats && stats.market && typeof stats.market === 'object' && !Array.isArray(stats.market)) { updateStats(stats, sentiment); + } else { + console.error('Failed final validation before updateStats:', { stats, sentiment }); + throw new Error('داده‌های stats.market نامعتبر است'); + } updateMarketTable(market.cryptocurrencies); updateTrending(trending.trending); updateDeFi(defi); updateCharts(market, sentiment); } catch (error) { console.error('Error loading market data:', error); + const marketTableBody = document.getElementById('marketTableBody'); + if (marketTableBody) { + marketTableBody.innerHTML = ``; + } + showToast('❌ خطا در بارگذاری داده‌های بازار: ' + (error.message || 'خطای نامشخص'), 'error'); } } function updateStats(stats, sentiment) { - const mcap = stats.market.total_market_cap; - document.getElementById('totalMarketCap').textContent = '$' + (mcap / 1e12).toFixed(2) + 'T'; + try { + // More robust validation with detailed checks + if (!stats || typeof stats !== 'object' || stats === null) { + console.warn('updateStats: stats is undefined, null, or not an object', stats); + return; + } + if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) { + console.warn('updateStats: stats.market is invalid', { stats, market: stats.market }); + return; + } + if (!sentiment || typeof sentiment !== 'object' || sentiment === null) { + console.warn('updateStats: sentiment is undefined, null, or not an object', sentiment); + return; + } - const volume = stats.market.total_volume; - document.getElementById('totalVolume').textContent = '$' + (volume / 1e9).toFixed(2) + 'B'; + // Use safe property access for market data with additional checks + const marketObj = stats.market; + if (!marketObj || typeof marketObj !== 'object' || marketObj === null) { + console.warn('updateStats: marketObj is invalid', marketObj); + return; + } + + const mcap = (typeof marketObj.total_market_cap !== 'undefined' && marketObj.total_market_cap !== null) ? marketObj.total_market_cap : 0; + const totalMarketCapEl = document.getElementById('totalMarketCap'); + if (totalMarketCapEl) { + totalMarketCapEl.textContent = '$' + (mcap / 1e12).toFixed(2) + 'T'; + } - const btcDom = stats.market.btc_dominance; - document.getElementById('btcDominance').textContent = btcDom.toFixed(1) + '%'; + const volume = (typeof marketObj.total_volume !== 'undefined' && marketObj.total_volume !== null) ? marketObj.total_volume : 0; + const totalVolumeEl = document.getElementById('totalVolume'); + if (totalVolumeEl) { + totalVolumeEl.textContent = '$' + (volume / 1e9).toFixed(2) + 'B'; + } - const fg = sentiment.fear_greed_index.value; - const classification = sentiment.fear_greed_index.classification; - document.getElementById('fearGreed').textContent = fg; - document.getElementById('sentimentLabel').innerHTML = `${classification}`; + const btcDom = (typeof marketObj.btc_dominance !== 'undefined' && marketObj.btc_dominance !== null) ? marketObj.btc_dominance : 0; + const btcDominanceEl = document.getElementById('btcDominance'); + if (btcDominanceEl) { + btcDominanceEl.textContent = btcDom.toFixed(1) + '%'; + } + + // Handle sentiment data - support both structures: + // 1. sentiment.fear_greed_index.value (from /api/sentiment) + // 2. sentiment.fear_greed_value (from /api/stats) + let fg = 50; + let classification = 'Neutral'; + + if (sentiment.fear_greed_index && typeof sentiment.fear_greed_index === 'object') { + // Structure from /api/sentiment endpoint + fg = (typeof sentiment.fear_greed_index.value !== 'undefined') ? sentiment.fear_greed_index.value : 50; + classification = sentiment.fear_greed_index.classification || 'Neutral'; + } else if (typeof sentiment.fear_greed_value !== 'undefined') { + // Structure from /api/stats endpoint + fg = sentiment.fear_greed_value; + classification = sentiment.classification || 'Neutral'; + } else if (typeof sentiment.value !== 'undefined') { + // Fallback structure + fg = sentiment.value; + classification = sentiment.classification || 'Neutral'; + } - const sentEl = document.getElementById('sentimentLabel'); + const fearGreedEl = document.getElementById('fearGreed'); + if (fearGreedEl) { + fearGreedEl.textContent = fg; + } + const sentimentLabelEl = document.getElementById('sentimentLabel'); + if (sentimentLabelEl) { + sentimentLabelEl.innerHTML = `${classification}`; + if (fg < 25) { - sentEl.style.color = 'var(--accent-red)'; + sentimentLabelEl.style.color = 'var(--accent-red)'; } else if (fg < 45) { - sentEl.style.color = 'var(--accent-yellow)'; + sentimentLabelEl.style.color = 'var(--accent-yellow)'; } else if (fg < 55) { - sentEl.style.color = 'var(--text-secondary)'; + sentimentLabelEl.style.color = 'var(--text-secondary)'; } else if (fg < 75) { - sentEl.style.color = 'var(--accent-blue)'; + sentimentLabelEl.style.color = 'var(--accent-blue)'; } else { - sentEl.style.color = 'var(--accent-green)'; + sentimentLabelEl.style.color = 'var(--accent-green)'; + } + } + } catch (error) { + console.error('Error updating stats:', error); + console.error('Stats object:', stats); + console.error('Sentiment object:', sentiment); } } function updateMarketTable(cryptos) { + try { + if (!cryptos || !Array.isArray(cryptos) || cryptos.length === 0) { const tbody = document.getElementById('marketTableBody'); - tbody.innerHTML = cryptos.map((crypto, index) => ` - + if (tbody) { + tbody.innerHTML = ''; + } + return; + } + + const tbody = document.getElementById('marketTableBody'); + if (!tbody) return; + + // Store data for filtering + marketDataCache = cryptos; + + tbody.innerHTML = cryptos.map((crypto, index) => { + const price = crypto.price || 0; + const change24h = crypto.change_24h || 0; + const marketCap = crypto.market_cap || 0; + const volume24h = crypto.volume_24h || 0; + const symbol = crypto.symbol || 'N/A'; + const name = crypto.name || 'نامشخص'; + const changeClass = change24h >= 0 ? 'positive' : 'negative'; + const changeIcon = change24h >= 0 ? '📈' : '📉'; + + return ` + - - - - + + + + - `).join(''); + `; + }).join(''); + } catch (error) { + console.error('Error updating market table:', error); + const tbody = document.getElementById('marketTableBody'); + if (tbody) { + tbody.innerHTML = ''; + } + } } function updateTrending(trending) { + try { const grid = document.getElementById('trendingGrid'); - grid.innerHTML = trending.map((coin, index) => ` + if (!grid) return; + + if (!trending || !Array.isArray(trending) || trending.length === 0) { + grid.innerHTML = '
هیچ ترندی یافت نشد
'; + return; + } + + grid.innerHTML = trending.map((coin, index) => { + const name = coin.name || 'نامشخص'; + const symbol = coin.symbol || 'N/A'; + return `
#${index + 1}
- ${coin.thumb ? `` : ''} + ${coin.thumb ? `` : ''}
-
${coin.name}
-
${coin.symbol}
+
${name}
+
${symbol}
- `).join(''); + `; + }).join(''); + } catch (error) { + console.error('Error updating trending:', error); + const grid = document.getElementById('trendingGrid'); + if (grid) { + grid.innerHTML = '
خطا در نمایش ترندها
'; + } + } } function updateDeFi(defi) { + try { const list = document.getElementById('defiList'); - const protocols = defi.protocols || []; + if (!list) return; + + const protocols = defi && defi.protocols ? defi.protocols : []; + const totalTvl = defi && defi.total_tvl ? defi.total_tvl : 0; list.innerHTML = ` -
-
$${(defi.total_tvl / 1e9).toFixed(2)}B
-
Total Value Locked
+
+
$${(totalTvl / 1e9).toFixed(2)}B
+
Total Value Locked
- ${protocols.map((p, i) => ` -
+ ${protocols.length > 0 ? protocols.map((p, i) => { + const name = p.name || 'نامشخص'; + const chain = p.chain || 'N/A'; + const tvl = p.tvl || 0; + const change24h = p.change_24h || 0; + const changeClass = change24h >= 0 ? 'positive' : 'negative'; + return ` +
+
-
${i + 1}. ${p.name}
-
${p.chain}
+
${i + 1}. ${name}
+
+ 🔗 ${chain} +
-
$${(p.tvl / 1e9).toFixed(2)}B
-
- ${p.change_24h >= 0 ? '+' : ''}${p.change_24h.toFixed(2)}% +
$${(tvl / 1e9).toFixed(2)}B
+
+ ${change24h >= 0 ? '📈' : '📉'} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}%
- `).join('')}
`; + }).join('') : '
📦
هیچ پروتکلی یافت نشد
'} +
+ `; + } catch (error) { + console.error('Error updating DeFi:', error); + const list = document.getElementById('defiList'); + if (list) { + list.innerHTML = '
خطا در نمایش داده‌های DeFi
'; + } + } } function initCharts() { @@ -2195,22 +3362,30 @@ Crypto market is bullish today } // استفاده از WebSocket Client جدید + let wsConnectAttempts = 0; + const MAX_WS_CONNECT_ATTEMPTS = 10; // حداکثر 10 تلاش (10 ثانیه) + let wsStatsInterval = null; + function connectWebSocket() { // WebSocket client از websocket-client.js استفاده می‌شود // که به صورت خودکار اتصال برقرار می‌کند - if (window.wsClient) { + // بررسی وجود wsClient و متدهای مورد نیاز + if (window.wsClient && typeof window.wsClient.on === 'function' && typeof window.wsClient.requestStats === 'function') { console.log('✅ WebSocket Client آماده است'); + wsConnectAttempts = 0; // Reset counter on success // ثبت handler برای به‌روزرسانی آمار window.wsClient.on('stats_update', (message) => { console.log('📊 Stats update:', message.data); + if (typeof updateOnlineStats === 'function') { updateOnlineStats(message.data); + } }); window.wsClient.on('provider_stats', (message) => { console.log('📡 Provider stats:', message.data); - if (currentTab === 'monitor') { + if (currentTab === 'monitor' && typeof updateProviderStatsDisplay === 'function') { updateProviderStatsDisplay(message.data); } }); @@ -2224,20 +3399,40 @@ Crypto market is bullish today // درخواست آمار اولیه setTimeout(() => { - if (window.wsClient.isConnected) { + if (window.wsClient && window.wsClient.isConnected) { window.wsClient.requestStats(); } }, 1000); - // درخواست آمار هر 10 ثانیه - setInterval(() => { + // درخواست آمار هر 10 ثانیه (فقط یک بار تنظیم شود) + if (!wsStatsInterval) { + wsStatsInterval = setInterval(() => { if (window.wsClient && window.wsClient.isConnected) { window.wsClient.requestStats(); } }, 10000); + } } else { - console.log('⏳ در انتظار WebSocket Client...'); + wsConnectAttempts++; + if (wsConnectAttempts < MAX_WS_CONNECT_ATTEMPTS) { + // فقط هر 5 ثانیه یک بار لاگ کنیم تا console پر نشود + if (wsConnectAttempts % 5 === 0 || wsConnectAttempts === 1) { + console.log(`⏳ در انتظار WebSocket Client... (${wsConnectAttempts}/${MAX_WS_CONNECT_ATTEMPTS})`); + } setTimeout(connectWebSocket, 1000); + } else { + console.warn('⚠️ WebSocket Client پس از ' + MAX_WS_CONNECT_ATTEMPTS + ' تلاش آماده نشد. ممکن است فایل websocket-client.js لود نشده باشد یا WebSocket پشتیبانی نشود.'); + console.warn('⚠️ بررسی کنید که فایل /static/js/websocket-client.js به درستی لود شده باشد.'); + // تلاش نهایی بعد از 5 ثانیه + setTimeout(() => { + if (!window.wsClient) { + console.warn('⚠️ WebSocket Client غیرفعال است. برخی ویژگی‌های real-time ممکن است کار نکنند.'); + console.warn('⚠️ برای فعال کردن WebSocket، صفحه را refresh کنید (Ctrl+F5 برای clear cache).'); + } + }, 5000); + // متوقف کردن تلاش‌های بیشتر + return; + } } } @@ -2277,17 +3472,47 @@ Crypto market is bullish today // Monitor Functions async function loadMonitorData() { try { + // Show loading state + const tbody = document.getElementById('providersTable'); + if (tbody) { + tbody.innerHTML = '
'; + } + + const [statusRes, providersRes] = await Promise.all([ + fetch('/api/status'), + fetch('/api/providers') + ]); + + // Check if responses are OK + if (!statusRes.ok) throw new Error(`خطا در دریافت وضعیت: ${statusRes.status}`); + if (!providersRes.ok) throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status}`); + const [status, providers] = await Promise.all([ - fetch('/api/status').then(r => r.json()), - fetch('/api/providers').then(r => r.json()) + statusRes.json(), + providersRes.json() ]); - document.getElementById('totalAPIs').textContent = status.total_providers; - document.getElementById('onlineAPIs').textContent = status.online; - document.getElementById('offlineAPIs').textContent = status.offline; - document.getElementById('avgResponse').textContent = status.avg_response_time_ms + 'ms'; + // Validate data + if (!status || typeof status.total_providers === 'undefined') throw new Error('داده‌های وضعیت نامعتبر است'); + if (!providers || !Array.isArray(providers)) throw new Error('لیست APIها نامعتبر است'); - const tbody = document.getElementById('providersTable'); + if (document.getElementById('totalAPIs')) { + document.getElementById('totalAPIs').textContent = status.total_providers || 0; + } + if (document.getElementById('onlineAPIs')) { + document.getElementById('onlineAPIs').textContent = status.online || 0; + } + if (document.getElementById('offlineAPIs')) { + document.getElementById('offlineAPIs').textContent = status.offline || 0; + } + if (document.getElementById('avgResponse')) { + document.getElementById('avgResponse').textContent = (status.avg_response_time_ms || 0) + 'ms'; + } + + if (tbody) { + if (providers.length === 0) { + tbody.innerHTML = ''; + } else { tbody.innerHTML = providers.map(p => { let statusClass = 'badge-success'; if (p.status === 'offline') statusClass = 'badge-danger'; @@ -2295,16 +3520,28 @@ Crypto market is bullish today return ` - - - - - + + + + + `; }).join(''); + } + } } catch (error) { console.error('Error loading monitor data:', error); + const tbody = document.getElementById('providersTable'); + if (tbody) { + tbody.innerHTML = ``; + } + showToast('❌ خطا در بارگذاری داده‌های مانیتور: ' + (error.message || 'خطای نامشخص'), 'error'); } } @@ -2884,19 +4121,262 @@ Crypto market is bullish today } // Toast notification function - function showToast(message, type = 'info') { + // Enhanced Toast Notification System + function showToast(message, type = 'info', title = null) { + const toastContainer = document.getElementById('toastContainer') || document.body; const toast = document.createElement('div'); toast.className = `toast toast-${type}`; + + const icons = { + success: '✅', + error: '❌', + warning: '⚠️', + info: 'ℹ️' + }; + + const titles = { + success: 'موفق!', + error: 'خطا!', + warning: 'هشدار!', + info: 'اطلاعیه' + }; + toast.innerHTML = ` - ${type === 'success' ? '✅' : type === 'error' ? '❌' : 'ℹ️'} - ${message} +
${icons[type] || icons.info}
+
+
${title || titles[type] || titles.info}
+
${message}
+
+ `; - document.body.appendChild(toast); - + + toastContainer.appendChild(toast); + + // Auto remove after 5 seconds setTimeout(() => { - toast.style.animation = 'slideInRight 0.3s reverse'; + toast.style.animation = 'toastSlideIn 0.3s reverse'; setTimeout(() => toast.remove(), 300); + }, 5000); + + // Add click to dismiss + toast.addEventListener('click', (e) => { + if (e.target.classList.contains('toast-close') || e.target === toast) { + toast.style.animation = 'toastSlideIn 0.3s reverse'; + setTimeout(() => toast.remove(), 300); + } + }); + } + + // Progress Indicator Functions + function showProgress(percent = 0) { + const progressBar = document.getElementById('progressBar'); + if (progressBar) { + progressBar.style.width = percent + '%'; + } + } + + function hideProgress() { + const progressBar = document.getElementById('progressBar'); + if (progressBar) { + progressBar.style.width = '0%'; + } + } + + // Loading Overlay Functions + function showLoading(message = 'در حال بارگذاری...') { + const overlay = document.getElementById('loadingOverlay'); + const text = document.getElementById('loadingText'); + if (overlay) { + overlay.classList.add('show'); + } + if (text) { + text.textContent = message; + } + } + + function hideLoading() { + const overlay = document.getElementById('loadingOverlay'); + if (overlay) { + overlay.classList.remove('show'); + } + } + + // Feedback Overlay Functions + function showFeedback(type, title, message) { + const overlay = document.getElementById('feedbackOverlay'); + const icon = document.getElementById('feedbackIcon'); + const titleEl = document.getElementById('feedbackTitle'); + const messageEl = document.getElementById('feedbackMessage'); + + if (overlay && icon && titleEl && messageEl) { + const icons = { + success: '✅', + error: '❌', + warning: '⚠️' + }; + + icon.textContent = icons[type] || icons.success; + titleEl.textContent = title; + messageEl.textContent = message; + + overlay.classList.add('show'); + + // Auto hide after 3 seconds + setTimeout(() => { + hideFeedback(); }, 3000); + } + } + + function hideFeedback() { + const overlay = document.getElementById('feedbackOverlay'); + if (overlay) { + overlay.classList.remove('show'); + } + } + + // Scroll to Top Function + function scrollToTop() { + window.scrollTo({ + top: 0, + behavior: 'smooth' + }); + } + + // Show FAB when scrolling down + let lastScroll = 0; + window.addEventListener('scroll', () => { + const fab = document.querySelector('.fab'); + if (fab) { + const currentScroll = window.pageYOffset; + if (currentScroll > 300) { + fab.style.opacity = '1'; + fab.style.pointerEvents = 'all'; + } else { + fab.style.opacity = '0'; + fab.style.pointerEvents = 'none'; + } + lastScroll = currentScroll; + } + }); + + // Filter Market Table Function + let currentFilter = 'all'; + let marketDataCache = []; + + function filterMarketTable() { + const searchInput = document.getElementById('marketSearch'); + const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; + const tbody = document.getElementById('marketTableBody'); + + if (!tbody) return; + + const rows = tbody.querySelectorAll('tr'); + let visibleCount = 0; + + // Remove existing no-results row + const existingNoResults = tbody.querySelector('tr[data-no-results]'); + if (existingNoResults) { + existingNoResults.remove(); + } + + rows.forEach((row, index) => { + if (row.querySelector('td[colspan]')) { + return; // Skip loading/error rows + } + + const cells = row.querySelectorAll('td'); + if (cells.length < 4) return; + + const name = cells[1]?.textContent?.toLowerCase() || ''; + const symbol = cells[1]?.querySelector('.crypto-symbol')?.textContent?.toLowerCase() || ''; + const changeText = cells[3]?.textContent || ''; + const changeValue = parseFloat(changeText.replace(/[^0-9.-]/g, '')) || 0; + + let matchesSearch = !searchTerm || name.includes(searchTerm) || symbol.includes(searchTerm); + let matchesFilter = true; + + if (currentFilter === 'top10') { + matchesFilter = index < 10; + } else if (currentFilter === 'gainers') { + matchesFilter = changeValue > 0; + } else if (currentFilter === 'losers') { + matchesFilter = changeValue < 0; + } else if (currentFilter === 'volume') { + // This would need volume data - for now show all + matchesFilter = true; + } + + if (matchesSearch && matchesFilter) { + row.style.display = ''; + visibleCount++; + row.style.animation = `rowSlideIn 0.3s ease-out`; + row.style.animationDelay = `${index * 0.05}s`; + } else { + row.style.display = 'none'; + } + }); + + // Show message if no results + if (visibleCount === 0 && rows.length > 0 && !searchTerm && currentFilter === 'all') { + // Don't show message if no search/filter is applied + return; + } + + if (visibleCount === 0) { + const noResultsRow = document.createElement('tr'); + noResultsRow.setAttribute('data-no-results', 'true'); + noResultsRow.innerHTML = ``; + tbody.appendChild(noResultsRow); + } + } + + function filterByCategory(category) { + currentFilter = category; + + // Update active chip + document.querySelectorAll('.filter-chip').forEach(chip => { + chip.classList.remove('active'); + }); + if (event && event.target) { + event.target.classList.add('active'); + } + + filterMarketTable(); + } + + // Number Counter Animation + function animateNumber(element, from, to, duration = 1000) { + if (!element) return; + + const start = performance.now(); + const difference = to - from; + + function update(currentTime) { + const elapsed = currentTime - start; + const progress = Math.min(elapsed / duration, 1); + + // Easing function + const easeOutQuart = 1 - Math.pow(1 - progress, 4); + const current = from + (difference * easeOutQuart); + + element.textContent = typeof to === 'number' && to >= 1000 + ? current.toLocaleString('fa-IR', { maximumFractionDigits: 2 }) + : current.toFixed(2); + + if (progress < 1) { + requestAnimationFrame(update); + } else { + element.classList.add('updated'); + setTimeout(() => element.classList.remove('updated'), 500); + } + } + + requestAnimationFrame(update); } // Close modals when clicking outside @@ -3175,6 +4655,158 @@ Crypto market is bullish today ]); } + // Load System Alerts + async function loadSystemAlerts() { + try { + const response = await fetch('/api/diagnostics/last'); + const report = await response.json(); + + if (report.message || !report.issues || report.issues.length === 0) { + document.getElementById('systemAlertsSection').style.display = 'none'; + return; + } + + displaySystemAlerts(report.issues); + document.getElementById('systemAlertsSection').style.display = 'block'; + } catch (error) { + console.error('Error loading system alerts:', error); + } + } + + function displaySystemAlerts(issues) { + const container = document.getElementById('systemAlertsContainer'); + if (!container) return; + + const severityConfig = { + 'critical': { + icon: 'icon-error', + color: 'var(--accent-red)', + bg: 'rgba(239, 68, 68, 0.1)', + border: 'rgba(239, 68, 68, 0.3)' + }, + 'warning': { + icon: 'icon-warning', + color: 'var(--accent-yellow)', + bg: 'rgba(245, 158, 11, 0.1)', + border: 'rgba(245, 158, 11, 0.3)' + }, + 'info': { + icon: 'icon-info', + color: 'var(--accent-blue)', + bg: 'rgba(59, 130, 246, 0.1)', + border: 'rgba(59, 130, 246, 0.3)' + } + }; + + const solutions = { + 'HF_API_TOKEN': { + title: 'تنظیم متغیر محیطی HF_API_TOKEN', + steps: [ + '1. یک توکن از HuggingFace دریافت کنید:', + ' - به https://huggingface.co/settings/tokens بروید', + ' - یک توکن جدید ایجاد کنید', + '2. توکن را به متغیر محیطی اضافه کنید:', + ' Windows: set HF_API_TOKEN=your_token_here', + ' Linux/Mac: export HF_API_TOKEN=your_token_here', + ' یا در فایل .env: HF_API_TOKEN=your_token_here' + ] + }, + 'resources.json': { + title: 'ایجاد فایل resources.json', + steps: [ + 'این فایل به صورت خودکار ساخته می‌شود.', + 'اگر نیاز به تنظیمات دستی دارید، می‌توانید آن را ایجاد کنید:', + '{', + ' "resources": []', + '}' + ] + }, + 'config.json': { + title: 'ایجاد فایل config.json', + steps: [ + 'این فایل به صورت خودکار ساخته می‌شود.', + 'اگر نیاز به تنظیمات دستی دارید، می‌توانید آن را ایجاد کنید.' + ] + }, + 'HuggingFace API': { + title: 'رفع مشکل اتصال به HuggingFace', + steps: [ + '1. بررسی اتصال اینترنت', + '2. بررسی فایروال و پروکسی', + '3. بررسی DNS settings', + '4. اگر از VPN استفاده می‌کنید، آن را غیرفعال کنید', + '5. بررسی کنید که https://api.huggingface.co قابل دسترسی باشد' + ] + }, + 'Auto-Discovery': { + title: 'فعال‌سازی Auto-Discovery Service', + steps: [ + 'برای فعال‌سازی سرویس Auto-Discovery:', + '1. متغیر محیطی را تنظیم کنید:', + ' export ENABLE_AUTO_DISCOVERY=true', + '2. یا در فایل .env اضافه کنید:', + ' ENABLE_AUTO_DISCOVERY=true', + '3. سرور را restart کنید' + ] + } + }; + + let html = ''; + issues.forEach((issue, index) => { + const config = severityConfig[issue.severity] || severityConfig['info']; + const solutionKey = issue.title.includes('HF_API_TOKEN') ? 'HF_API_TOKEN' : + issue.title.includes('resources.json') ? 'resources.json' : + issue.title.includes('config.json') ? 'config.json' : + issue.title.includes('HuggingFace') ? 'HuggingFace API' : + issue.title.includes('Auto-Discovery') ? 'Auto-Discovery' : null; + + const solution = solutionKey ? solutions[solutionKey] : null; + + html += ` +
+
+
+ +
+
+
+
+

+ ${issue.title} +

+
+ ${issue.description} +
+
+ + ${issue.category} + +
+ + ${solution ? ` +
+
+ + راه‌حل: +
+
+ ${solution.steps.map(step => `
${step}
`).join('')} +
+
+ ` : ''} + +
+ ${new Date(issue.timestamp).toLocaleString('fa-IR')} +
+
+
+
+ `; + }); + + container.innerHTML = html || '
هیچ هشداری یافت نشد
'; + } + async function runDiagnostics(autoFix = false) { const resultsDiv = document.getElementById('diagnosticsResults'); resultsDiv.innerHTML = '
'; @@ -3186,10 +4818,19 @@ Crypto market is bullish today const report = await response.json(); displayDiagnosticsReport(report); - showToast(`✅ اشکال‌یابی انجام شد (${report.total_issues} مشکل یافت شد)`, 'success'); + showToast(`اشکال‌یابی انجام شد (${report.total_issues} مشکل یافت شد)`, 'success', 'Diagnostics'); + + // Update system alerts + if (report.issues && report.issues.length > 0) { + displaySystemAlerts(report.issues); + document.getElementById('systemAlertsSection').style.display = 'block'; + } } catch (error) { - resultsDiv.innerHTML = `
❌ خطا: ${error.message}
`; - showToast('❌ خطا در اجرای اشکال‌یابی', 'error'); + resultsDiv.innerHTML = `
+ + خطا: ${error.message} +
`; + showToast('خطا در اجرای اشکال‌یابی', 'error', 'Error'); } } @@ -3303,6 +4944,12 @@ Crypto market is bullish today } displayDiagnosticsReport(report); + + // Load system alerts + if (report.issues && report.issues.length > 0) { + displaySystemAlerts(report.issues); + document.getElementById('systemAlertsSection').style.display = 'block'; + } } catch (error) { console.error('Error loading last diagnostics:', error); }
در حال بارگذاری داده‌های بازار...
+
+
خطا در بارگذاری داده‌ها
+
${error.message || 'خطای نامشخص'}
+ +
هیچ داده‌ای یافت نشد
${crypto.rank || index + 1}
- ${crypto.image ? `${crypto.symbol}` : - `
${crypto.symbol[0]}
`} + ${crypto.image ? `${symbol}` : + `
${symbol[0] || '?'}
`}
-
${crypto.name}
-
${crypto.symbol}
+
${name}
+
${symbol}
$${crypto.price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}${crypto.change_24h >= 0 ? '+' : ''}${crypto.change_24h.toFixed(2)}%$${(crypto.market_cap / 1e9).toFixed(2)}B$${(crypto.volume_24h / 1e9).toFixed(2)}B$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}${changeIcon} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}%$${(marketCap / 1e9).toFixed(2)}B$${(volume24h / 1e9).toFixed(2)}B
خطا در نمایش داده‌ها
در حال بارگذاری وضعیت APIها...
هیچ APIای یافت نشد
${p.name}${p.category}${p.status.toUpperCase()}${p.response_time_ms}ms${new Date(p.last_fetch).toLocaleTimeString()}${p.name || 'نامشخص'}${p.category || 'نامشخص'}${(p.status || 'unknown').toUpperCase()}${p.response_time_ms || p.avg_response_time_ms || 0}ms${p.last_fetch ? new Date(p.last_fetch).toLocaleTimeString() : 'نامشخص'}
+
+
خطا در بارگذاری داده‌ها
+
${error.message || 'خطای نامشخص'}
+ +
+
🔍
+
نتیجه‌ای یافت نشد
+
لطفاً عبارت جستجوی دیگری را امتحان کنید
+