| <!DOCTYPE html> |
| <html lang="en"> |
|
|
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Crypto Monitor ULTIMATE - Unified Dashboard</title> |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" |
| rel="stylesheet"> |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> |
| <link rel="stylesheet" href="/static/css/connection-status.css"> |
| <script src="/static/js/websocket-client.js"></script> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| :root { |
| --bg-dark: #0a0e1a; |
| --bg-card: #111827; |
| --bg-card-hover: #1f2937; |
| --text-primary: #f9fafb; |
| --text-secondary: #9ca3af; |
| --accent-blue: #3b82f6; |
| --accent-green: #10b981; |
| --accent-red: #ef4444; |
| --accent-yellow: #f59e0b; |
| --accent-purple: #8b5cf6; |
| --accent-pink: #ec4899; |
| --border: rgba(255, 255, 255, 0.1); |
| --shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5); |
| } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; |
| background: var(--bg-dark); |
| color: var(--text-primary); |
| line-height: 1.6; |
| overflow-x: hidden; |
| } |
| |
| body::before { |
| content: ''; |
| position: fixed; |
| top: 0; |
| left: 0; |
| right: 0; |
| bottom: 0; |
| background: |
| radial-gradient(circle at 20% 30%, rgba(59, 130, 246, 0.15) 0%, transparent 40%), |
| radial-gradient(circle at 80% 70%, rgba(139, 92, 246, 0.15) 0%, transparent 40%), |
| radial-gradient(circle at 50% 50%, rgba(16, 185, 129, 0.1) 0%, transparent 30%); |
| pointer-events: none; |
| z-index: 0; |
| } |
| |
| .container { |
| max-width: 1920px; |
| margin: 0 auto; |
| padding: 20px; |
| position: relative; |
| z-index: 1; |
| } |
| |
| |
| .header { |
| background: linear-gradient(135deg, rgba(17, 24, 39, 0.8) 0%, rgba(31, 41, 55, 0.4) 100%); |
| backdrop-filter: blur(20px); |
| border: 1px solid var(--border); |
| border-radius: 24px; |
| padding: 30px; |
| margin-bottom: 30px; |
| box-shadow: var(--shadow); |
| } |
| |
| .header-top { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| flex-wrap: wrap; |
| gap: 20px; |
| margin-bottom: 20px; |
| } |
| |
| .logo { |
| display: flex; |
| align-items: center; |
| gap: 15px; |
| } |
| |
| .logo-icon { |
| width: 60px; |
| height: 60px; |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899); |
| border-radius: 16px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| font-size: 32px; |
| box-shadow: 0 10px 40px rgba(59, 130, 246, 0.4); |
| animation: pulse-glow 3s ease-in-out infinite; |
| } |
| |
| @keyframes pulse-glow { |
| |
| 0%, |
| 100% { |
| box-shadow: 0 10px 40px rgba(59, 130, 246, 0.4); |
| } |
| |
| 50% { |
| box-shadow: 0 10px 60px rgba(139, 92, 246, 0.6); |
| } |
| } |
| |
| .logo-text h1 { |
| font-size: 32px; |
| font-weight: 900; |
| background: linear-gradient(135deg, #3b82f6, #8b5cf6, #ec4899); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| letter-spacing: -1px; |
| } |
| |
| .logo-text p { |
| font-size: 14px; |
| color: var(--text-secondary); |
| font-weight: 500; |
| } |
| |
| .header-actions { |
| display: flex; |
| gap: 12px; |
| align-items: center; |
| flex-wrap: wrap; |
| } |
| |
| .status-badge { |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| padding: 12px 24px; |
| background: rgba(16, 185, 129, 0.15); |
| border: 1px solid rgba(16, 185, 129, 0.3); |
| border-radius: 12px; |
| font-size: 14px; |
| font-weight: 600; |
| } |
| |
| .status-dot { |
| width: 10px; |
| height: 10px; |
| background: var(--accent-green); |
| border-radius: 50%; |
| animation: pulse 2s infinite; |
| } |
| |
| @keyframes pulse { |
| |
| 0%, |
| 100% { |
| opacity: 1; |
| transform: scale(1); |
| } |
| |
| 50% { |
| opacity: 0.5; |
| transform: scale(1.2); |
| } |
| } |
| |
| .live-indicator { |
| padding: 8px 16px; |
| background: rgba(239, 68, 68, 0.15); |
| border: 1px solid rgba(239, 68, 68, 0.3); |
| border-radius: 8px; |
| font-size: 12px; |
| font-weight: 700; |
| text-transform: uppercase; |
| color: var(--accent-red); |
| animation: blink 2s infinite; |
| } |
| |
| @keyframes blink { |
| |
| 0%, |
| 50%, |
| 100% { |
| opacity: 1; |
| } |
| |
| 25%, |
| 75% { |
| opacity: 0.3; |
| } |
| } |
| |
| |
| .tabs { |
| display: flex; |
| gap: 10px; |
| margin-bottom: 30px; |
| flex-wrap: wrap; |
| border-bottom: 2px solid var(--border); |
| padding-bottom: 10px; |
| } |
| |
| .tab { |
| padding: 12px 24px; |
| background: transparent; |
| border: none; |
| border-radius: 12px 12px 0 0; |
| color: var(--text-secondary); |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.3s; |
| font-size: 14px; |
| position: relative; |
| } |
| |
| .tab:hover { |
| color: var(--text-primary); |
| background: rgba(255, 255, 255, 0.05); |
| } |
| |
| .tab.active { |
| color: var(--accent-blue); |
| background: rgba(59, 130, 246, 0.1); |
| } |
| |
| .tab.active::after { |
| content: ''; |
| position: absolute; |
| bottom: -12px; |
| left: 0; |
| right: 0; |
| height: 3px; |
| background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple)); |
| border-radius: 2px; |
| } |
| |
| .tab-content { |
| display: none; |
| animation: fadeIn 0.3s; |
| } |
| |
| .tab-content.active { |
| display: block; |
| } |
| |
| @keyframes fadeIn { |
| from { |
| opacity: 0; |
| transform: translateY(10px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| |
| .stats-grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); |
| gap: 16px; |
| margin-bottom: 24px; |
| align-items: stretch; |
| } |
| |
| @media (min-width: 1280px) { |
| .stats-grid--market { |
| grid-template-columns: repeat(5, minmax(180px, 1fr)); |
| } |
| } |
| |
| .stat-card { |
| background: rgba(17, 24, 39, 0.6); |
| backdrop-filter: blur(10px); |
| border: 1px solid var(--border); |
| border-radius: 20px; |
| padding: 20px; |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| position: relative; |
| overflow: hidden; |
| display: flex; |
| flex-direction: column; |
| gap: 12px; |
| } |
| |
| .stat-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 4px; |
| background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink)); |
| transform: scaleX(0); |
| transition: transform 0.3s ease; |
| } |
| |
| .stat-card:hover { |
| transform: translateY(-8px) scale(1.02); |
| border-color: rgba(59, 130, 246, 0.5); |
| box-shadow: 0 20px 50px rgba(59, 130, 246, 0.3); |
| } |
| |
| .stat-card:hover::before { |
| transform: scaleX(1); |
| } |
| |
| .stat-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 8px; |
| } |
| |
| .stat-icon { |
| width: 44px; |
| height: 44px; |
| border-radius: 14px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2)); |
| color: var(--accent-blue); |
| } |
| |
| .stat-icon svg { |
| width: 24px; |
| height: 24px; |
| stroke: currentColor; |
| stroke-width: 1.6; |
| stroke-linecap: round; |
| stroke-linejoin: round; |
| fill: none; |
| } |
| |
| .stat-value { |
| font-size: 30px; |
| font-weight: 900; |
| margin-bottom: 0; |
| background: linear-gradient(135deg, var(--text-primary), var(--text-secondary)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .stat-label { |
| font-size: 13px; |
| color: var(--text-secondary); |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| font-weight: 600; |
| } |
| |
| .stat-change { |
| font-size: 14px; |
| font-weight: 700; |
| margin-top: 12px; |
| display: flex; |
| align-items: center; |
| gap: 6px; |
| } |
| |
| .stat-change.positive { |
| color: var(--accent-green); |
| } |
| |
| .stat-change.negative { |
| color: var(--accent-red); |
| } |
| |
| |
| .market-section { |
| background: rgba(17, 24, 39, 0.6); |
| backdrop-filter: blur(10px); |
| border: 1px solid var(--border); |
| border-radius: 24px; |
| padding: 30px; |
| margin-bottom: 30px; |
| box-shadow: var(--shadow); |
| } |
| |
| .section-header { |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| margin-bottom: 25px; |
| flex-wrap: wrap; |
| gap: 15px; |
| } |
| |
| .section-title { |
| font-size: 24px; |
| font-weight: 800; |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| background-clip: text; |
| } |
| |
| .refresh-btn { |
| padding: 10px 20px; |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); |
| border: none; |
| border-radius: 10px; |
| color: white; |
| font-weight: 600; |
| cursor: pointer; |
| transition: all 0.3s ease; |
| } |
| |
| .refresh-btn:hover { |
| transform: scale(1.05); |
| box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4); |
| } |
| |
| table { |
| width: 100%; |
| border-collapse: collapse; |
| } |
| |
| thead { |
| background: rgba(59, 130, 246, 0.1); |
| } |
| |
| th { |
| padding: 16px; |
| text-align: left; |
| font-size: 12px; |
| font-weight: 700; |
| color: var(--text-secondary); |
| text-transform: uppercase; |
| letter-spacing: 1px; |
| } |
| |
| td { |
| padding: 16px; |
| border-top: 1px solid var(--border); |
| font-size: 14px; |
| } |
| |
| tr:hover { |
| background: rgba(59, 130, 246, 0.05); |
| } |
| |
| .crypto-name { |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| } |
| |
| .crypto-img { |
| width: 36px; |
| height: 36px; |
| border-radius: 10px; |
| object-fit: cover; |
| } |
| |
| .price { |
| font-weight: 700; |
| font-size: 15px; |
| } |
| |
| .change { |
| padding: 6px 12px; |
| border-radius: 8px; |
| font-weight: 700; |
| font-size: 13px; |
| } |
| |
| .change.positive { |
| background: rgba(16, 185, 129, 0.15); |
| color: var(--accent-green); |
| } |
| |
| .change.negative { |
| background: rgba(239, 68, 68, 0.15); |
| color: var(--accent-red); |
| } |
| |
| |
| .chart-container { |
| background: rgba(17, 24, 39, 0.6); |
| backdrop-filter: blur(10px); |
| border: 1px solid var(--border); |
| border-radius: 24px; |
| padding: 30px; |
| margin-bottom: 30px; |
| } |
| |
| canvas { |
| max-height: 350px; |
| } |
| |
| |
| .loading { |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| padding: 60px; |
| } |
| |
| .spinner { |
| width: 50px; |
| height: 50px; |
| border: 4px solid var(--border); |
| border-top-color: var(--accent-blue); |
| border-radius: 50%; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { |
| to { |
| transform: rotate(360deg); |
| } |
| } |
| |
| |
| .form-group { |
| margin-bottom: 20px; |
| } |
| |
| .form-label { |
| display: block; |
| margin-bottom: 8px; |
| font-weight: 600; |
| color: var(--text-primary); |
| } |
| |
| .form-input, |
| .form-select, |
| .form-textarea { |
| width: 100%; |
| padding: 12px; |
| background: rgba(17, 24, 39, 0.6); |
| border: 1px solid var(--border); |
| border-radius: 8px; |
| color: var(--text-primary); |
| font-family: inherit; |
| font-size: 14px; |
| } |
| |
| .form-input:focus, |
| .form-select:focus, |
| .form-textarea:focus { |
| outline: none; |
| border-color: var(--accent-blue); |
| } |
| |
| .form-textarea { |
| resize: vertical; |
| min-height: 100px; |
| } |
| |
| |
| .badge { |
| display: inline-block; |
| padding: 4px 12px; |
| border-radius: 12px; |
| font-size: 12px; |
| font-weight: 600; |
| } |
| |
| .badge-success { |
| background: rgba(16, 185, 129, 0.15); |
| color: var(--accent-green); |
| } |
| |
| .badge-warning { |
| background: rgba(245, 158, 11, 0.15); |
| color: var(--accent-yellow); |
| } |
| |
| .badge-danger { |
| background: rgba(239, 68, 68, 0.15); |
| color: var(--accent-red); |
| } |
| |
| .badge-info { |
| background: rgba(59, 130, 246, 0.15); |
| color: var(--accent-blue); |
| } |
| |
| |
| .alert { |
| padding: 12px 16px; |
| border-radius: 8px; |
| margin-bottom: 16px; |
| font-size: 14px; |
| } |
| |
| .alert-success { |
| background: rgba(16, 185, 129, 0.15); |
| color: var(--accent-green); |
| border-left: 4px solid var(--accent-green); |
| } |
| |
| .alert-error { |
| background: rgba(239, 68, 68, 0.15); |
| color: var(--accent-red); |
| border-left: 4px solid var(--accent-red); |
| } |
| |
| |
| @media (max-width: 768px) { |
| .stats-grid { |
| grid-template-columns: 1fr; |
| } |
| |
| .header-top { |
| flex-direction: column; |
| align-items: flex-start; |
| } |
| |
| table { |
| font-size: 12px; |
| } |
| |
| th, |
| td { |
| padding: 10px; |
| } |
| |
| .tabs { |
| overflow-x: auto; |
| } |
| } |
| |
| |
| .modal { |
| display: none; |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.8); |
| backdrop-filter: blur(5px); |
| z-index: 1000; |
| justify-content: center; |
| align-items: center; |
| animation: fadeIn 0.3s; |
| } |
| |
| .modal.active { |
| display: flex; |
| } |
| |
| .modal-content { |
| background: var(--bg-card); |
| border-radius: 20px; |
| padding: 30px; |
| max-width: 600px; |
| width: 90%; |
| max-height: 90vh; |
| overflow-y: auto; |
| border: 1px solid var(--border); |
| box-shadow: 0 25px 50px rgba(0, 0, 0, 0.5); |
| animation: slideUp 0.3s; |
| } |
| |
| @keyframes slideUp { |
| from { |
| opacity: 0; |
| transform: translateY(30px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| |
| .btn-icon { |
| padding: 8px 12px; |
| border-radius: 8px; |
| border: 1px solid; |
| cursor: pointer; |
| font-size: 12px; |
| font-weight: 600; |
| transition: all 0.3s; |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| } |
| |
| .btn-icon:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); |
| } |
| |
| |
| .pool-card-hover { |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| .pool-card-hover:hover { |
| transform: translateY(-5px); |
| box-shadow: 0 20px 40px rgba(59, 130, 246, 0.2); |
| } |
| |
| |
| .empty-state { |
| text-align: center; |
| padding: 60px 20px; |
| background: rgba(17, 24, 39, 0.6); |
| border-radius: 20px; |
| border: 2px dashed var(--border); |
| } |
| |
| .empty-state-icon { |
| font-size: 64px; |
| margin-bottom: 20px; |
| opacity: 0.5; |
| } |
| |
| |
| .skeleton { |
| background: linear-gradient(90deg, rgba(255, 255, 255, 0.05) 25%, rgba(255, 255, 255, 0.1) 50%, rgba(255, 255, 255, 0.05) 75%); |
| background-size: 200% 100%; |
| animation: loading 1.5s infinite; |
| } |
| |
| @keyframes loading { |
| 0% { |
| background-position: 200% 0; |
| } |
| |
| 100% { |
| background-position: -200% 0; |
| } |
| } |
| |
| |
| ::-webkit-scrollbar { |
| width: 10px; |
| height: 10px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: var(--bg-dark); |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); |
| border-radius: 5px; |
| } |
| |
| ::-webkit-scrollbar-thumb:hover { |
| background: linear-gradient(135deg, var(--accent-purple), var(--accent-pink)); |
| } |
| |
| |
| .toast { |
| position: fixed; |
| bottom: 20px; |
| right: 20px; |
| background: var(--bg-card); |
| border: 1px solid var(--border); |
| border-radius: 12px; |
| padding: 16px 20px; |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); |
| z-index: 2000; |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| min-width: 300px; |
| animation: slideInRight 0.3s; |
| } |
| |
| @keyframes slideInRight { |
| from { |
| opacity: 0; |
| transform: translateX(100%); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateX(0); |
| } |
| } |
| |
| .toast-success { |
| border-left: 4px solid var(--accent-green); |
| } |
| |
| .toast-error { |
| border-left: 4px solid var(--accent-red); |
| } |
| |
| .toast-info { |
| border-left: 4px solid var(--accent-blue); |
| } |
| |
| |
| .online-users-card { |
| background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2)); |
| backdrop-filter: blur(10px); |
| border: 2px solid rgba(59, 130, 246, 0.3); |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .online-users-card::before { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| right: 0; |
| height: 4px; |
| background: linear-gradient(90deg, #3b82f6, #8b5cf6, #ec4899); |
| } |
| |
| .online-users-card:hover { |
| border-color: rgba(59, 130, 246, 0.6); |
| box-shadow: 0 20px 50px rgba(59, 130, 246, 0.4); |
| } |
| |
| .pulse-ring { |
| position: absolute; |
| top: 50%; |
| right: 20px; |
| transform: translateY(-50%); |
| width: 60px; |
| height: 60px; |
| } |
| |
| .pulse-ring::before, |
| .pulse-ring::after { |
| content: ''; |
| position: absolute; |
| border: 2px solid var(--accent-blue); |
| border-radius: 50%; |
| width: 100%; |
| height: 100%; |
| opacity: 0; |
| animation: pulse-ring-animation 3s infinite; |
| } |
| |
| .pulse-ring::after { |
| animation-delay: 1.5s; |
| } |
| |
| @keyframes pulse-ring-animation { |
| 0% { |
| transform: scale(0.5); |
| opacity: 1; |
| } |
| |
| 100% { |
| transform: scale(1.5); |
| opacity: 0; |
| } |
| } |
| |
| |
| .ws-status-indicator { |
| position: fixed; |
| top: 20px; |
| right: 20px; |
| z-index: 10000; |
| background: rgba(17, 24, 39, 0.95); |
| backdrop-filter: blur(20px); |
| border: 1px solid var(--border); |
| border-radius: 16px; |
| padding: 12px 20px; |
| display: flex; |
| align-items: center; |
| gap: 12px; |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); |
| transition: all 0.3s ease; |
| } |
| |
| .ws-status-indicator:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 15px 50px rgba(0, 0, 0, 0.6); |
| } |
| |
| .ws-status-indicator.connected { |
| border-color: rgba(16, 185, 129, 0.5); |
| } |
| |
| .ws-status-indicator.disconnected { |
| border-color: rgba(239, 68, 68, 0.5); |
| } |
| |
| |
| .stat-value { |
| animation: fadeInUp 0.5s ease; |
| } |
| |
| @keyframes fadeInUp { |
| from { |
| opacity: 0; |
| transform: translateY(20px); |
| } |
| |
| to { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| } |
| |
| .shimmer { |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .shimmer::after { |
| content: ''; |
| position: absolute; |
| top: 0; |
| right: 0; |
| bottom: 0; |
| left: 0; |
| background: linear-gradient(90deg, |
| transparent, |
| rgba(255, 255, 255, 0.1), |
| transparent); |
| animation: shimmer 2s infinite; |
| } |
| |
| @keyframes shimmer { |
| 0% { |
| transform: translateX(-100%); |
| } |
| |
| 100% { |
| transform: translateX(100%); |
| } |
| } |
| |
| |
| .stat-icon { |
| position: relative; |
| } |
| |
| .stat-icon::after { |
| content: ''; |
| position: absolute; |
| inset: -10px; |
| background: inherit; |
| filter: blur(20px); |
| opacity: 0; |
| transition: opacity 0.3s ease; |
| z-index: -1; |
| } |
| |
| .stat-card:hover .stat-icon::after { |
| opacity: 0.6; |
| } |
| |
| .count-updated { |
| animation: pop-scale 0.4s ease; |
| } |
| |
| @keyframes pop-scale { |
| 0% { |
| transform: scale(1); |
| } |
| |
| 50% { |
| transform: scale(1.08); |
| } |
| |
| 100% { |
| transform: scale(1); |
| } |
| } |
| |
| |
| .refresh-btn { |
| position: relative; |
| overflow: hidden; |
| } |
| |
| .refresh-btn::before { |
| content: ''; |
| position: absolute; |
| top: 50%; |
| left: 50%; |
| width: 0; |
| height: 0; |
| border-radius: 50%; |
| background: rgba(255, 255, 255, 0.3); |
| transform: translate(-50%, -50%); |
| transition: width 0.6s, height 0.6s; |
| } |
| |
| .refresh-btn:active::before { |
| width: 300px; |
| height: 300px; |
| } |
| |
| |
| .live-indicator { |
| position: relative; |
| } |
| |
| .live-indicator::before { |
| content: ''; |
| position: absolute; |
| left: -20px; |
| top: 50%; |
| transform: translateY(-50%); |
| width: 12px; |
| height: 12px; |
| background: var(--accent-red); |
| border-radius: 50%; |
| animation: live-pulse 2s infinite; |
| } |
| |
| @keyframes live-pulse { |
| |
| 0%, |
| 100% { |
| box-shadow: 0 0 0 0 var(--accent-red); |
| } |
| |
| 50% { |
| box-shadow: 0 0 0 8px transparent; |
| } |
| } |
| |
| |
| .glass-card { |
| background: rgba(255, 255, 255, 0.05); |
| backdrop-filter: blur(10px) saturate(180%); |
| border: 1px solid rgba(255, 255, 255, 0.1); |
| } |
| |
| |
| .animated-progress { |
| position: relative; |
| height: 4px; |
| background: rgba(255, 255, 255, 0.1); |
| border-radius: 2px; |
| overflow: hidden; |
| } |
| |
| .animated-progress::after { |
| content: ''; |
| position: absolute; |
| top: 0; |
| left: 0; |
| height: 100%; |
| width: 30%; |
| background: linear-gradient(90deg, |
| transparent, |
| var(--accent-blue), |
| transparent); |
| animation: progress-slide 2s infinite; |
| } |
| |
| @keyframes progress-slide { |
| 0% { |
| transform: translateX(-100%); |
| } |
| |
| 100% { |
| transform: translateX(400%); |
| } |
| } |
| |
| |
| |
| |
| .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; |
| } |
| |
| |
| .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 { |
| 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-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%; |
| } |
| |
| |
| 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); |
| } |
| } |
| |
| |
| 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-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 { |
| 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); |
| } |
| |
| |
| .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 { |
| 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; } |
| } |
| |
| |
| .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); |
| } |
| |
| |
| .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-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); |
| } |
| } |
| |
| |
| html { |
| scroll-behavior: smooth; |
| } |
| |
| |
| *:focus-visible { |
| outline: 2px solid var(--accent-blue); |
| outline-offset: 2px; |
| border-radius: 4px; |
| } |
| |
| |
| .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 { |
| 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 { |
| 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 { |
| animation: badgePulse 2s ease-in-out infinite; |
| } |
| |
| @keyframes badgePulse { |
| 0%, 100% { transform: scale(1); } |
| 50% { transform: scale(1.1); } |
| } |
| |
| |
| button, a, input, select, textarea { |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| |
| 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; |
| } |
| |
| |
| .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-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-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); |
| } |
| </style> |
|
|
| |
| <svg style="display: none;" xmlns="http://www.w3.org/2000/svg"> |
| |
| <symbol id="icon-success" viewBox="0 0 24 24"> |
| <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="M8 12l2 2 4-4" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-error" viewBox="0 0 24 24"> |
| <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="M12 8v4M12 16h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-warning" viewBox="0 0 24 24"> |
| <path d="M12 2L2 22h20L12 2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M12 9v4M12 17h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-info" viewBox="0 0 24 24"> |
| <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="M12 16v-4M12 8h.01" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-market" viewBox="0 0 24 24"> |
| <path d="M3 3v18h18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M7 12l4-4 4 4 6-6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-monitor" viewBox="0 0 24 24"> |
| <rect x="2" y="3" width="20" height="14" rx="2" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="M8 21h8M12 17v4" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-advanced" viewBox="0 0 24 24"> |
| <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-settings" viewBox="0 0 24 24"> |
| <circle cx="12" cy="12" r="3" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="M12 1v6m0 6v6M5.64 5.64l4.24 4.24m4.24 4.24l4.24 4.24M1 12h6m6 0h6M5.64 18.36l4.24-4.24m4.24-4.24l4.24-4.24" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-hf" viewBox="0 0 24 24"> |
| <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="M8 12h8M12 8v8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-pools" viewBox="0 0 24 24"> |
| <path d="M21.5 2v6h-6M2.5 22v-6h6M2 11.5a10 10 0 0 1 18.8-4.3M22 12.5a10 10 0 0 1-18.8 4.2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-logs" viewBox="0 0 24 24"> |
| <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M14 2v6h6M16 13H8M16 17H8M10 9H8" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-resources" viewBox="0 0 24 24"> |
| <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M3.27 6.96L12 12.01l8.73-5.05M12 22.08V12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-reports" viewBox="0 0 24 24"> |
| <path d="M3 3v18h18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M18 17V9M12 17V5M6 17v-3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-search" viewBox="0 0 24 24"> |
| <circle cx="11" cy="11" r="8" fill="none" stroke="currentColor" stroke-width="2"/> |
| <path d="m21 21-4.35-4.35" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-refresh" viewBox="0 0 24 24"> |
| <path d="M23 4v6h-6M1 20v-6h6M3.51 9a10 10 0 0 1 17.8-4.3M20.49 15a10 10 0 0 1-17.8 4.3" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-trending-up" viewBox="0 0 24 24"> |
| <path d="M23 6l-9.5 9.5-5-5L1 18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M17 6h6v6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-trending-down" viewBox="0 0 24 24"> |
| <path d="M23 18l-9.5-9.5-5 5L1 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M17 18h6v-6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-volume" viewBox="0 0 24 24"> |
| <path d="M11 5L6 9H2v6h4l5 4V5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-diamond" viewBox="0 0 24 24"> |
| <path d="M6 3h12l4 6-10 12L2 9l4-6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M11 3l1 18M13 3l-1 18M6 3l5 18M18 3l-5 18" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-fire" viewBox="0 0 24 24"> |
| <path d="M8.5 14.5A2.5 2.5 0 0 0 11 12c0-1.38-.5-2-1-3-1.072-2.143-.224-4.054 2-6 .5 2.5 2 4.9 4 6.5 2 1.6 3 3.5 3 5.5a7 7 0 1 1-14 0c0-1.153.433-2.294 1-3a2.5 2.5 0 0 0 2.5 2.5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-link" viewBox="0 0 24 24"> |
| <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-export" viewBox="0 0 24 24"> |
| <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M7 10l5 5 5-5M12 15V3" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-delete" viewBox="0 0 24 24"> |
| <path d="M3 6h18M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M10 11v6M14 11v6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-brain" viewBox="0 0 24 24"> |
| <path d="M9.5 2A2.5 2.5 0 0 1 12 4.5v15a2.5 2.5 0 0 1-4.96.44L2 22v-4a2.5 2.5 0 0 1 2.5-2.5h5z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| <path d="M14.5 2A2.5 2.5 0 0 0 12 4.5v15a2.5 2.5 0 0 0 4.96.44L22 22v-4a2.5 2.5 0 0 0-2.5-2.5h-5z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-arrow-up" viewBox="0 0 24 24"> |
| <path d="M12 19V5M5 12l7-7 7 7" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| </symbol> |
|
|
| |
| <symbol id="icon-live" viewBox="0 0 24 24"> |
| <circle cx="12" cy="12" r="10" fill="currentColor"/> |
| </symbol> |
| </svg> |
| </head> |
|
|
| <body> |
| |
| <div class="progress-indicator" id="progressIndicator"> |
| <div class="progress-bar" id="progressBar"></div> |
| </div> |
|
|
| |
| <div class="toast-container" id="toastContainer"></div> |
|
|
| |
| <div class="loading-overlay" id="loadingOverlay"> |
| <div class="loading-spinner-large"></div> |
| <div class="loading-text" id="loadingText">در حال بارگذاری...</div> |
| </div> |
|
|
| |
| <div class="feedback-overlay" id="feedbackOverlay"> |
| <div class="feedback-card"> |
| <div class="feedback-icon" id="feedbackIcon">✅</div> |
| <div class="feedback-title" id="feedbackTitle">موفق!</div> |
| <div class="feedback-message" id="feedbackMessage">عملیات با موفقیت انجام شد</div> |
| <button class="refresh-btn ripple" onclick="hideFeedback()">بستن</button> |
| </div> |
| </div> |
|
|
| |
| <button class="fab ripple" onclick="scrollToTop()" title="بازگشت به بالا"> |
| ↑ |
| </button> |
|
|
| |
| <div id="ws-connection-status" class="ws-status-indicator disconnected"> |
| <div id="ws-status-dot" class="status-dot status-dot-offline"></div> |
| <span id="ws-status-text" class="ws-status-text">در حال اتصال...</span> |
| <div id="online-users-badge" class="badge badge-info badge-pulse" style="margin-left: 10px;">0</div> |
| </div> |
|
|
| <div class="container"> |
| |
| <div class="header"> |
| <div class="header-top"> |
| <div class="logo"> |
| <div class="logo-icon">₿</div> |
| <div class="logo-text"> |
| <h1>Crypto Monitor ULTIMATE</h1> |
| <p>Unified Dashboard • Real-time data from 100+ free APIs</p> |
| </div> |
| </div> |
| <div class="header-actions"> |
| <div class="live-indicator"> |
| <span class="status-icon"><svg><use href="#icon-live"></use></svg></span> |
| LIVE |
| </div> |
| <div class="status-badge"> |
| <div class="status-dot"></div> |
| <span id="statusText">All Systems Operational</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="tabs"> |
| <button class="tab active" onclick="switchTab('market')"> |
| <span class="tab-icon"><svg><use href="#icon-market"></use></svg></span> |
| Market |
| </button> |
| <button class="tab" onclick="switchTab('monitor')"> |
| <span class="tab-icon"><svg><use href="#icon-monitor"></use></svg></span> |
| API Monitor |
| </button> |
| <button class="tab" onclick="switchTab('advanced')"> |
| <span class="tab-icon"><svg><use href="#icon-advanced"></use></svg></span> |
| Advanced |
| </button> |
| <button class="tab" onclick="switchTab('admin')"> |
| <span class="tab-icon"><svg><use href="#icon-settings"></use></svg></span> |
| Admin |
| </button> |
| <button class="tab" onclick="switchTab('hf')"> |
| <span class="tab-icon"><svg><use href="#icon-hf"></use></svg></span> |
| HuggingFace |
| </button> |
| <button class="tab" onclick="switchTab('pools')"> |
| <span class="tab-icon"><svg><use href="#icon-pools"></use></svg></span> |
| Pools |
| </button> |
| <button class="tab" onclick="switchTab('logs')"> |
| <span class="tab-icon"><svg><use href="#icon-logs"></use></svg></span> |
| Logs |
| </button> |
| <button class="tab" onclick="switchTab('resources')"> |
| <span class="tab-icon"><svg><use href="#icon-resources"></use></svg></span> |
| Resources |
| </button> |
| <button class="tab" onclick="switchTab('reports')"> |
| <span class="tab-icon"><svg><use href="#icon-reports"></use></svg></span> |
| Reports |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-market" class="tab-content active"> |
| |
| <div class="stats-grid stats-grid--market"> |
| <div class="stat-card online-users-card"> |
| <div class="pulse-ring"></div> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <circle cx="9" cy="8" r="3" /> |
| <circle cx="17" cy="9" r="2.5" /> |
| <path d="M4 19v-1.5A4.5 4.5 0 018.5 13h1A4.5 4.5 0 0114 17.5V19" /> |
| <path d="M17 19v-1a3 3 0 00-3-3h-1.5" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value shimmer" id="active-users-count">0</div> |
| <div class="stat-label">کاربران آنلاین</div> |
| <div class="stat-change positive"> |
| <span>📊</span> |
| <span>کل نشستها: <span id="total-sessions-count">0</span></span> |
| </div> |
| <div class="animated-progress"></div> |
| </div> |
|
|
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <circle cx="12" cy="12" r="6" /> |
| <path d="M12 8v8" /> |
| <path d="M9.5 10.5h5" /> |
| <path d="M9.5 13.5h5" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="totalMarketCap">$0.00T</div> |
| <div class="stat-label">Total Market Cap</div> |
| <div class="stat-change positive" id="mcapChange"> |
| <span>↑</span> <span>0.0%</span> |
| </div> |
| </div> |
|
|
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <path d="M6 16V9" /> |
| <path d="M12 16V5" /> |
| <path d="M18 16v-7" /> |
| <path d="M4 16h16" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="totalVolume">$0.00B</div> |
| <div class="stat-label">24h Trading Volume</div> |
| <div class="stat-change positive"> |
| <span>↑</span> <span>Volume spike</span> |
| </div> |
| </div> |
|
|
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <circle cx="12" cy="12" r="6" /> |
| <path d="M10 8h3a2 2 0 010 4h-3h3a2 2 0 010 4h-3" /> |
| <path d="M11 6v2" /> |
| <path d="M11 16v2" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="btcDominance">0.0%</div> |
| <div class="stat-label">BTC Dominance</div> |
| <div class="stat-change"> |
| <span id="btcDomIcon">↑</span> <span id="btcDomChange">0.0%</span> |
| </div> |
| </div> |
|
|
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <path |
| d="M5.5 15c0 3.6 3 5.5 6.5 5.5s6.5-1.9 6.5-5.5c0-2.6-1.7-4.4-3.4-6.4-.4-.5-.8-1-1.1-1.6-.3-.6-.6-1.3-.8-2-1.6 1.4-3.3 3.2-3.3 5.1-1.2-.8-2.4-2-2.4-3.6C7 9 5.5 11.4 5.5 13.5Z" /> |
| <path d="M10.5 18.5c-1.1-.7-1.8-1.7-1.8-3 0-1.1.5-2 1.2-2.8" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="fearGreed">50</div> |
| <div class="stat-label">Fear & Greed Index</div> |
| <div class="stat-change" id="sentimentLabel"> |
| <span>Neutral</span> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title gradient-text"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-diamond"></use></svg></span> |
| Live Market Data |
| </div> |
| <button class="refresh-btn ripple" onclick="loadMarketData()" data-tooltip="بهروزرسانی دادههای بازار"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| Refresh |
| </button> |
| </div> |
| |
| |
| <div class="search-container"> |
| <span class="search-icon icon"><svg><use href="#icon-search"></use></svg></span> |
| <input type="text" class="search-input" id="marketSearch" placeholder="جستجوی ارز دیجیتال (مثال: Bitcoin, BTC, Ethereum)..." oninput="filterMarketTable()"> |
| </div> |
| |
| |
| <div class="filter-chips"> |
| <button class="filter-chip active" onclick="filterByCategory('all')">همه</button> |
| <button class="filter-chip" onclick="filterByCategory('top10')">Top 10</button> |
| <button class="filter-chip" onclick="filterByCategory('gainers')"> |
| <span class="icon icon-sm status-icon-success"><svg><use href="#icon-trending-up"></use></svg></span> |
| در حال رشد |
| </button> |
| <button class="filter-chip" onclick="filterByCategory('losers')"> |
| <span class="icon icon-sm status-icon-error"><svg><use href="#icon-trending-down"></use></svg></span> |
| در حال سقوط |
| </button> |
| <button class="filter-chip" onclick="filterByCategory('volume')"> |
| <span class="icon icon-sm"><svg><use href="#icon-volume"></use></svg></span> |
| حجم بالا |
| </button> |
| </div> |
| <div style="overflow-x: auto;"> |
| <table id="marketTable"> |
| <thead> |
| <tr> |
| <th>#</th> |
| <th>Name</th> |
| <th>Price</th> |
| <th>24h Change</th> |
| <th>Market Cap</th> |
| <th>Volume 24h</th> |
| </tr> |
| </thead> |
| <tbody id="marketTableBody"> |
| <tr> |
| <td colspan="6"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| |
| <div |
| style="display: grid; grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); gap: 30px; margin-bottom: 30px;"> |
| <div class="chart-container"> |
| <div class="section-title" style="margin-bottom: 20px;">📈 Market Dominance</div> |
| <canvas id="dominanceChart"></canvas> |
| </div> |
|
|
| <div class="chart-container"> |
| <div class="section-title" style="margin-bottom: 20px;">😱 Fear & Greed Index</div> |
| <div style="text-align: center;"> |
| <canvas id="gaugeChart"></canvas> |
| <div style="font-size: 48px; font-weight: 900; margin: 20px 0 10px;" id="sentimentValue">50 |
| </div> |
| <div style="color: var(--text-secondary); font-size: 16px; font-weight: 600;" |
| id="sentimentText">Neutral</div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-fire"></use></svg></span> |
| Trending Now |
| </div> |
| <div id="trendingGrid" |
| style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">🏦 Top DeFi Protocols</div> |
| <div id="defiList"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-monitor" class="tab-content"> |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <rect x="4" y="4" width="16" height="5" rx="2" /> |
| <rect x="4" y="11" width="16" height="5" rx="2" /> |
| <path d="M8 6h.01" /> |
| <path d="M8 13h.01" /> |
| <path d="M12 16v4" /> |
| <path d="M10 20h4" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="totalAPIs">0</div> |
| <div class="stat-label">Total APIs</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <circle cx="12" cy="12" r="7.5" /> |
| <path d="M9.2 12.2l2 2 4-4" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="onlineAPIs" style="color: var(--accent-green);">0</div> |
| <div class="stat-label">Online</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <circle cx="12" cy="12" r="7.5" /> |
| <path d="M9 9l6 6" /> |
| <path d="M15 9l-6 6" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="offlineAPIs" style="color: var(--accent-red);">0</div> |
| <div class="stat-label">Offline</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <path d="M11 3L6 13h5l-1 8 8-14h-5l1-4z" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="avgResponse" style="font-size: 28px;">0ms</div> |
| <div class="stat-label">Avg Response</div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-monitor"></use></svg></span> |
| API Providers Status |
| </div> |
| <button class="refresh-btn ripple" onclick="loadMonitorData()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| Refresh |
| </button> |
| </div> |
| <div style="overflow-x: auto;"> |
| <table> |
| <thead> |
| <tr> |
| <th>Provider</th> |
| <th>Category</th> |
| <th>Status</th> |
| <th>Response Time</th> |
| <th>Last Check</th> |
| </tr> |
| </thead> |
| <tbody id="providersTable"> |
| <tr> |
| <td colspan="5" style="text-align: center;">Loading...</td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-brain"></use></svg></span> |
| HuggingFace Sentiment Analysis |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Enter crypto-related text (one per line):</label> |
| <textarea class="form-textarea" id="sentimentText" rows="5" |
| placeholder="BTC strong breakout ETH looks weak Market is bullish">BTC strong breakout |
| ETH looks weak |
| Market is bullish today</textarea> |
| </div> |
| <button class="refresh-btn ripple" onclick="runSentiment()"> |
| <span class="icon icon-sm"><svg><use href="#icon-brain"></use></svg></span> |
| Analyze Sentiment |
| </button> |
| <div id="sentimentResult" |
| style="margin-top: 20px; padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; text-align: center; font-size: 36px; font-weight: 900;"> |
| —</div> |
| <pre id="sentimentDetails" |
| style="background: #1e293b; color: #e2e8f0; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin-top: 15px; max-height: 300px; overflow-y: auto;"></pre> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-advanced" class="tab-content"> |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <rect x="4" y="4" width="16" height="5" rx="2" /> |
| <rect x="4" y="11" width="16" height="5" rx="2" /> |
| <path d="M8 6h.01" /> |
| <path d="M8 13h.01" /> |
| <path d="M12 16v4" /> |
| <path d="M10 20h4" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="totalApis">0</div> |
| <div class="stat-label">Total APIs</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <path d="M6 4v7m0 5v4" /> |
| <circle cx="6" cy="11" r="2" /> |
| <path d="M12 4v2m0 4v10" /> |
| <circle cx="12" cy="8" r="2" /> |
| <path d="M18 4v9m0 4v3" /> |
| <circle cx="18" cy="15" r="2" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="activeTasks">0</div> |
| <div class="stat-label">Active Tasks</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <ellipse cx="12" cy="6" rx="7" ry="3" /> |
| <path d="M5 6v6c0 1.7 3.1 3 7 3s7-1.3 7-3V6" /> |
| <path d="M5 12c0 1.7 3.1 3 7 3s7-1.3 7-3" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="cachedData">0</div> |
| <div class="stat-label">Cached Data</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-header"> |
| <div class="stat-icon"> |
| <svg viewBox="0 0 24 24" aria-hidden="true"> |
| <path d="M8 3v4" /> |
| <path d="M16 3v4" /> |
| <path d="M6 11h12" /> |
| <path d="M10 15h4v6" /> |
| <path d="M8 7h8v4a6 6 0 01-12 0V7z" /> |
| </svg> |
| </div> |
| </div> |
| <div class="stat-value" id="wsConnections">0</div> |
| <div class="stat-label">WS Connections</div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title">🔧 Advanced Actions</div> |
| </div> |
| <div style="display: flex; gap: 15px; flex-wrap: wrap;"> |
| <button class="refresh-btn" onclick="exportJSON()">💾 Export JSON</button> |
| <button class="refresh-btn" onclick="exportCSV()">📊 Export CSV</button> |
| <button class="refresh-btn" onclick="createBackup()">🔄 Create Backup</button> |
| <button class="refresh-btn" onclick="clearCache()">🗑️ Clear Cache</button> |
| <button class="refresh-btn" onclick="forceUpdateAll()">🔃 Force Update All</button> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">📈 Recent Activity</div> |
| <div id="activityLog" |
| style="max-height: 400px; overflow-y: auto; font-family: monospace; font-size: 12px;"> |
| <div style="padding: 10px; border-left: 3px solid var(--accent-blue); margin-bottom: 8px;"> |
| <span style="opacity: 0.6;">--:--:--</span> Waiting for updates... |
| </div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">🔌 API Sources</div> |
| <div id="apiList"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-admin" class="tab-content"> |
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">➕ Add New API Source</div> |
| <div class="form-group"> |
| <label class="form-label">API Name</label> |
| <input type="text" class="form-input" id="newApiName" placeholder="e.g., CoinGecko"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">API URL</label> |
| <input type="text" class="form-input" id="newApiUrl" placeholder="https://api.example.com/endpoint"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Category</label> |
| <select class="form-select" id="newApiCategory"> |
| <option value="market_data">Market Data</option> |
| <option value="blockchain_explorers">Blockchain Explorers</option> |
| <option value="news">News & Social</option> |
| <option value="sentiment">Sentiment</option> |
| <option value="defi">DeFi</option> |
| <option value="nft">NFT</option> |
| </select> |
| </div> |
| <button class="refresh-btn" onclick="addNewAPI()">➕ Add API Source</button> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-logs"></use></svg></span> |
| Current API Sources |
| </div> |
| <div id="apisList">Loading...</div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-settings"></use></svg></span> |
| Settings |
| </div> |
| <div class="form-group"> |
| <label class="form-label">API Check Interval (seconds)</label> |
| <input type="number" class="form-input" id="checkInterval" value="30" min="10" max="300"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Dashboard Auto-Refresh (seconds)</label> |
| <input type="number" class="form-input" id="dashboardRefresh" value="30" min="5" max="300"> |
| </div> |
| <button class="refresh-btn" onclick="saveSettings()">💾 Save Settings</button> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-reports"></use></svg></span> |
| Statistics |
| </div> |
| <div class="stats-grid"> |
| <div class="stat-card"> |
| <div class="stat-value" id="statTotal">0</div> |
| <div class="stat-label">Total API Sources</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statOnline" style="color: var(--accent-green);">0</div> |
| <div class="stat-label">Currently Online</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="statOffline" style="color: var(--accent-red);">0</div> |
| <div class="stat-label">Currently Offline</div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-hf" class="tab-content"> |
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md" style="margin-left: 8px;"><svg><use href="#icon-reports"></use></svg></span> |
| Health Status |
| </div> |
| <button class="refresh-btn ripple" onclick="loadHFHealth()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| Refresh |
| </button> |
| </div> |
| <pre id="healthOutput" |
| style="background: #1e293b; color: #e2e8f0; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; max-height: 200px; overflow-y: auto;">Loading...</pre> |
| </div> |
|
|
| <div |
| style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 30px;"> |
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">🤖 Models Registry</div> |
| <button class="refresh-btn" onclick="loadModels()" style="margin-bottom: 15px;">Load Models</button> |
| <div id="modelsList" |
| style="max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 8px;"> |
| <p style="color: var(--text-secondary);">Click "Load Models" to fetch...</p> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">📚 Datasets Registry</div> |
| <button class="refresh-btn" onclick="loadDatasets()" style="margin-bottom: 15px;">Load |
| Datasets</button> |
| <div id="datasetsList" |
| style="max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 8px;"> |
| <p style="color: var(--text-secondary);">Click "Load Datasets" to fetch...</p> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">🔍 Search Registry</div> |
| <div class="form-group"> |
| <input type="text" class="form-input" id="searchQuery" |
| placeholder="Search query (e.g., crypto, bitcoin, sentiment)" value="crypto"> |
| </div> |
| <div style="display: flex; gap: 10px; margin-bottom: 15px;"> |
| <button class="refresh-btn" onclick="doSearch()">Search Models</button> |
| <button class="refresh-btn" onclick="doSearchDatasets()">Search Datasets</button> |
| </div> |
| <div id="searchResults" |
| style="max-height: 300px; overflow-y: auto; background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 8px;"> |
| <p style="color: var(--text-secondary);">Enter a query and click search...</p> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;"> |
| <span class="icon icon-md"><svg><use href="#icon-brain"></use></svg></span> |
| Sentiment Analysis |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Enter text samples (one per line):</label> |
| <textarea class="form-textarea" id="sentimentTexts" rows="5" |
| placeholder="BTC strong breakout ETH looks weak Market sentiment is bullish">BTC strong breakout |
| ETH looks weak |
| Crypto market is bullish today</textarea> |
| </div> |
| <button class="refresh-btn ripple" onclick="doSentiment()"> |
| <span class="icon icon-sm"><svg><use href="#icon-brain"></use></svg></span> |
| Run Sentiment Analysis |
| </button> |
| <div id="voteDisplay" |
| style="margin-top: 20px; padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; text-align: center; font-size: 36px; font-weight: 900;"> |
| —</div> |
| <pre id="sentimentOutput" |
| style="background: #1e293b; color: #e2e8f0; padding: 15px; border-radius: 8px; overflow-x: auto; font-size: 12px; margin-top: 15px; max-height: 300px; overflow-y: auto;">Results will appear here...</pre> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-logs" class="tab-content"> |
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md"><svg><use href="#icon-logs"></use></svg></span> |
| Log Management |
| </div> |
| <div style="display: flex; gap: 10px;"> |
| <button class="refresh-btn ripple" onclick="loadLogs()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| Refresh |
| </button> |
| <button class="refresh-btn ripple" onclick="exportLogsJSON()" |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);"> |
| <span class="icon icon-sm"><svg><use href="#icon-export"></use></svg></span> |
| Export JSON |
| </button> |
| <button class="refresh-btn ripple" onclick="exportLogsCSV()" |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);"> |
| <span class="icon icon-sm"><svg><use href="#icon-reports"></use></svg></span> |
| Export CSV |
| </button> |
| <button class="refresh-btn ripple" onclick="clearAllLogs()" |
| style="background: rgba(239, 68, 68, 0.2); color: var(--accent-red);"> |
| <span class="icon icon-sm"><svg><use href="#icon-delete"></use></svg></span> |
| Clear |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div |
| style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 20px;"> |
| <div class="form-group"> |
| <label class="form-label">Level</label> |
| <select class="form-select" id="logLevelFilter" onchange="loadLogs()"> |
| <option value="">All Levels</option> |
| <option value="debug">Debug</option> |
| <option value="info">Info</option> |
| <option value="warning">Warning</option> |
| <option value="error">Error</option> |
| <option value="critical">Critical</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Category</label> |
| <select class="form-select" id="logCategoryFilter" onchange="loadLogs()"> |
| <option value="">All Categories</option> |
| <option value="provider">Provider</option> |
| <option value="pool">Pool</option> |
| <option value="api">API</option> |
| <option value="system">System</option> |
| <option value="health_check">Health Check</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Search</label> |
| <input type="text" class="form-input" id="logSearch" placeholder="Search logs..." |
| onkeyup="loadLogs()"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Limit</label> |
| <input type="number" class="form-input" id="logLimit" value="100" min="10" max="1000" |
| onchange="loadLogs()"> |
| </div> |
| </div> |
|
|
| |
| <div class="stats-grid" style="margin-bottom: 20px;"> |
| <div class="stat-card"> |
| <div class="stat-value" id="totalLogs">0</div> |
| <div class="stat-label">Total Logs</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="errorLogs" style="color: var(--accent-red);">0</div> |
| <div class="stat-label">Errors</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="infoLogs" style="color: var(--accent-blue);">0</div> |
| <div class="stat-label">Info</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="warningLogs" style="color: var(--accent-yellow);">0</div> |
| <div class="stat-label">Warnings</div> |
| </div> |
| </div> |
|
|
| |
| <div style="overflow-x: auto;"> |
| <table> |
| <thead> |
| <tr> |
| <th>Time</th> |
| <th>Level</th> |
| <th>Category</th> |
| <th>Message</th> |
| <th>Provider</th> |
| <th>Response Time</th> |
| </tr> |
| </thead> |
| <tbody id="logsTableBody"> |
| <tr> |
| <td colspan="6" style="text-align: center;">Loading logs...</td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-resources" class="tab-content"> |
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title">📦 Resource Management</div> |
| <div style="display: flex; gap: 10px;"> |
| <button class="refresh-btn" onclick="loadResources()">🔄 Refresh</button> |
| <button class="refresh-btn" onclick="exportResourcesJSON()" |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);">💾 Export |
| JSON</button> |
| <button class="refresh-btn" onclick="exportResourcesCSV()" |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);">📊 Export |
| CSV</button> |
| <button class="refresh-btn" onclick="backupResources()" |
| style="background: rgba(59, 130, 246, 0.2); color: var(--accent-blue);">💾 Backup</button> |
| <button class="refresh-btn" onclick="showImportModal()" |
| style="background: rgba(139, 92, 246, 0.2); color: var(--accent-purple);">📥 Import</button> |
| </div> |
| </div> |
|
|
| |
| <div class="stats-grid" style="margin-bottom: 20px;"> |
| <div class="stat-card"> |
| <div class="stat-value" id="totalResources">0</div> |
| <div class="stat-label">Total Resources</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="freeResources" style="color: var(--accent-green);">0</div> |
| <div class="stat-label">Free APIs</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="paidResources" style="color: var(--accent-yellow);">0</div> |
| <div class="stat-label">Paid APIs</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" id="authResources">0</div> |
| <div class="stat-label">Requires Auth</div> |
| </div> |
| </div> |
|
|
| |
| <div style="margin-bottom: 20px;"> |
| <label class="form-label">Filter by Category</label> |
| <select class="form-select" id="resourceCategoryFilter" onchange="loadResources()" |
| style="max-width: 300px;"> |
| <option value="">All Categories</option> |
| <option value="market_data">Market Data</option> |
| <option value="exchange">Exchange</option> |
| <option value="blockchain_explorer">Block Explorer</option> |
| <option value="rpc">RPC</option> |
| <option value="defi">DeFi</option> |
| <option value="news">News</option> |
| <option value="sentiment">Sentiment</option> |
| <option value="analytics">Analytics</option> |
| </select> |
| </div> |
|
|
| |
| <div id="resourcesGrid" |
| style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 20px;"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="importModal" class="modal"> |
| <div class="modal-content"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> |
| <h2 style="font-size: 24px; font-weight: 800;">📥 Import Resources</h2> |
| <button onclick="closeImportModal()" |
| style="background: none; border: none; color: var(--text-secondary); font-size: 28px; cursor: pointer;">×</button> |
| </div> |
| <form id="importForm"> |
| <div class="form-group"> |
| <label class="form-label">File Path</label> |
| <input type="text" class="form-input" id="importFilePath" placeholder="path/to/file.json" |
| required> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Import Mode</label> |
| <select class="form-select" id="importMode"> |
| <option value="true">Merge (Add to existing)</option> |
| <option value="false">Replace (Overwrite all)</option> |
| </select> |
| </div> |
| <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;"> |
| <button type="button" class="refresh-btn" onclick="closeImportModal()" |
| style="background: rgba(239, 68, 68, 0.2); color: var(--accent-red);">Cancel</button> |
| <button type="submit" class="refresh-btn">Import</button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-reports" class="tab-content"> |
| |
| <div class="market-section" id="systemAlertsSection" style="display: none;"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md status-icon-warning"><svg><use href="#icon-warning"></use></svg></span> |
| System Status & Alerts |
| </div> |
| <button class="refresh-btn ripple" onclick="loadSystemAlerts()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| Refresh |
| </button> |
| </div> |
| <div id="systemAlertsContainer"></div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md"><svg><use href="#icon-search"></use></svg></span> |
| System Diagnostics |
| </div> |
| <div style="display: flex; gap: 10px;"> |
| <button class="refresh-btn ripple" onclick="runDiagnostics(false)" |
| style="background: rgba(59, 130, 246, 0.2); color: var(--accent-blue);"> |
| <span class="icon icon-sm"><svg><use href="#icon-search"></use></svg></span> |
| بررسی |
| </button> |
| <button class="refresh-btn ripple" onclick="runDiagnostics(true)" |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);"> |
| <span class="icon icon-sm"><svg><use href="#icon-settings"></use></svg></span> |
| بررسی و تعمیر |
| </button> |
| <button class="refresh-btn ripple" onclick="loadReports()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| بهروزرسانی |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div id="diagnosticsResults" style="margin-top: 20px;"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md"><svg><use href="#icon-pools"></use></svg></span> |
| Auto-Discovery Service Report |
| </div> |
| <button class="refresh-btn ripple" onclick="loadDiscoveryReport()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| بهروزرسانی |
| </button> |
| </div> |
| <div id="discoveryReport"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title"> |
| <span class="icon icon-md"><svg><use href="#icon-brain"></use></svg></span> |
| HuggingFace Models Status Report |
| </div> |
| <button class="refresh-btn ripple" onclick="loadModelsReport()"> |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> |
| بهروزرسانی |
| </button> |
| </div> |
| <div id="modelsReport"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="tab-pools" class="tab-content"> |
| <div class="market-section"> |
| <div class="section-header"> |
| <div class="section-title">🔄 Source Pool Management</div> |
| <div style="display: flex; gap: 10px;"> |
| <button class="refresh-btn" onclick="showCreatePoolModal()">➕ Create Pool</button> |
| <button class="refresh-btn" onclick="loadPools()">🔄 Refresh</button> |
| </div> |
| </div> |
| <div id="poolsContainer" |
| style="display: grid; grid-template-columns: repeat(auto-fill, minmax(400px, 1fr)); gap: 20px;"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="market-section"> |
| <div class="section-title" style="margin-bottom: 20px;">📜 Rotation History</div> |
| <div id="rotationHistory" style="max-height: 400px; overflow-y: auto;"> |
| <div class="loading"> |
| <div class="spinner"></div> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div id="createPoolModal" class="modal"> |
| <div class="modal-content"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> |
| <h2 style="font-size: 24px; font-weight: 800;">➕ Create New Pool</h2> |
| <button onclick="closeCreatePoolModal()" |
| style="background: none; border: none; color: var(--text-secondary); font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">×</button> |
| </div> |
| <form id="createPoolForm"> |
| <div class="form-group"> |
| <label class="form-label">Pool Name</label> |
| <input type="text" class="form-input" id="poolName" required |
| placeholder="e.g., Market Data Pool"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Category</label> |
| <select class="form-select" id="poolCategory" required> |
| <option value="market_data">Market Data</option> |
| <option value="blockchain_explorers">Blockchain Explorers</option> |
| <option value="news">News & Social</option> |
| <option value="sentiment">Sentiment</option> |
| <option value="defi">DeFi</option> |
| <option value="nft">NFT</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Rotation Strategy</label> |
| <select class="form-select" id="rotationStrategy" required> |
| <option value="round_robin">Round Robin</option> |
| <option value="priority">Priority Based</option> |
| <option value="weighted">Weighted</option> |
| <option value="least_used">Least Used</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Description (optional)</label> |
| <textarea class="form-textarea" id="poolDescription" rows="3" |
| placeholder="Pool description..."></textarea> |
| </div> |
| <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;"> |
| <button type="button" class="refresh-btn" onclick="closeCreatePoolModal()" |
| style="background: rgba(239, 68, 68, 0.2); color: var(--accent-red);">Cancel</button> |
| <button type="submit" class="refresh-btn">Create Pool</button> |
| </div> |
| </form> |
| </div> |
| </div> |
|
|
| |
| <div id="addMemberModal" class="modal"> |
| <div class="modal-content"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> |
| <h2 style="font-size: 24px; font-weight: 800;">➕ Add Provider to Pool</h2> |
| <button onclick="closeAddMemberModal()" |
| style="background: none; border: none; color: var(--text-secondary); font-size: 28px; cursor: pointer; padding: 0; width: 32px; height: 32px; display: flex; align-items: center; justify-content: center;">×</button> |
| </div> |
| <form id="addMemberForm"> |
| <div class="form-group"> |
| <label class="form-label">Provider</label> |
| <select class="form-select" id="memberProvider" required> |
| <option value="">Select a provider...</option> |
| </select> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Priority (1-10, higher = better)</label> |
| <input type="number" class="form-input" id="memberPriority" value="1" min="1" max="10"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Weight (1-100, for weighted strategy)</label> |
| <input type="number" class="form-input" id="memberWeight" value="1" min="1" max="100"> |
| </div> |
| <div style="display: flex; gap: 12px; justify-content: flex-end; margin-top: 20px;"> |
| <button type="button" class="refresh-btn" onclick="closeAddMemberModal()" |
| style="background: rgba(239, 68, 68, 0.2); color: var(--accent-red);">Cancel</button> |
| <button type="submit" class="refresh-btn">Add Member</button> |
| </div> |
| </form> |
| </div> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let charts = {}; |
| let wsConnection = null; |
| let currentTab = 'market'; |
| |
| |
| function switchTab(tabName) { |
| currentTab = tabName; |
| document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); |
| document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active')); |
| |
| event.target.classList.add('active'); |
| document.getElementById(`tab-${tabName}`).classList.add('active'); |
| |
| |
| if (tabName === 'market') { |
| loadMarketData(); |
| } else if (tabName === 'monitor') { |
| loadMonitorData(); |
| } else if (tabName === 'advanced') { |
| loadAdvancedData(); |
| } else if (tabName === 'admin') { |
| loadAdminData(); |
| } else if (tabName === 'hf') { |
| loadHFHealth(); |
| } else if (tabName === 'pools') { |
| loadPools(); |
| } else if (tabName === 'logs') { |
| loadLogs(); |
| } else if (tabName === 'resources') { |
| loadResources(); |
| } else if (tabName === 'reports') { |
| loadReports(); |
| } |
| } |
| |
| |
| document.addEventListener('DOMContentLoaded', async () => { |
| await loadMarketData(); |
| initCharts(); |
| connectWebSocket(); |
| setInterval(() => { |
| if (currentTab === 'market') loadMarketData(); |
| else if (currentTab === 'monitor') loadMonitorData(); |
| }, 60000); |
| }); |
| |
| |
| async function loadMarketData() { |
| try { |
| |
| const marketTableBody = document.getElementById('marketTableBody'); |
| if (marketTableBody) { |
| marketTableBody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px;"><div class="loading"><div class="spinner"></div></div><div style="margin-top: 10px; color: var(--text-secondary);">در حال بارگذاری دادههای بازار...</div></td></tr>'; |
| } |
| |
| 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); |
| |
| |
| 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([ |
| marketRes.json(), |
| statsRes.json(), |
| sentimentRes.json(), |
| trendingRes.json(), |
| defiRes.json() |
| ]); |
| |
| |
| 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'); |
| } |
| |
| 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'); |
| } |
| |
| |
| |
| |
| 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'); |
| } |
| |
| |
| 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 = `<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);"> |
| <div style="font-size: 24px; margin-bottom: 10px;">❌</div> |
| <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری دادهها</div> |
| <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'خطای نامشخص'}</div> |
| <button onclick="loadMarketData()" style="margin-top: 15px; padding: 10px 20px; background: var(--accent-blue); border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: 600;">تلاش مجدد</button> |
| </td></tr>`; |
| } |
| showToast('❌ خطا در بارگذاری دادههای بازار: ' + (error.message || 'خطای نامشخص'), 'error'); |
| } |
| } |
| |
| function updateStats(stats, sentiment) { |
| try { |
| |
| 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 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 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 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) + '%'; |
| } |
| |
| |
| |
| |
| let fg = 50; |
| let classification = 'Neutral'; |
| |
| if (sentiment.fear_greed_index && typeof sentiment.fear_greed_index === 'object') { |
| |
| 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') { |
| |
| fg = sentiment.fear_greed_value; |
| classification = sentiment.classification || 'Neutral'; |
| } else if (typeof sentiment.value !== 'undefined') { |
| |
| fg = sentiment.value; |
| classification = sentiment.classification || 'Neutral'; |
| } |
| |
| const fearGreedEl = document.getElementById('fearGreed'); |
| if (fearGreedEl) { |
| fearGreedEl.textContent = fg; |
| } |
| const sentimentLabelEl = document.getElementById('sentimentLabel'); |
| if (sentimentLabelEl) { |
| sentimentLabelEl.innerHTML = `<span>${classification}</span>`; |
| |
| if (fg < 25) { |
| sentimentLabelEl.style.color = 'var(--accent-red)'; |
| } else if (fg < 45) { |
| sentimentLabelEl.style.color = 'var(--accent-yellow)'; |
| } else if (fg < 55) { |
| sentimentLabelEl.style.color = 'var(--text-secondary)'; |
| } else if (fg < 75) { |
| sentimentLabelEl.style.color = 'var(--accent-blue)'; |
| } else { |
| 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'); |
| if (tbody) { |
| tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ دادهای یافت نشد</td></tr>'; |
| } |
| return; |
| } |
| |
| const tbody = document.getElementById('marketTableBody'); |
| if (!tbody) return; |
| |
| |
| 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 ` |
| <tr data-name="${name.toLowerCase()}" data-symbol="${symbol.toLowerCase()}" data-change="${change24h}" data-rank="${crypto.rank || index + 1}"> |
| <td style="font-weight: 700; color: var(--text-secondary);">${crypto.rank || index + 1}</td> |
| <td> |
| <div class="crypto-name"> |
| ${crypto.image ? `<img src="${crypto.image}" class="crypto-img" alt="${symbol}" onerror="this.style.display='none'">` : |
| `<div class="crypto-img" style="background: linear-gradient(135deg, #3b82f6, #8b5cf6); display: flex; align-items: center; justify-content: center; font-weight: 700; color: white;">${symbol[0] || '?'}</div>`} |
| <div> |
| <div style="font-weight: 600;">${name}</div> |
| <div class="crypto-symbol">${symbol}</div> |
| </div> |
| </div> |
| </td> |
| <td class="price number-counter">$${price.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 6 })}</td> |
| <td><span class="change ${changeClass} pulse-data">${changeIcon} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}%</span></td> |
| <td style="font-weight: 600;">$${(marketCap / 1e9).toFixed(2)}B</td> |
| <td style="color: var(--text-secondary);">$${(volume24h / 1e9).toFixed(2)}B</td> |
| </tr> |
| `; |
| }).join(''); |
| } catch (error) { |
| console.error('Error updating market table:', error); |
| const tbody = document.getElementById('marketTableBody'); |
| if (tbody) { |
| tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش دادهها</td></tr>'; |
| } |
| } |
| } |
| |
| function updateTrending(trending) { |
| try { |
| const grid = document.getElementById('trendingGrid'); |
| if (!grid) return; |
| |
| if (!trending || !Array.isArray(trending) || trending.length === 0) { |
| grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ ترندی یافت نشد</div>'; |
| return; |
| } |
| |
| grid.innerHTML = trending.map((coin, index) => { |
| const name = coin.name || 'نامشخص'; |
| const symbol = coin.symbol || 'N/A'; |
| return ` |
| <div style="background: rgba(59, 130, 246, 0.1); border: 1px solid rgba(59, 130, 246, 0.2); border-radius: 12px; padding: 15px; display: flex; align-items: center; gap: 12px;"> |
| <div style="font-size: 20px; font-weight: 900; color: var(--accent-yellow);">#${index + 1}</div> |
| ${coin.thumb ? `<img src="${coin.thumb}" style="width: 32px; height: 32px; border-radius: 8px;" onerror="this.style.display='none'">` : ''} |
| <div> |
| <div style="font-weight: 600;">${name}</div> |
| <div style="font-size: 12px; color: var(--text-secondary);">${symbol}</div> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } catch (error) { |
| console.error('Error updating trending:', error); |
| const grid = document.getElementById('trendingGrid'); |
| if (grid) { |
| grid.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش ترندها</div>'; |
| } |
| } |
| } |
| |
| function updateDeFi(defi) { |
| try { |
| const list = document.getElementById('defiList'); |
| if (!list) return; |
| |
| const protocols = defi && defi.protocols ? defi.protocols : []; |
| const totalTvl = defi && defi.total_tvl ? defi.total_tvl : 0; |
| |
| list.innerHTML = ` |
| <div class="stat-card" style="margin-bottom: 20px; text-align: center; background: linear-gradient(135deg, rgba(59, 130, 246, 0.2), rgba(139, 92, 246, 0.2));"> |
| <div class="stat-value gradient-text" style="font-size: 42px; margin-bottom: 8px;">$${(totalTvl / 1e9).toFixed(2)}B</div> |
| <div class="stat-label" style="font-size: 16px;">Total Value Locked</div> |
| </div> |
| <div style="display: grid; gap: 12px;"> |
| ${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 ` |
| <div class="stat-card" style="animation-delay: ${i * 0.05}s; cursor: pointer;" onclick="showToast('${name}: $${(tvl / 1e9).toFixed(2)}B TVL', 'info', 'DeFi Protocol')"> |
| <div style="display: flex; justify-content: space-between; align-items: center;"> |
| <div> |
| <div style="font-weight: 700; font-size: 16px; margin-bottom: 4px;">${i + 1}. ${name}</div> |
| <div style="font-size: 12px; color: var(--text-secondary); display: flex; align-items: center; gap: 6px;"> |
| <span>🔗</span> <span>${chain}</span> |
| </div> |
| </div> |
| <div style="text-align: right;"> |
| <div class="stat-value" style="font-size: 18px; margin-bottom: 4px;">$${(tvl / 1e9).toFixed(2)}B</div> |
| <div class="stat-change ${changeClass}" style="font-size: 13px;"> |
| ${change24h >= 0 ? '📈' : '📉'} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}% |
| </div> |
| </div> |
| </div> |
| </div> |
| `; |
| }).join('') : '<div class="empty-state"><div class="empty-state-icon">📦</div><div>هیچ پروتکلی یافت نشد</div></div>'} |
| </div> |
| `; |
| } catch (error) { |
| console.error('Error updating DeFi:', error); |
| const list = document.getElementById('defiList'); |
| if (list) { |
| list.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--accent-red);">خطا در نمایش دادههای DeFi</div>'; |
| } |
| } |
| } |
| |
| function initCharts() { |
| Chart.defaults.color = '#9ca3af'; |
| Chart.defaults.borderColor = 'rgba(255, 255, 255, 0.1)'; |
| |
| charts.dominance = new Chart(document.getElementById('dominanceChart'), { |
| type: 'doughnut', |
| data: { |
| labels: ['Bitcoin', 'Ethereum', 'Others'], |
| datasets: [{ |
| data: [45, 18, 37], |
| backgroundColor: ['#f7931a', '#627eea', '#8b5cf6'], |
| borderWidth: 0 |
| }] |
| }, |
| options: { |
| responsive: true, |
| plugins: { |
| legend: { position: 'bottom', labels: { padding: 15, font: { size: 12, weight: 600 } } } |
| } |
| } |
| }); |
| |
| charts.gauge = new Chart(document.getElementById('gaugeChart'), { |
| type: 'doughnut', |
| data: { |
| datasets: [{ |
| data: [50, 50], |
| backgroundColor: ['#3b82f6', 'rgba(255, 255, 255, 0.1)'], |
| borderWidth: 0 |
| }] |
| }, |
| options: { |
| rotation: -90, |
| circumference: 180, |
| cutout: '75%', |
| plugins: { legend: { display: false }, tooltip: { enabled: false } } |
| } |
| }); |
| } |
| |
| function updateCharts(market, sentiment) { |
| const btcDom = market.global.btc_dominance; |
| const ethDom = market.global.eth_dominance; |
| charts.dominance.data.datasets[0].data = [btcDom, ethDom, 100 - btcDom - ethDom]; |
| charts.dominance.update(); |
| |
| const fg = sentiment.fear_greed_index.value; |
| charts.gauge.data.datasets[0].data = [fg, 100 - fg]; |
| |
| if (fg < 25) { |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#ef4444'; |
| } else if (fg < 45) { |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#f59e0b'; |
| } else if (fg < 55) { |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#6b7280'; |
| } else if (fg < 75) { |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#3b82f6'; |
| } else { |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#10b981'; |
| } |
| |
| charts.gauge.update(); |
| document.getElementById('sentimentValue').textContent = fg; |
| document.getElementById('sentimentText').textContent = sentiment.fear_greed_index.classification; |
| } |
| |
| |
| let wsConnectAttempts = 0; |
| const MAX_WS_CONNECT_ATTEMPTS = 10; |
| let wsStatsInterval = null; |
| |
| function connectWebSocket() { |
| |
| |
| |
| |
| if (window.wsClient && typeof window.wsClient.on === 'function' && typeof window.wsClient.requestStats === 'function') { |
| console.log('✅ WebSocket Client آماده است'); |
| wsConnectAttempts = 0; |
| |
| |
| 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' && typeof updateProviderStatsDisplay === 'function') { |
| updateProviderStatsDisplay(message.data); |
| } |
| }); |
| |
| window.wsClient.on('market_update', (message) => { |
| console.log('💰 Market update'); |
| if (currentTab === 'market') { |
| loadMarketData(); |
| } |
| }); |
| |
| |
| setTimeout(() => { |
| if (window.wsClient && window.wsClient.isConnected) { |
| window.wsClient.requestStats(); |
| } |
| }, 1000); |
| |
| |
| if (!wsStatsInterval) { |
| wsStatsInterval = setInterval(() => { |
| if (window.wsClient && window.wsClient.isConnected) { |
| window.wsClient.requestStats(); |
| } |
| }, 10000); |
| } |
| } else { |
| wsConnectAttempts++; |
| if (wsConnectAttempts < MAX_WS_CONNECT_ATTEMPTS) { |
| |
| 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 به درستی لود شده باشد.'); |
| |
| setTimeout(() => { |
| if (!window.wsClient) { |
| console.warn('⚠️ WebSocket Client غیرفعال است. برخی ویژگیهای real-time ممکن است کار نکنند.'); |
| console.warn('⚠️ برای فعال کردن WebSocket، صفحه را refresh کنید (Ctrl+F5 برای clear cache).'); |
| } |
| }, 5000); |
| |
| return; |
| } |
| } |
| } |
| |
| |
| function updateOnlineStats(data) { |
| const activeEl = document.getElementById('active-users-count'); |
| const totalEl = document.getElementById('total-sessions-count'); |
| |
| if (data.active_connections !== undefined && activeEl) { |
| activeEl.textContent = data.active_connections; |
| |
| activeEl.classList.add('count-updated'); |
| setTimeout(() => activeEl.classList.remove('count-updated'), 500); |
| } |
| |
| if (data.total_sessions !== undefined && totalEl) { |
| totalEl.textContent = data.total_sessions; |
| } |
| } |
| |
| |
| function updateProviderStatsDisplay(stats) { |
| if (stats.summary) { |
| const summary = stats.summary; |
| if (document.getElementById('totalAPIs')) { |
| document.getElementById('totalAPIs').textContent = summary.total_providers || 0; |
| } |
| if (document.getElementById('onlineAPIs')) { |
| document.getElementById('onlineAPIs').textContent = summary.online || 0; |
| } |
| if (document.getElementById('offlineAPIs')) { |
| document.getElementById('offlineAPIs').textContent = summary.offline || 0; |
| } |
| } |
| } |
| |
| |
| async function loadMonitorData() { |
| try { |
| |
| const tbody = document.getElementById('providersTable'); |
| if (tbody) { |
| tbody.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 40px;"><div class="loading"><div class="spinner"></div></div><div style="margin-top: 10px; color: var(--text-secondary);">در حال بارگذاری وضعیت APIها...</div></td></tr>'; |
| } |
| |
| const [statusRes, providersRes] = await Promise.all([ |
| fetch('/api/status'), |
| fetch('/api/providers') |
| ]); |
| |
| |
| if (!statusRes.ok) throw new Error(`خطا در دریافت وضعیت: ${statusRes.status}`); |
| if (!providersRes.ok) throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status}`); |
| |
| const [status, providers] = await Promise.all([ |
| statusRes.json(), |
| providersRes.json() |
| ]); |
| |
| |
| if (!status || typeof status.total_providers === 'undefined') throw new Error('دادههای وضعیت نامعتبر است'); |
| if (!providers || !Array.isArray(providers)) throw new Error('لیست APIها نامعتبر است'); |
| |
| 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 = '<tr><td colspan="5" style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ APIای یافت نشد</td></tr>'; |
| } else { |
| tbody.innerHTML = providers.map(p => { |
| let statusClass = 'badge-success'; |
| if (p.status === 'offline') statusClass = 'badge-danger'; |
| else if (p.status === 'degraded') statusClass = 'badge-warning'; |
| |
| return ` |
| <tr> |
| <td><strong>${p.name || 'نامشخص'}</strong></td> |
| <td><span class="badge badge-info">${p.category || 'نامشخص'}</span></td> |
| <td><span class="badge ${statusClass}">${(p.status || 'unknown').toUpperCase()}</span></td> |
| <td>${p.response_time_ms || p.avg_response_time_ms || 0}ms</td> |
| <td style="color: var(--text-secondary); font-size: 13px;">${p.last_fetch ? new Date(p.last_fetch).toLocaleTimeString() : 'نامشخص'}</td> |
| </tr> |
| `; |
| }).join(''); |
| } |
| } |
| } catch (error) { |
| console.error('Error loading monitor data:', error); |
| const tbody = document.getElementById('providersTable'); |
| if (tbody) { |
| tbody.innerHTML = `<tr><td colspan="5" style="text-align: center; padding: 40px; color: var(--accent-red);"> |
| <div style="font-size: 24px; margin-bottom: 10px;">❌</div> |
| <div style="font-weight: 600; margin-bottom: 5px;">خطا در بارگذاری دادهها</div> |
| <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'خطای نامشخص'}</div> |
| <button onclick="loadMonitorData()" style="margin-top: 15px; padding: 10px 20px; background: var(--accent-blue); border: none; border-radius: 8px; color: white; cursor: pointer; font-weight: 600;">تلاش مجدد</button> |
| </td></tr>`; |
| } |
| showToast('❌ خطا در بارگذاری دادههای مانیتور: ' + (error.message || 'خطای نامشخص'), 'error'); |
| } |
| } |
| |
| async function runSentiment() { |
| const text = document.getElementById('sentimentText').value; |
| const texts = text.split('\n').filter(t => t.trim()); |
| |
| if (texts.length === 0) { |
| showToast('Please enter at least one line of text', 'info'); |
| return; |
| } |
| |
| try { |
| document.getElementById('sentimentResult').textContent = '⏳ Analyzing...'; |
| document.getElementById('sentimentDetails').textContent = ''; |
| |
| const res = await fetch('/api/hf/run-sentiment', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ texts }) |
| }); |
| |
| const data = await res.json(); |
| const vote = data.vote || 0; |
| let emoji = '😐'; |
| let color = 'var(--text-secondary)'; |
| |
| if (vote > 0.2) { |
| emoji = '📈'; |
| color = 'var(--accent-green)'; |
| } else if (vote < -0.2) { |
| emoji = '📉'; |
| color = 'var(--accent-red)'; |
| } |
| |
| document.getElementById('sentimentResult').innerHTML = `<span style="color: ${color};">${emoji} ${vote.toFixed(3)}</span>`; |
| document.getElementById('sentimentDetails').textContent = JSON.stringify(data, null, 2); |
| } catch (error) { |
| document.getElementById('sentimentResult').innerHTML = '<span style="color: var(--accent-red);">❌ Error</span>'; |
| document.getElementById('sentimentDetails').textContent = 'Error: ' + error.message; |
| } |
| } |
| |
| |
| async function loadAdvancedData() { |
| try { |
| const response = await fetch('/api/v2/status'); |
| const data = await response.json(); |
| |
| document.getElementById('totalApis').textContent = data.services.config_loader.apis_loaded; |
| document.getElementById('activeTasks').textContent = data.services.scheduler.total_tasks; |
| document.getElementById('cachedData').textContent = data.services.persistence.cached_apis; |
| document.getElementById('wsConnections').textContent = data.services.websocket.total_connections; |
| |
| const apisResponse = await fetch('/api/v2/config/apis'); |
| const apisData = await apisResponse.json(); |
| displayAPIs(apisData.apis); |
| } catch (error) { |
| console.error('Error loading advanced data:', error); |
| } |
| } |
| |
| function displayAPIs(apis) { |
| const listElement = document.getElementById('apiList'); |
| listElement.innerHTML = ''; |
| |
| for (const [apiId, api] of Object.entries(apis)) { |
| const item = document.createElement('div'); |
| item.style.cssText = 'background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 12px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;'; |
| item.innerHTML = ` |
| <div> |
| <div style="font-weight: 600;">${api.name}</div> |
| <div style="font-size: 12px; color: var(--text-secondary);">${api.category}</div> |
| </div> |
| <button class="refresh-btn" onclick="forceUpdate('${apiId}')" style="padding: 6px 12px; font-size: 12px;">🔄 Update</button> |
| `; |
| listElement.appendChild(item); |
| } |
| } |
| |
| async function exportJSON() { |
| try { |
| const response = await fetch('/api/v2/export/json', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ include_history: true }) |
| }); |
| const data = await response.json(); |
| showToast('✅ JSON export created!', 'success'); |
| addLog(`Exported to JSON: ${data.filepath}`); |
| } catch (error) { |
| showToast('❌ Export failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function exportCSV() { |
| try { |
| const response = await fetch('/api/v2/export/csv', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ flatten: true }) |
| }); |
| const data = await response.json(); |
| showToast('✅ CSV export created!', 'success'); |
| addLog(`Exported to CSV: ${data.filepath}`); |
| } catch (error) { |
| showToast('❌ Export failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function createBackup() { |
| try { |
| const response = await fetch('/api/v2/backup', { method: 'POST' }); |
| const data = await response.json(); |
| showToast('✅ Backup created!', 'success'); |
| addLog(`Backup created: ${data.backup_file}`); |
| } catch (error) { |
| showToast('❌ Backup failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function clearCache() { |
| if (!confirm('Clear all cached data?')) return; |
| try { |
| await fetch('/api/v2/cleanup/cache', { method: 'POST' }); |
| showToast('✅ Cache cleared!', 'success'); |
| addLog('Cache cleared'); |
| } catch (error) { |
| showToast('❌ Failed to clear cache', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function forceUpdateAll() { |
| try { |
| const response = await fetch('/api/v2/config/apis'); |
| const data = await response.json(); |
| for (const apiId of Object.keys(data.apis)) { |
| await forceUpdate(apiId); |
| await new Promise(resolve => setTimeout(resolve, 100)); |
| } |
| showToast('✅ All APIs updated!', 'success'); |
| } catch (error) { |
| showToast('❌ Update failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function forceUpdate(apiId) { |
| try { |
| await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, { method: 'POST' }); |
| addLog(`Forced update: ${apiId}`); |
| } catch (error) { |
| console.error(error); |
| } |
| } |
| |
| function addLog(text) { |
| const logContainer = document.getElementById('activityLog'); |
| const time = new Date().toLocaleTimeString(); |
| const entry = document.createElement('div'); |
| entry.style.cssText = 'padding: 10px; border-left: 3px solid var(--accent-blue); margin-bottom: 8px;'; |
| entry.innerHTML = `<span style="opacity: 0.6;">${time}</span> ${text}`; |
| logContainer.insertBefore(entry, logContainer.firstChild); |
| while (logContainer.children.length > 50) { |
| logContainer.removeChild(logContainer.lastChild); |
| } |
| } |
| |
| |
| async function loadAdminData() { |
| try { |
| const [status, providers] = await Promise.all([ |
| fetch('/api/status').then(r => r.json()), |
| fetch('/api/providers').then(r => r.json()) |
| ]); |
| |
| document.getElementById('statTotal').textContent = status.total_providers; |
| document.getElementById('statOnline').textContent = status.online; |
| document.getElementById('statOffline').textContent = status.offline; |
| |
| const list = document.getElementById('apisList'); |
| list.innerHTML = providers.map(api => ` |
| <div style="background: rgba(17, 24, 39, 0.6); padding: 15px; border-radius: 12px; margin-bottom: 10px; display: flex; justify-content: space-between; align-items: center;"> |
| <div> |
| <div style="font-weight: 600;">${api.name}</div> |
| <div style="font-size: 12px; color: var(--text-secondary);">${api.category}</div> |
| </div> |
| <span class="badge ${api.status === 'online' ? 'badge-success' : 'badge-danger'}">${api.status.toUpperCase()}</span> |
| </div> |
| `).join(''); |
| } catch (error) { |
| console.error('Error loading admin data:', error); |
| } |
| } |
| |
| async function addNewAPI() { |
| const name = document.getElementById('newApiName').value; |
| const url = document.getElementById('newApiUrl').value; |
| const category = document.getElementById('newApiCategory').value; |
| |
| if (!name || !url) { |
| showToast('Please fill in API name and URL', 'info'); |
| return; |
| } |
| |
| showToast('✅ API added! Note: Restart server to activate.', 'success'); |
| document.getElementById('newApiName').value = ''; |
| document.getElementById('newApiUrl').value = ''; |
| loadAdminData(); |
| } |
| |
| function saveSettings() { |
| const interval = document.getElementById('checkInterval').value; |
| const refresh = document.getElementById('dashboardRefresh').value; |
| localStorage.setItem('monitorSettings', JSON.stringify({ interval, refresh })); |
| showToast('✅ Settings saved!', 'success'); |
| } |
| |
| |
| async function loadHFHealth() { |
| try { |
| const data = await fetch('/api/hf/health').then(r => r.json()); |
| document.getElementById('healthOutput').textContent = JSON.stringify(data, null, 2); |
| } catch (err) { |
| document.getElementById('healthOutput').textContent = `Error: ${err.message}`; |
| } |
| } |
| |
| async function loadModels() { |
| try { |
| document.getElementById('modelsList').innerHTML = '<p style="color: var(--text-secondary);">Loading...</p>'; |
| |
| document.getElementById('modelsList').innerHTML = '<p style="color: var(--text-secondary);">Models registry endpoint not implemented</p>'; |
| } catch (err) { |
| document.getElementById('modelsList').innerHTML = `<p style="color: var(--accent-red);">Error: ${err.message}</p>`; |
| } |
| } |
| |
| async function loadDatasets() { |
| try { |
| document.getElementById('datasetsList').innerHTML = '<p style="color: var(--text-secondary);">Loading...</p>'; |
| |
| document.getElementById('datasetsList').innerHTML = '<p style="color: var(--text-secondary);">Datasets registry endpoint not implemented</p>'; |
| } catch (err) { |
| document.getElementById('datasetsList').innerHTML = `<p style="color: var(--accent-red);">Error: ${err.message}</p>`; |
| } |
| } |
| |
| async function doSearch() { |
| const q = document.getElementById('searchQuery').value; |
| document.getElementById('searchResults').innerHTML = `<p style="color: var(--text-secondary);">Searching for "${q}"...</p>`; |
| |
| document.getElementById('searchResults').innerHTML = '<p style="color: var(--text-secondary);">Search endpoint not implemented</p>'; |
| } |
| |
| async function doSearchDatasets() { |
| const q = document.getElementById('searchQuery').value; |
| document.getElementById('searchResults').innerHTML = `<p style="color: var(--text-secondary);">Searching datasets for "${q}"...</p>`; |
| |
| document.getElementById('searchResults').innerHTML = '<p style="color: var(--text-secondary);">Search endpoint not implemented</p>'; |
| } |
| |
| async function doSentiment() { |
| const texts = document.getElementById('sentimentTexts').value.split('\n').filter(t => t.trim()); |
| if (texts.length === 0) { |
| showToast('Please enter at least one text sample', 'info'); |
| return; |
| } |
| try { |
| document.getElementById('voteDisplay').innerHTML = '<span>⏳ Analyzing...</span>'; |
| document.getElementById('sentimentOutput').textContent = 'Running sentiment analysis...'; |
| |
| const data = await fetch('/api/hf/run-sentiment', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ texts }) |
| }).then(r => r.json()); |
| |
| const vote = data.vote || 0; |
| let voteClass = 'vote-neutral'; |
| let voteEmoji = '😐'; |
| if (vote > 0.2) { voteClass = 'vote-positive'; voteEmoji = '📈'; } |
| else if (vote < -0.2) { voteClass = 'vote-negative'; voteEmoji = '📉'; } |
| |
| document.getElementById('voteDisplay').innerHTML = `<span style="color: ${voteClass === 'vote-positive' ? 'var(--accent-green)' : voteClass === 'vote-negative' ? 'var(--accent-red)' : 'var(--text-secondary)'};">${voteEmoji} ${vote.toFixed(3)}</span>`; |
| document.getElementById('sentimentOutput').textContent = JSON.stringify(data, null, 2); |
| } catch (err) { |
| document.getElementById('voteDisplay').innerHTML = '<span style="color: var(--accent-red);">Error</span>'; |
| document.getElementById('sentimentOutput').textContent = `Error: ${err.message}`; |
| } |
| } |
| |
| |
| let currentPoolId = null; |
| let allProvidersList = []; |
| |
| async function loadPools() { |
| try { |
| const [poolsRes, historyRes] = await Promise.all([ |
| fetch('/api/pools').then(r => r.json()), |
| fetch('/api/pools/history?limit=20').then(r => r.json()) |
| ]); |
| |
| const pools = poolsRes.pools || []; |
| const container = document.getElementById('poolsContainer'); |
| |
| if (pools.length === 0) { |
| container.innerHTML = ` |
| <div style="grid-column: 1/-1; text-align: center; padding: 40px; background: rgba(17, 24, 39, 0.6); border-radius: 20px; border: 2px dashed var(--border);"> |
| <div style="font-size: 48px; margin-bottom: 15px;">🔄</div> |
| <div style="font-size: 18px; font-weight: 600; margin-bottom: 10px; color: var(--text-primary);">No pools configured</div> |
| <div style="color: var(--text-secondary); margin-bottom: 20px;">Create your first pool to get started with API source rotation</div> |
| <button class="refresh-btn" onclick="showCreatePoolModal()">➕ Create Pool</button> |
| </div> |
| `; |
| } else { |
| container.innerHTML = pools.map(pool => createPoolCard(pool)).join(''); |
| } |
| |
| |
| const history = historyRes.history || []; |
| const historyContainer = document.getElementById('rotationHistory'); |
| if (history.length === 0) { |
| historyContainer.innerHTML = '<p style="color: var(--text-secondary); text-align: center; padding: 20px;">No rotation history yet</p>'; |
| } else { |
| historyContainer.innerHTML = history.map(h => ` |
| <div style="padding: 15px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; margin-bottom: 10px; border-left: 3px solid var(--accent-blue);"> |
| <div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px;"> |
| <div> |
| <div style="font-weight: 600; margin-bottom: 5px;">${h.pool_name}</div> |
| <div style="font-size: 12px; color: var(--text-secondary);">Rotated to: <strong>${h.provider_name}</strong></div> |
| </div> |
| <div style="text-align: right;"> |
| <div style="font-size: 12px; color: var(--text-secondary);">${new Date(h.timestamp).toLocaleString()}</div> |
| <span class="badge badge-info" style="margin-top: 5px; display: inline-block;">${h.reason}</span> |
| </div> |
| </div> |
| </div> |
| `).join(''); |
| } |
| } catch (error) { |
| console.error('Error loading pools:', error); |
| document.getElementById('poolsContainer').innerHTML = ` |
| <div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--accent-red);"> |
| <div>❌ Error loading pools: ${error.message}</div> |
| </div> |
| `; |
| } |
| } |
| |
| function createPoolCard(pool) { |
| const currentProvider = pool.current_provider |
| ? `<div style="margin-bottom: 15px; padding: 12px; background: rgba(16, 185, 129, 0.1); border-radius: 10px; border: 1px solid rgba(16, 185, 129, 0.3);"> |
| <div style="display: flex; align-items: center; gap: 8px;"> |
| <span style="width: 10px; height: 10px; background: var(--accent-green); border-radius: 50%; display: inline-block;"></span> |
| <span style="font-weight: 600;">Current: ${pool.current_provider.name}</span> |
| </div> |
| </div>` |
| : '<div style="margin-bottom: 15px; padding: 12px; background: rgba(239, 68, 68, 0.1); border-radius: 10px; border: 1px solid rgba(239, 68, 68, 0.3); color: var(--text-secondary);">No active provider</div>'; |
| |
| const membersHTML = pool.members && pool.members.length > 0 |
| ? pool.members.map(member => { |
| const successRate = member.success_rate || 0; |
| const statusClass = successRate >= 90 ? 'badge-success' : successRate >= 70 ? 'badge-warning' : 'badge-danger'; |
| const rateLimit = member.rate_limit || { usage: 0, limit: 100, percentage: 0 }; |
| |
| return ` |
| <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px; margin-bottom: 8px;"> |
| <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px;"> |
| <div style="font-weight: 600;">${member.provider_name}</div> |
| <span class="badge ${statusClass}">${successRate.toFixed(1)}%</span> |
| </div> |
| <div style="display: flex; gap: 15px; font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;"> |
| <span>Used: ${member.use_count || 0}</span> |
| <span>Priority: ${member.priority || 1}</span> |
| <span>Weight: ${member.weight || 1}</span> |
| </div> |
| <div> |
| <div style="font-size: 11px; color: var(--text-secondary); margin-bottom: 4px;"> |
| Rate Limit: ${rateLimit.usage}/${rateLimit.limit} (${rateLimit.percentage}%) |
| </div> |
| <div style="height: 6px; background: rgba(255, 255, 255, 0.1); border-radius: 3px; overflow: hidden;"> |
| <div style="height: 100%; background: ${rateLimit.percentage < 70 ? 'var(--accent-green)' : rateLimit.percentage < 90 ? 'var(--accent-yellow)' : 'var(--accent-red)'}; width: ${rateLimit.percentage}%; transition: width 0.3s;"></div> |
| </div> |
| </div> |
| </div> |
| `; |
| }).join('') |
| : '<div style="color: var(--text-secondary); font-size: 14px; padding: 20px; text-align: center;">No members in pool</div>'; |
| |
| return ` |
| <div class="pool-card-hover" style="background: rgba(17, 24, 39, 0.6); backdrop-filter: blur(10px); border: 1px solid var(--border); border-radius: 20px; padding: 25px;"> |
| <div style="display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px;"> |
| <div> |
| <div style="font-size: 20px; font-weight: 700; margin-bottom: 8px;">${pool.pool_name}</div> |
| <span class="badge badge-info">${pool.category}</span> |
| </div> |
| <div style="display: flex; gap: 8px;"> |
| <button onclick="addMemberToPool(${pool.pool_id})" style="padding: 8px 12px; background: rgba(59, 130, 246, 0.2); border: 1px solid var(--accent-blue); border-radius: 8px; color: var(--accent-blue); cursor: pointer; font-size: 12px; font-weight: 600;">➕</button> |
| <button onclick="rotatePool(${pool.pool_id})" style="padding: 8px 12px; background: rgba(16, 185, 129, 0.2); border: 1px solid var(--accent-green); border-radius: 8px; color: var(--accent-green); cursor: pointer; font-size: 12px; font-weight: 600;">🔄</button> |
| <button onclick="deletePool(${pool.pool_id}, '${pool.pool_name}')" style="padding: 8px 12px; background: rgba(239, 68, 68, 0.2); border: 1px solid var(--accent-red); border-radius: 8px; color: var(--accent-red); cursor: pointer; font-size: 12px; font-weight: 600;">🗑️</button> |
| </div> |
| </div> |
| |
| ${currentProvider} |
| |
| <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; margin: 20px 0;"> |
| <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;"> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Strategy</div> |
| <div style="font-weight: 600; font-size: 14px;">${pool.rotation_strategy.replace('_', ' ')}</div> |
| </div> |
| <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;"> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Rotations</div> |
| <div style="font-weight: 600; font-size: 14px;">${pool.total_rotations || 0}</div> |
| </div> |
| <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;"> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Members</div> |
| <div style="font-weight: 600; font-size: 14px;">${pool.members ? pool.members.length : 0}</div> |
| </div> |
| <div style="background: rgba(255, 255, 255, 0.05); padding: 12px; border-radius: 10px;"> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">Status</div> |
| <span class="badge ${pool.enabled ? 'badge-success' : 'badge-danger'}">${pool.enabled ? 'Enabled' : 'Disabled'}</span> |
| </div> |
| </div> |
| |
| <div style="margin-top: 20px;"> |
| <div style="font-weight: 600; margin-bottom: 12px; font-size: 14px;">Pool Members</div> |
| <div style="max-height: 300px; overflow-y: auto;"> |
| ${membersHTML} |
| </div> |
| </div> |
| </div> |
| `; |
| } |
| |
| async function loadProvidersForPool() { |
| try { |
| const providers = await fetch('/api/providers').then(r => r.json()); |
| allProvidersList = providers; |
| const select = document.getElementById('memberProvider'); |
| select.innerHTML = '<option value="">Select a provider...</option>' + providers.map(p => { |
| const providerId = p.name.toLowerCase().replace(/\s+/g, '_'); |
| return `<option value="${providerId}">${p.name} (${p.category})</option>`; |
| }).join(''); |
| } catch (error) { |
| console.error('Error loading providers:', error); |
| } |
| } |
| |
| function showCreatePoolModal() { |
| document.getElementById('createPoolModal').classList.add('active'); |
| } |
| |
| function closeCreatePoolModal() { |
| document.getElementById('createPoolModal').classList.remove('active'); |
| document.getElementById('createPoolForm').reset(); |
| } |
| |
| function addMemberToPool(poolId) { |
| currentPoolId = poolId; |
| loadProvidersForPool(); |
| document.getElementById('addMemberModal').classList.add('active'); |
| } |
| |
| function closeAddMemberModal() { |
| document.getElementById('addMemberModal').classList.remove('active'); |
| document.getElementById('addMemberForm').reset(); |
| currentPoolId = null; |
| } |
| |
| document.getElementById('createPoolForm').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const data = { |
| name: document.getElementById('poolName').value, |
| category: document.getElementById('poolCategory').value, |
| rotation_strategy: document.getElementById('rotationStrategy').value, |
| description: document.getElementById('poolDescription').value |
| }; |
| |
| try { |
| const response = await fetch('/api/pools', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(data) |
| }); |
| |
| if (response.ok) { |
| showToast('✅ Pool created successfully!', 'success'); |
| closeCreatePoolModal(); |
| loadPools(); |
| } else { |
| const error = await response.json(); |
| showToast('❌ Error: ' + (error.detail || 'Failed to create pool'), 'error'); |
| } |
| } catch (error) { |
| showToast('❌ Error: ' + error.message, 'error'); |
| console.error(error); |
| } |
| }); |
| |
| document.getElementById('addMemberForm').addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const data = { |
| provider_id: document.getElementById('memberProvider').value, |
| priority: parseInt(document.getElementById('memberPriority').value), |
| weight: parseInt(document.getElementById('memberWeight').value) |
| }; |
| |
| try { |
| const response = await fetch(`/api/pools/${currentPoolId}/members`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(data) |
| }); |
| |
| if (response.ok) { |
| showToast('✅ Member added successfully!', 'success'); |
| closeAddMemberModal(); |
| loadPools(); |
| } else { |
| const error = await response.json(); |
| showToast('❌ Error: ' + (error.detail || 'Failed to add member'), 'error'); |
| } |
| } catch (error) { |
| showToast('❌ Error: ' + error.message, 'error'); |
| console.error(error); |
| } |
| }); |
| |
| async function rotatePool(poolId) { |
| try { |
| const response = await fetch(`/api/pools/${poolId}/rotate`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ reason: 'manual' }) |
| }); |
| |
| if (response.ok) { |
| const result = await response.json(); |
| showToast(`✅ Rotated to ${result.provider_name}`, 'success'); |
| loadPools(); |
| } else { |
| const error = await response.json(); |
| showToast('❌ Error: ' + (error.detail || 'Failed to rotate'), 'error'); |
| } |
| } catch (error) { |
| showToast('❌ Error: ' + error.message, 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function deletePool(poolId, poolName) { |
| if (!confirm(`Are you sure you want to delete pool "${poolName}"?`)) { |
| return; |
| } |
| |
| try { |
| const response = await fetch(`/api/pools/${poolId}`, { |
| method: 'DELETE' |
| }); |
| |
| if (response.ok) { |
| showToast('✅ Pool deleted successfully!', 'success'); |
| loadPools(); |
| } else { |
| const error = await response.json(); |
| showToast('❌ Error: ' + (error.detail || 'Failed to delete pool'), 'error'); |
| } |
| } catch (error) { |
| alert('❌ Error: ' + error.message); |
| console.error(error); |
| } |
| } |
| |
| |
| |
| 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 = ` |
| <div class="toast-icon">${icons[type] || icons.info}</div> |
| <div class="toast-content"> |
| <div class="toast-title">${title || titles[type] || titles.info}</div> |
| <div class="toast-message">${message}</div> |
| </div> |
| <button class="toast-close" onclick="this.parentElement.remove()">×</button> |
| `; |
| |
| toastContainer.appendChild(toast); |
| |
| |
| setTimeout(() => { |
| toast.style.animation = 'toastSlideIn 0.3s reverse'; |
| setTimeout(() => toast.remove(), 300); |
| }, 5000); |
| |
| |
| 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); |
| } |
| }); |
| } |
| |
| |
| 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%'; |
| } |
| } |
| |
| |
| 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'); |
| } |
| } |
| |
| |
| 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'); |
| |
| |
| setTimeout(() => { |
| hideFeedback(); |
| }, 3000); |
| } |
| } |
| |
| function hideFeedback() { |
| const overlay = document.getElementById('feedbackOverlay'); |
| if (overlay) { |
| overlay.classList.remove('show'); |
| } |
| } |
| |
| |
| function scrollToTop() { |
| window.scrollTo({ |
| top: 0, |
| behavior: 'smooth' |
| }); |
| } |
| |
| |
| 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; |
| } |
| }); |
| |
| |
| 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; |
| |
| |
| const existingNoResults = tbody.querySelector('tr[data-no-results]'); |
| if (existingNoResults) { |
| existingNoResults.remove(); |
| } |
| |
| rows.forEach((row, index) => { |
| if (row.querySelector('td[colspan]')) { |
| return; |
| } |
| |
| 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') { |
| |
| 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'; |
| } |
| }); |
| |
| |
| if (visibleCount === 0 && rows.length > 0 && !searchTerm && currentFilter === 'all') { |
| |
| return; |
| } |
| |
| if (visibleCount === 0) { |
| const noResultsRow = document.createElement('tr'); |
| noResultsRow.setAttribute('data-no-results', 'true'); |
| noResultsRow.innerHTML = `<td colspan="6" style="text-align: center; padding: 40px; color: var(--text-secondary);"> |
| <div style="font-size: 48px; margin-bottom: 10px;">🔍</div> |
| <div style="font-weight: 600; margin-bottom: 5px;">نتیجهای یافت نشد</div> |
| <div style="font-size: 14px;">لطفاً عبارت جستجوی دیگری را امتحان کنید</div> |
| </td>`; |
| tbody.appendChild(noResultsRow); |
| } |
| } |
| |
| function filterByCategory(category) { |
| currentFilter = category; |
| |
| |
| document.querySelectorAll('.filter-chip').forEach(chip => { |
| chip.classList.remove('active'); |
| }); |
| if (event && event.target) { |
| event.target.classList.add('active'); |
| } |
| |
| filterMarketTable(); |
| } |
| |
| |
| 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); |
| |
| |
| 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); |
| } |
| |
| |
| document.addEventListener('click', (e) => { |
| if (e.target.classList.contains('modal')) { |
| e.target.style.display = 'none'; |
| } |
| }); |
| |
| |
| |
| async function loadLogs() { |
| try { |
| const level = document.getElementById('logLevelFilter')?.value || ''; |
| const category = document.getElementById('logCategoryFilter')?.value || ''; |
| const search = document.getElementById('logSearch')?.value || ''; |
| const limit = parseInt(document.getElementById('logLimit')?.value || '100'); |
| |
| let url = `/api/logs?limit=${limit}`; |
| if (level) url += `&level=${level}`; |
| if (category) url += `&category=${category}`; |
| if (search) url += `&search=${search}`; |
| |
| const response = await fetch(url); |
| const data = await response.json(); |
| |
| |
| const statsResponse = await fetch('/api/logs/stats'); |
| const stats = await statsResponse.json(); |
| |
| if (document.getElementById('totalLogs')) { |
| document.getElementById('totalLogs').textContent = stats.total || 0; |
| document.getElementById('errorLogs').textContent = stats.errors || 0; |
| document.getElementById('infoLogs').textContent = stats.by_level?.info || 0; |
| document.getElementById('warningLogs').textContent = stats.by_level?.warning || 0; |
| } |
| |
| |
| const tbody = document.getElementById('logsTableBody'); |
| if (data.logs && data.logs.length > 0) { |
| tbody.innerHTML = data.logs.map(log => { |
| const levelColor = { |
| 'error': 'var(--accent-red)', |
| 'critical': 'var(--accent-red)', |
| 'warning': 'var(--accent-yellow)', |
| 'info': 'var(--accent-blue)', |
| 'debug': 'var(--text-secondary)' |
| }[log.level] || 'var(--text-secondary)'; |
| |
| return ` |
| <tr> |
| <td style="font-size: 12px; color: var(--text-secondary);">${new Date(log.timestamp).toLocaleString()}</td> |
| <td><span style="color: ${levelColor}; font-weight: 700;">${log.level.toUpperCase()}</span></td> |
| <td><span class="badge badge-info">${log.category}</span></td> |
| <td>${log.message}</td> |
| <td>${log.provider_id || '—'}</td> |
| <td>${log.response_time ? log.response_time.toFixed(0) + 'ms' : '—'}</td> |
| </tr> |
| `; |
| }).join(''); |
| } else { |
| tbody.innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--text-secondary);">No logs found</td></tr>'; |
| } |
| } catch (error) { |
| console.error('Error loading logs:', error); |
| if (document.getElementById('logsTableBody')) { |
| document.getElementById('logsTableBody').innerHTML = '<tr><td colspan="6" style="text-align: center; color: var(--accent-red);">Error loading logs</td></tr>'; |
| } |
| } |
| } |
| |
| async function exportLogsJSON() { |
| try { |
| const level = document.getElementById('logLevelFilter')?.value || ''; |
| const category = document.getElementById('logCategoryFilter')?.value || ''; |
| |
| let url = '/api/logs/export/json?'; |
| if (level) url += `level=${level}&`; |
| if (category) url += `category=${category}&`; |
| |
| const response = await fetch(url); |
| const data = await response.json(); |
| showToast(`✅ Logs exported to ${data.filepath}`, 'success'); |
| } catch (error) { |
| showToast('❌ Export failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function exportLogsCSV() { |
| try { |
| const level = document.getElementById('logLevelFilter')?.value || ''; |
| const category = document.getElementById('logCategoryFilter')?.value || ''; |
| |
| let url = '/api/logs/export/csv?'; |
| if (level) url += `level=${level}&`; |
| if (category) url += `category=${category}&`; |
| |
| const response = await fetch(url); |
| const data = await response.json(); |
| showToast(`✅ Logs exported to ${data.filepath}`, 'success'); |
| } catch (error) { |
| showToast('❌ Export failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function clearAllLogs() { |
| if (!confirm('Are you sure you want to clear all logs? This cannot be undone.')) { |
| return; |
| } |
| try { |
| const response = await fetch('/api/logs', { method: 'DELETE' }); |
| if (response.ok) { |
| showToast('✅ All logs cleared', 'success'); |
| loadLogs(); |
| } else { |
| showToast('❌ Failed to clear logs', 'error'); |
| } |
| } catch (error) { |
| showToast('❌ Error clearing logs', 'error'); |
| console.error(error); |
| } |
| } |
| |
| |
| |
| async function loadResources() { |
| try { |
| const category = document.getElementById('resourceCategoryFilter')?.value || ''; |
| |
| let url = '/api/resources'; |
| if (category) { |
| url = `/api/resources/category/${category}`; |
| } |
| |
| const response = await fetch(url); |
| const data = await response.json(); |
| |
| |
| const stats = category ? { count: data.count } : data.statistics; |
| if (stats && document.getElementById('totalResources')) { |
| document.getElementById('totalResources').textContent = stats.total_providers || stats.count || 0; |
| document.getElementById('freeResources').textContent = stats.by_free?.free || 0; |
| document.getElementById('paidResources').textContent = stats.by_free?.paid || 0; |
| document.getElementById('authResources').textContent = stats.by_auth?.requires_auth || 0; |
| } |
| |
| |
| const grid = document.getElementById('resourcesGrid'); |
| const providers = category ? data.providers : Object.values(data.providers || {}); |
| |
| if (providers && providers.length > 0) { |
| grid.innerHTML = providers.map(provider => { |
| const authBadge = provider.requires_auth |
| ? '<span class="badge badge-warning">Auth Required</span>' |
| : '<span class="badge badge-success">No Auth</span>'; |
| |
| const freeBadge = provider.free !== false |
| ? '<span class="badge badge-success">Free</span>' |
| : '<span class="badge badge-danger">Paid</span>'; |
| |
| return ` |
| <div class="stat-card pool-card-hover"> |
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 15px;"> |
| <div> |
| <div style="font-size: 18px; font-weight: 700; margin-bottom: 8px;">${provider.name}</div> |
| <span class="badge badge-info">${provider.category}</span> |
| </div> |
| <div style="display: flex; gap: 5px; flex-direction: column;"> |
| ${authBadge} |
| ${freeBadge} |
| </div> |
| </div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 10px; word-break: break-all;"> |
| ${provider.base_url} |
| </div> |
| <div style="display: flex; justify-content: space-between; font-size: 12px; color: var(--text-secondary);"> |
| <span>Priority: ${provider.priority || 5}</span> |
| <span>Weight: ${provider.weight || 50}</span> |
| </div> |
| ${provider.docs_url ? `<div style="margin-top: 10px;"><a href="${provider.docs_url}" target="_blank" style="color: var(--accent-blue); font-size: 12px;">📖 Docs</a></div>` : ''} |
| </div> |
| `; |
| }).join(''); |
| } else { |
| grid.innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--text-secondary);">No resources found</div>'; |
| } |
| } catch (error) { |
| console.error('Error loading resources:', error); |
| if (document.getElementById('resourcesGrid')) { |
| document.getElementById('resourcesGrid').innerHTML = '<div style="grid-column: 1/-1; text-align: center; padding: 40px; color: var(--accent-red);">Error loading resources</div>'; |
| } |
| } |
| } |
| |
| async function exportResourcesJSON() { |
| try { |
| const response = await fetch('/api/resources/export/json'); |
| const data = await response.json(); |
| showToast(`✅ Resources exported to ${data.filepath}`, 'success'); |
| } catch (error) { |
| showToast('❌ Export failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function exportResourcesCSV() { |
| try { |
| const response = await fetch('/api/resources/export/csv'); |
| const data = await response.json(); |
| showToast(`✅ Resources exported to ${data.filepath}`, 'success'); |
| } catch (error) { |
| showToast('❌ Export failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| async function backupResources() { |
| try { |
| const response = await fetch('/api/resources/backup', { method: 'POST' }); |
| const data = await response.json(); |
| showToast(`✅ Backup created: ${data.filepath}`, 'success'); |
| } catch (error) { |
| showToast('❌ Backup failed', 'error'); |
| console.error(error); |
| } |
| } |
| |
| function showImportModal() { |
| document.getElementById('importModal').classList.add('active'); |
| } |
| |
| function closeImportModal() { |
| document.getElementById('importModal').classList.remove('active'); |
| const form = document.getElementById('importForm'); |
| if (form) form.reset(); |
| } |
| |
| |
| const importForm = document.getElementById('importForm'); |
| if (importForm) { |
| importForm.addEventListener('submit', async (e) => { |
| e.preventDefault(); |
| const filePath = document.getElementById('importFilePath').value; |
| const merge = document.getElementById('importMode').value === 'true'; |
| |
| try { |
| const response = await fetch(`/api/resources/import/json?file_path=${encodeURIComponent(filePath)}&merge=${merge}`, { |
| method: 'POST' |
| }); |
| |
| if (response.ok) { |
| const data = await response.json(); |
| showToast(`✅ Resources imported successfully (${data.merged ? 'merged' : 'replaced'})`, 'success'); |
| closeImportModal(); |
| loadResources(); |
| } else { |
| const error = await response.json(); |
| showToast(`❌ Import failed: ${error.detail}`, 'error'); |
| } |
| } catch (error) { |
| showToast('❌ Error importing resources', 'error'); |
| console.error(error); |
| } |
| }); |
| } |
| |
| |
| |
| async function loadReports() { |
| await Promise.all([ |
| loadDiscoveryReport(), |
| loadModelsReport(), |
| loadLastDiagnostics() |
| ]); |
| } |
| |
| |
| 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 += ` |
| <div class="stat-card" style="margin-bottom: 15px; border-left: 4px solid ${config.border}; background: ${config.bg}; animation-delay: ${index * 0.1}s;"> |
| <div style="display: flex; align-items: start; gap: 12px;"> |
| <div class="icon icon-md status-icon-${issue.severity}" style="flex-shrink: 0; margin-top: 2px;"> |
| <svg><use href="#${config.icon}"></use></svg> |
| </div> |
| <div style="flex: 1;"> |
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;"> |
| <div> |
| <h3 style="font-size: 16px; font-weight: 700; margin-bottom: 4px; color: ${config.color};"> |
| ${issue.title} |
| </h3> |
| <div style="font-size: 13px; color: var(--text-secondary); margin-bottom: 8px;"> |
| ${issue.description} |
| </div> |
| </div> |
| <span class="badge badge-${issue.severity === 'critical' ? 'danger' : issue.severity === 'warning' ? 'warning' : 'info'}" style="flex-shrink: 0;"> |
| ${issue.category} |
| </span> |
| </div> |
| |
| ${solution ? ` |
| <div style="margin-top: 12px; padding: 12px; background: rgba(17, 24, 39, 0.6); border-radius: 8px; border: 1px solid var(--border);"> |
| <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;"> |
| <span class="icon icon-sm status-icon-info"><svg><use href="#icon-info"></use></svg></span> |
| <strong style="font-size: 14px;">راهحل:</strong> |
| </div> |
| <div style="font-size: 12px; color: var(--text-secondary); font-family: 'Courier New', monospace; line-height: 1.8;"> |
| ${solution.steps.map(step => `<div style="margin-bottom: 4px;">${step}</div>`).join('')} |
| </div> |
| </div> |
| ` : ''} |
| |
| <div style="margin-top: 8px; font-size: 11px; color: var(--text-secondary);"> |
| ${new Date(issue.timestamp).toLocaleString('fa-IR')} |
| </div> |
| </div> |
| </div> |
| </div> |
| `; |
| }); |
| |
| container.innerHTML = html || '<div style="text-align: center; padding: 40px; color: var(--text-secondary);">هیچ هشداری یافت نشد</div>'; |
| } |
| |
| async function runDiagnostics(autoFix = false) { |
| const resultsDiv = document.getElementById('diagnosticsResults'); |
| resultsDiv.innerHTML = '<div class="loading"><div class="spinner"></div></div>'; |
| |
| try { |
| const response = await fetch(`/api/diagnostics/run?auto_fix=${autoFix}`, { |
| method: 'POST' |
| }); |
| const report = await response.json(); |
| |
| displayDiagnosticsReport(report); |
| showToast(`اشکالیابی انجام شد (${report.total_issues} مشکل یافت شد)`, 'success', 'Diagnostics'); |
| |
| |
| if (report.issues && report.issues.length > 0) { |
| displaySystemAlerts(report.issues); |
| document.getElementById('systemAlertsSection').style.display = 'block'; |
| } |
| } catch (error) { |
| resultsDiv.innerHTML = `<div class="alert alert-error"> |
| <span class="icon icon-md status-icon-error"><svg><use href="#icon-error"></use></svg></span> |
| خطا: ${error.message} |
| </div>`; |
| showToast('خطا در اجرای اشکالیابی', 'error', 'Error'); |
| } |
| } |
| |
| function displayDiagnosticsReport(report) { |
| const resultsDiv = document.getElementById('diagnosticsResults'); |
| |
| const severityColors = { |
| 'critical': 'var(--accent-red)', |
| 'warning': 'var(--accent-yellow)', |
| 'info': 'var(--accent-blue)' |
| }; |
| |
| const severityIcons = { |
| 'critical': '🔴', |
| 'warning': '⚠️', |
| 'info': 'ℹ️' |
| }; |
| |
| let html = ` |
| <div class="stats-grid" style="margin-bottom: 20px;"> |
| <div class="stat-card"> |
| <div class="stat-value" style="color: ${severityColors.critical};">${report.critical_issues}</div> |
| <div class="stat-label">مشکلات بحرانی</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" style="color: ${severityColors.warning};">${report.warnings}</div> |
| <div class="stat-label">هشدارها</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" style="color: ${severityColors.info};">${report.info_issues}</div> |
| <div class="stat-label">اطلاعات</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value">${report.fixed_issues.length}</div> |
| <div class="stat-label">تعمیر شده</div> |
| </div> |
| </div> |
| `; |
| |
| if (report.fixed_issues && report.fixed_issues.length > 0) { |
| html += ` |
| <div style="margin-bottom: 20px; padding: 15px; background: rgba(16, 185, 129, 0.1); border-radius: 12px; border-left: 4px solid var(--accent-green);"> |
| <h3 style="margin-bottom: 10px; color: var(--accent-green);">✅ مشکلات تعمیر شده</h3> |
| ${report.fixed_issues.map(issue => ` |
| <div style="padding: 10px; margin-bottom: 8px; background: rgba(255, 255, 255, 0.05); border-radius: 8px;"> |
| <strong>${issue.title}</strong><br> |
| <span style="font-size: 12px; color: var(--text-secondary);">${issue.description}</span> |
| </div> |
| `).join('')} |
| </div> |
| `; |
| } |
| |
| if (report.issues && report.issues.length > 0) { |
| html += ` |
| <div> |
| <h3 style="margin-bottom: 15px;">📋 لیست مشکلات</h3> |
| ${report.issues.map(issue => ` |
| <div style="padding: 15px; margin-bottom: 10px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; border-left: 4px solid ${severityColors[issue.severity]};"> |
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 8px;"> |
| <div> |
| <span style="font-size: 20px; margin-left: 8px;">${severityIcons[issue.severity]}</span> |
| <strong>${issue.title}</strong> |
| </div> |
| <span class="badge badge-${issue.severity === 'critical' ? 'danger' : issue.severity === 'warning' ? 'warning' : 'info'}">${issue.category}</span> |
| </div> |
| <div style="color: var(--text-secondary); margin-bottom: 8px; font-size: 14px;"> |
| ${issue.description} |
| </div> |
| ${issue.fixable && issue.fix_action ? ` |
| <div style="margin-top: 8px; padding: 8px; background: rgba(59, 130, 246, 0.1); border-radius: 6px; font-family: monospace; font-size: 12px;"> |
| 🔧 ${issue.fix_action} |
| </div> |
| ` : ''} |
| <div style="margin-top: 8px; font-size: 11px; color: var(--text-secondary);"> |
| ${new Date(issue.timestamp).toLocaleString('fa-IR')} |
| </div> |
| </div> |
| `).join('')} |
| </div> |
| `; |
| } else { |
| html += ` |
| <div style="text-align: center; padding: 40px; background: rgba(16, 185, 129, 0.1); border-radius: 12px;"> |
| <div style="font-size: 48px; margin-bottom: 15px;">✅</div> |
| <div style="font-size: 18px; font-weight: 600; color: var(--accent-green);">هیچ مشکلی یافت نشد!</div> |
| <div style="color: var(--text-secondary); margin-top: 10px;">سیستم شما در وضعیت مطلوب است.</div> |
| </div> |
| `; |
| } |
| |
| html += ` |
| <div style="margin-top: 20px; padding: 15px; background: rgba(17, 24, 39, 0.6); border-radius: 12px;"> |
| <div style="font-size: 12px; color: var(--text-secondary);"> |
| <strong>زمان اجرا:</strong> ${(report.duration_ms / 1000).toFixed(2)} ثانیه<br> |
| <strong>تاریخ:</strong> ${new Date(report.timestamp).toLocaleString('fa-IR')} |
| </div> |
| </div> |
| `; |
| |
| resultsDiv.innerHTML = html; |
| } |
| |
| async function loadLastDiagnostics() { |
| try { |
| const response = await fetch('/api/diagnostics/last'); |
| const report = await response.json(); |
| |
| if (report.message) { |
| return; |
| } |
| |
| displayDiagnosticsReport(report); |
| |
| |
| 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); |
| } |
| } |
| |
| async function loadDiscoveryReport() { |
| const reportDiv = document.getElementById('discoveryReport'); |
| reportDiv.innerHTML = '<div class="loading"><div class="spinner"></div></div>'; |
| |
| try { |
| const response = await fetch('/api/reports/discovery'); |
| const report = await response.json(); |
| |
| const lastRun = report.last_run; |
| const status = report.service_status; |
| |
| let html = ` |
| <div class="stats-grid" style="margin-bottom: 20px;"> |
| <div class="stat-card"> |
| <div class="stat-value" style="color: ${report.enabled ? 'var(--accent-green)' : 'var(--accent-red)'};"> |
| ${report.enabled ? '✅' : '❌'} |
| </div> |
| <div class="stat-label">وضعیت سرویس</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value">${report.model || 'N/A'}</div> |
| <div class="stat-label">مدل استفاده شده</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value">${Math.floor(report.interval_seconds / 3600)}h</div> |
| <div class="stat-label">فاصله اجرا</div> |
| </div> |
| ${report.next_run_estimate ? ` |
| <div class="stat-card"> |
| <div class="stat-value" style="color: var(--accent-purple);">⏰</div> |
| <div class="stat-label">اجرای بعدی</div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-top: 5px;"> |
| ${new Date(report.next_run_estimate).toLocaleString('fa-IR')} |
| </div> |
| </div> |
| ` : ''} |
| </div> |
| `; |
| |
| if (lastRun) { |
| html += ` |
| <div style="background: rgba(17, 24, 39, 0.6); padding: 20px; border-radius: 12px;"> |
| <h3 style="margin-bottom: 15px;">📊 آخرین اجرا</h3> |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;"> |
| <div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">شروع</div> |
| <div style="font-weight: 600;">${new Date(lastRun.started_at).toLocaleString('fa-IR')}</div> |
| </div> |
| <div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">پایان</div> |
| <div style="font-weight: 600;">${new Date(lastRun.finished_at).toLocaleString('fa-IR')}</div> |
| </div> |
| ${report.next_run_estimate ? ` |
| <div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">⏰ اجرای بعدی</div> |
| <div style="font-weight: 600; color: var(--accent-purple);">${new Date(report.next_run_estimate).toLocaleString('fa-IR')}</div> |
| </div> |
| ` : ''} |
| <div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">کاندیداها</div> |
| <div style="font-weight: 600; color: var(--accent-blue);">${lastRun.candidates_seen || 0}</div> |
| </div> |
| <div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">پیشنهاد شده</div> |
| <div style="font-weight: 600; color: var(--accent-yellow);">${lastRun.suggested || 0}</div> |
| </div> |
| <div> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 5px;">ذخیره شده</div> |
| <div style="font-weight: 600; color: var(--accent-green);">${lastRun.persisted || 0}</div> |
| </div> |
| </div> |
| ${lastRun.persisted_ids && lastRun.persisted_ids.length > 0 ? ` |
| <div style="margin-top: 15px; padding-top: 15px; border-top: 1px solid var(--border);"> |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 8px;">Providerهای اضافه شده:</div> |
| <div style="display: flex; flex-wrap: wrap; gap: 8px;"> |
| ${lastRun.persisted_ids.map(id => `<span class="badge badge-success">${id}</span>`).join('')} |
| </div> |
| </div> |
| ` : ''} |
| </div> |
| `; |
| } else { |
| html += ` |
| <div style="text-align: center; padding: 40px; color: var(--text-secondary);"> |
| هنوز اجرایی انجام نشده است |
| </div> |
| `; |
| } |
| |
| reportDiv.innerHTML = html; |
| } catch (error) { |
| reportDiv.innerHTML = `<div class="alert alert-error">❌ خطا: ${error.message}</div>`; |
| } |
| } |
| |
| async function loadModelsReport() { |
| const reportDiv = document.getElementById('modelsReport'); |
| reportDiv.innerHTML = '<div class="loading"><div class="spinner"></div></div>'; |
| |
| try { |
| const response = await fetch('/api/reports/models'); |
| const report = await response.json(); |
| |
| if (report.error) { |
| reportDiv.innerHTML = ` |
| <div class="alert alert-warning"> |
| ⚠️ ${report.error} |
| </div> |
| `; |
| return; |
| } |
| |
| let html = ` |
| <div class="stats-grid" style="margin-bottom: 20px;"> |
| <div class="stat-card"> |
| <div class="stat-value">${report.total_models}</div> |
| <div class="stat-label">کل مدلها</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" style="color: var(--accent-green);">${report.available}</div> |
| <div class="stat-label">در دسترس</div> |
| </div> |
| <div class="stat-card"> |
| <div class="stat-value" style="color: var(--accent-red);">${report.errors}</div> |
| <div class="stat-label">خطا</div> |
| </div> |
| </div> |
| `; |
| |
| if (report.models && report.models.length > 0) { |
| html += ` |
| <div style="display: grid; gap: 15px;"> |
| ${report.models.map(model => ` |
| <div style="padding: 20px; background: rgba(17, 24, 39, 0.6); border-radius: 12px; border-left: 4px solid ${model.status === 'available' ? 'var(--accent-green)' : 'var(--accent-red)'};"> |
| <div style="display: flex; justify-content: space-between; align-items: start; margin-bottom: 10px;"> |
| <div> |
| <h3 style="margin-bottom: 5px;">${model.model_id}</h3> |
| <span class="badge badge-${model.status === 'available' ? 'success' : 'danger'}">${model.status === 'available' ? '✅ در دسترس' : '❌ خطا'}</span> |
| </div> |
| </div> |
| ${model.status === 'available' ? ` |
| <div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-top: 15px;"> |
| ${model.downloads ? `<div><span style="font-size: 12px; color: var(--text-secondary);">دانلودها:</span> <strong>${model.downloads.toLocaleString()}</strong></div>` : ''} |
| ${model.likes ? `<div><span style="font-size: 12px; color: var(--text-secondary);">لایکها:</span> <strong>${model.likes}</strong></div>` : ''} |
| ${model.pipeline_tag ? `<div><span style="font-size: 12px; color: var(--text-secondary);">نوع:</span> <strong>${model.pipeline_tag}</strong></div>` : ''} |
| </div> |
| ` : ` |
| <div style="color: var(--accent-red); margin-top: 10px;"> |
| ${model.error || 'خطای نامشخص'} |
| </div> |
| `} |
| </div> |
| `).join('')} |
| </div> |
| `; |
| } |
| |
| reportDiv.innerHTML = html; |
| } catch (error) { |
| reportDiv.innerHTML = `<div class="alert alert-error">❌ خطا: ${error.message}</div>`; |
| } |
| } |
| </script> |
| </body> |
|
|
| </html> |