Spaces:
Paused
Paused
| <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> | |
| <script src="/static/js/trading-pairs-loader.js"></script> | |
| <script src="/static/js/app.js"></script> | |
| <link rel="stylesheet" href="/static/css/main.css"> | |
| <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 */ | |
| .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 */ | |
| .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 */ | |
| .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 Table */ | |
| .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); | |
| } | |
| /* Charts */ | |
| .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 */ | |
| .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); | |
| } | |
| } | |
| /* Forms */ | |
| .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; | |
| } | |
| /* Badges */ | |
| .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 */ | |
| .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); | |
| } | |
| /* Responsive */ | |
| @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 Styles */ | |
| .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); | |
| } | |
| } | |
| /* Improved Button Styles */ | |
| .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 Effect */ | |
| .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); | |
| } | |
| /* Improved Empty States */ | |
| .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; | |
| } | |
| /* Loading States */ | |
| .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; | |
| } | |
| } | |
| /* Scrollbar */ | |
| ::-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 Notifications */ | |
| .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; | |
| } | |
| } | |
| /* === Connection Status Indicator === */ | |
| .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; | |
| } | |
| /* === توسط Badge === */ | |
| .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; | |
| } | |
| } | |
| /* === Glassmorphism برای کارتها === */ | |
| .glass-card { | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(10px) saturate(180%); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| /* === Progress Bar انیمیت شده === */ | |
| .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%); | |
| } | |
| } | |
| /* === Modern UI Enhancements === */ | |
| /* Ripple Effect for Buttons */ | |
| .ripple { | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .ripple::after { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.5); | |
| transform: translate(-50%, -50%); | |
| transition: width 0.6s, height 0.6s; | |
| } | |
| .ripple:active::after { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| /* Enhanced Card Animations */ | |
| .stat-card, | |
| .market-section, | |
| .chart-container { | |
| animation: cardFadeIn 0.6s ease-out; | |
| animation-fill-mode: both; | |
| } | |
| .stat-card:nth-child(1) { animation-delay: 0.1s; } | |
| .stat-card:nth-child(2) { animation-delay: 0.2s; } | |
| .stat-card:nth-child(3) { animation-delay: 0.3s; } | |
| .stat-card:nth-child(4) { animation-delay: 0.4s; } | |
| .stat-card:nth-child(5) { animation-delay: 0.5s; } | |
| @keyframes cardFadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(30px) scale(0.95); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0) scale(1); | |
| } | |
| } | |
| /* Number Counter Animation */ | |
| .number-counter { | |
| display: inline-block; | |
| transition: all 0.3s ease; | |
| } | |
| .number-counter.updated { | |
| animation: numberPop 0.5s ease; | |
| } | |
| @keyframes numberPop { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.15); color: var(--accent-blue); } | |
| } | |
| /* Skeleton Loading */ | |
| .skeleton-loader { | |
| background: linear-gradient( | |
| 90deg, | |
| rgba(255, 255, 255, 0.05) 25%, | |
| rgba(255, 255, 255, 0.15) 50%, | |
| rgba(255, 255, 255, 0.05) 75% | |
| ); | |
| background-size: 200% 100%; | |
| animation: skeleton-loading 1.5s ease-in-out infinite; | |
| border-radius: 8px; | |
| } | |
| @keyframes skeleton-loading { | |
| 0% { background-position: 200% 0; } | |
| 100% { background-position: -200% 0; } | |
| } | |
| .skeleton-text { | |
| height: 16px; | |
| margin-bottom: 8px; | |
| } | |
| .skeleton-title { | |
| height: 24px; | |
| width: 60%; | |
| margin-bottom: 16px; | |
| } | |
| .skeleton-avatar { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| } | |
| /* Enhanced Table Row Animations */ | |
| table tbody tr { | |
| animation: rowSlideIn 0.4s ease-out; | |
| animation-fill-mode: both; | |
| } | |
| table tbody tr:nth-child(1) { animation-delay: 0.05s; } | |
| table tbody tr:nth-child(2) { animation-delay: 0.1s; } | |
| table tbody tr:nth-child(3) { animation-delay: 0.15s; } | |
| table tbody tr:nth-child(4) { animation-delay: 0.2s; } | |
| table tbody tr:nth-child(5) { animation-delay: 0.25s; } | |
| table tbody tr:nth-child(n+6) { animation-delay: 0.3s; } | |
| @keyframes rowSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| /* Enhanced Hover Effects */ | |
| tr { | |
| transition: all 0.2s ease; | |
| cursor: pointer; | |
| } | |
| tr:hover { | |
| background: rgba(59, 130, 246, 0.1) ; | |
| transform: translateX(5px); | |
| box-shadow: -5px 0 0 var(--accent-blue); | |
| } | |
| /* Search Bar */ | |
| .search-container { | |
| position: relative; | |
| margin-bottom: 20px; | |
| } | |
| .search-input { | |
| width: 100%; | |
| padding: 14px 20px 14px 50px; | |
| background: rgba(17, 24, 39, 0.8); | |
| border: 2px solid var(--border); | |
| border-radius: 12px; | |
| color: var(--text-primary); | |
| font-size: 14px; | |
| transition: all 0.3s ease; | |
| } | |
| .search-input:focus { | |
| outline: none; | |
| border-color: var(--accent-blue); | |
| box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.1); | |
| background: rgba(17, 24, 39, 0.95); | |
| } | |
| .search-icon { | |
| position: absolute; | |
| left: 18px; | |
| top: 50%; | |
| transform: translateY(-50%); | |
| color: var(--text-secondary); | |
| font-size: 18px; | |
| pointer-events: none; | |
| } | |
| /* Filter Chips */ | |
| .filter-chips { | |
| display: flex; | |
| gap: 10px; | |
| flex-wrap: wrap; | |
| margin-bottom: 20px; | |
| } | |
| .filter-chip { | |
| padding: 8px 16px; | |
| background: rgba(17, 24, 39, 0.6); | |
| border: 1px solid var(--border); | |
| border-radius: 20px; | |
| color: var(--text-secondary); | |
| font-size: 13px; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .filter-chip:hover { | |
| border-color: var(--accent-blue); | |
| color: var(--accent-blue); | |
| transform: translateY(-2px); | |
| } | |
| .filter-chip.active { | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| border-color: transparent; | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(59, 130, 246, 0.4); | |
| } | |
| /* Enhanced Toast Notifications */ | |
| .toast-container { | |
| position: fixed; | |
| top: 20px; | |
| right: 20px; | |
| z-index: 10000; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 12px; | |
| max-width: 400px; | |
| } | |
| .toast { | |
| position: relative; | |
| padding: 16px 20px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 12px; | |
| box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| min-width: 300px; | |
| animation: toastSlideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| backdrop-filter: blur(20px); | |
| } | |
| @keyframes toastSlideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(400px) scale(0.8); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0) scale(1); | |
| } | |
| } | |
| .toast-icon { | |
| font-size: 24px; | |
| flex-shrink: 0; | |
| } | |
| .toast-content { | |
| flex: 1; | |
| } | |
| .toast-title { | |
| font-weight: 700; | |
| font-size: 14px; | |
| margin-bottom: 4px; | |
| } | |
| .toast-message { | |
| font-size: 13px; | |
| color: var(--text-secondary); | |
| } | |
| .toast-close { | |
| background: none; | |
| border: none; | |
| color: var(--text-secondary); | |
| font-size: 20px; | |
| cursor: pointer; | |
| padding: 0; | |
| width: 24px; | |
| height: 24px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| border-radius: 6px; | |
| transition: all 0.2s ease; | |
| } | |
| .toast-close:hover { | |
| background: rgba(255, 255, 255, 0.1); | |
| color: var(--text-primary); | |
| } | |
| /* Progress Indicator */ | |
| .progress-indicator { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 3px; | |
| background: rgba(255, 255, 255, 0.1); | |
| z-index: 10001; | |
| overflow: hidden; | |
| } | |
| .progress-bar { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--accent-blue), var(--accent-purple), var(--accent-pink)); | |
| width: 0%; | |
| transition: width 0.3s ease; | |
| animation: progress-shimmer 2s infinite; | |
| } | |
| @keyframes progress-shimmer { | |
| 0% { background-position: -200% 0; } | |
| 100% { background-position: 200% 0; } | |
| } | |
| /* Floating Action Button */ | |
| .fab { | |
| position: fixed; | |
| bottom: 30px; | |
| right: 30px; | |
| width: 60px; | |
| height: 60px; | |
| border-radius: 50%; | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| border: none; | |
| color: white; | |
| font-size: 24px; | |
| cursor: pointer; | |
| box-shadow: 0 10px 30px rgba(59, 130, 246, 0.4); | |
| transition: all 0.3s ease; | |
| z-index: 1000; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .fab:hover { | |
| transform: scale(1.1) rotate(90deg); | |
| box-shadow: 0 15px 40px rgba(59, 130, 246, 0.6); | |
| } | |
| .fab:active { | |
| transform: scale(0.95); | |
| } | |
| /* Success/Error Feedback */ | |
| .feedback-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(0, 0, 0, 0.7); | |
| backdrop-filter: blur(5px); | |
| z-index: 10002; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .feedback-overlay.show { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .feedback-card { | |
| background: var(--bg-card); | |
| border-radius: 20px; | |
| padding: 40px; | |
| text-align: center; | |
| max-width: 400px; | |
| border: 2px solid var(--border); | |
| transform: scale(0.8); | |
| transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| } | |
| .feedback-overlay.show .feedback-card { | |
| transform: scale(1); | |
| } | |
| .feedback-icon { | |
| font-size: 64px; | |
| margin-bottom: 20px; | |
| animation: feedbackBounce 0.6s ease; | |
| } | |
| @keyframes feedbackBounce { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.2); } | |
| } | |
| .feedback-title { | |
| font-size: 24px; | |
| font-weight: 800; | |
| margin-bottom: 10px; | |
| } | |
| .feedback-message { | |
| color: var(--text-secondary); | |
| margin-bottom: 30px; | |
| } | |
| /* Pulse Animation for Live Data */ | |
| .pulse-data { | |
| animation: pulseGlow 2s ease-in-out infinite; | |
| } | |
| @keyframes pulseGlow { | |
| 0%, 100% { | |
| box-shadow: 0 0 5px rgba(59, 130, 246, 0.5); | |
| } | |
| 50% { | |
| box-shadow: 0 0 20px rgba(59, 130, 246, 0.8), 0 0 30px rgba(59, 130, 246, 0.4); | |
| } | |
| } | |
| /* Smooth Scroll */ | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| /* Enhanced Focus States */ | |
| *:focus-visible { | |
| outline: 2px solid var(--accent-blue); | |
| outline-offset: 2px; | |
| border-radius: 4px; | |
| } | |
| /* Loading Overlay */ | |
| .loading-overlay { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: rgba(10, 14, 26, 0.9); | |
| backdrop-filter: blur(10px); | |
| z-index: 10003; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| gap: 20px; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| } | |
| .loading-overlay.show { | |
| opacity: 1; | |
| pointer-events: all; | |
| } | |
| .loading-spinner-large { | |
| width: 80px; | |
| height: 80px; | |
| border: 6px solid var(--border); | |
| border-top-color: var(--accent-blue); | |
| border-radius: 50%; | |
| animation: spin 1s linear infinite; | |
| } | |
| .loading-text { | |
| font-size: 18px; | |
| font-weight: 600; | |
| color: var(--text-primary); | |
| } | |
| /* Tooltip */ | |
| .tooltip { | |
| position: relative; | |
| cursor: help; | |
| } | |
| .tooltip::before { | |
| content: attr(data-tooltip); | |
| position: absolute; | |
| bottom: 100%; | |
| left: 50%; | |
| transform: translateX(-50%) translateY(-10px); | |
| padding: 8px 12px; | |
| background: var(--bg-card); | |
| border: 1px solid var(--border); | |
| border-radius: 8px; | |
| font-size: 12px; | |
| white-space: nowrap; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: all 0.3s ease; | |
| z-index: 1000; | |
| } | |
| .tooltip:hover::before { | |
| opacity: 1; | |
| transform: translateX(-50%) translateY(-5px); | |
| } | |
| /* Gradient Text Animation */ | |
| .gradient-text { | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple), var(--accent-pink)); | |
| background-size: 200% 200%; | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| animation: gradientShift 3s ease infinite; | |
| } | |
| @keyframes gradientShift { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| /* Badge Pulse */ | |
| .badge-pulse { | |
| animation: badgePulse 2s ease-in-out infinite; | |
| } | |
| @keyframes badgePulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| } | |
| /* Smooth Transitions for All Interactive Elements */ | |
| button, a, input, select, textarea { | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* Enhanced Table Styling */ | |
| table { | |
| border-collapse: separate; | |
| border-spacing: 0; | |
| } | |
| thead th:first-child { | |
| border-top-left-radius: 12px; | |
| } | |
| thead th:last-child { | |
| border-top-right-radius: 12px; | |
| } | |
| tbody tr:last-child td:first-child { | |
| border-bottom-left-radius: 12px; | |
| } | |
| tbody tr:last-child td:last-child { | |
| border-bottom-right-radius: 12px; | |
| } | |
| /* SVG Icon Styles */ | |
| .icon { | |
| width: 20px; | |
| height: 20px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| } | |
| .icon-sm { | |
| width: 16px; | |
| height: 16px; | |
| } | |
| .icon-md { | |
| width: 24px; | |
| height: 24px; | |
| } | |
| .icon-lg { | |
| width: 32px; | |
| height: 32px; | |
| } | |
| .icon-xl { | |
| width: 48px; | |
| height: 48px; | |
| } | |
| .icon svg { | |
| width: 100%; | |
| height: 100%; | |
| stroke: currentColor; | |
| fill: none; | |
| stroke-width: 2; | |
| stroke-linecap: round; | |
| stroke-linejoin: round; | |
| } | |
| .icon-filled svg { | |
| fill: currentColor; | |
| stroke: none; | |
| } | |
| .icon-gradient { | |
| background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple)); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| background-clip: text; | |
| } | |
| /* Tab Icons */ | |
| .tab-icon { | |
| width: 18px; | |
| height: 18px; | |
| margin-right: 8px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .tab-icon svg { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Status Icons */ | |
| .status-icon { | |
| width: 20px; | |
| height: 20px; | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .status-icon-success svg { | |
| color: var(--accent-green); | |
| } | |
| .status-icon-error svg { | |
| color: var(--accent-red); | |
| } | |
| .status-icon-warning svg { | |
| color: var(--accent-yellow); | |
| } | |
| .status-icon-info svg { | |
| color: var(--accent-blue); | |
| } | |
| </style> | |
| <!-- SVG Icon Definitions --> | |
| <svg style="display: none;" xmlns="http://www.w3.org/2000/svg"> | |
| <!-- Success Icon --> | |
| <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> | |
| <!-- Error Icon --> | |
| <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> | |
| <!-- Warning Icon --> | |
| <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> | |
| <!-- Info Icon --> | |
| <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> | |
| <!-- Market/Chart Icon --> | |
| <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> | |
| <!-- Monitor/API Icon --> | |
| <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> | |
| <!-- Advanced/Flash Icon --> | |
| <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> | |
| <!-- Settings/Gear Icon --> | |
| <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> | |
| <!-- HuggingFace Icon --> | |
| <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> | |
| <!-- Pools/Refresh Icon --> | |
| <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> | |
| <!-- Logs/File Icon --> | |
| <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> | |
| <!-- Resources/Box Icon --> | |
| <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> | |
| <!-- Reports/Chart Icon --> | |
| <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> | |
| <!-- Search Icon --> | |
| <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> | |
| <!-- Refresh Icon --> | |
| <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> | |
| <!-- Trending Up Icon --> | |
| <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> | |
| <!-- Trending Down Icon --> | |
| <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> | |
| <!-- Volume Icon --> | |
| <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> | |
| <!-- Diamond/Gem Icon --> | |
| <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> | |
| <!-- Fire/Trending Icon --> | |
| <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> | |
| <!-- Link/Chain Icon --> | |
| <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> | |
| <!-- Export/Download Icon --> | |
| <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> | |
| <!-- Delete/Trash Icon --> | |
| <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> | |
| <!-- Brain/AI Icon --> | |
| <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> | |
| <!-- Arrow Up Icon --> | |
| <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> | |
| <!-- Live/Dot Icon --> | |
| <symbol id="icon-live" viewBox="0 0 24 24"> | |
| <circle cx="12" cy="12" r="10" fill="currentColor"/> | |
| </symbol> | |
| <!-- Bitcoin Icon --> | |
| <symbol id="icon-bitcoin" 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 2zm3.5 11.5c0 1.93-1.57 3.5-3.5 3.5h-2v-3h2c.83 0 1.5-.67 1.5-1.5s-.67-1.5-1.5-1.5h-2V8h2c.83 0 1.5-.67 1.5-1.5S12.83 5 12 5h-2V3h2c1.93 0 3.5 1.57 3.5 3.5 0 1.25-.68 2.34-1.68 2.93.84.59 1.18 1.68 1.18 2.57z" fill="currentColor"/> | |
| </symbol> | |
| <!-- Home Icon --> | |
| <symbol id="icon-home" viewBox="0 0 24 24"> | |
| <path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M9 22V12h6v10" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </symbol> | |
| <!-- Check Icon --> | |
| <symbol id="icon-check" viewBox="0 0 24 24"> | |
| <path d="M20 6L9 17l-5-5" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" fill="none"/> | |
| </symbol> | |
| <!-- Close Icon --> | |
| <symbol id="icon-close" viewBox="0 0 24 24"> | |
| <path d="M18 6L6 18M6 6l12 12" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
| </symbol> | |
| <!-- News Icon --> | |
| <symbol id="icon-news" viewBox="0 0 24 24"> | |
| <path d="M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <path d="M7 7h10M7 11h10M7 15h10" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> | |
| </symbol> | |
| <!-- Sentiment Icon --> | |
| <symbol id="icon-sentiment" viewBox="0 0 24 24"> | |
| <circle cx="12" cy="12" r="10" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <path d="M8 14s1.5 2 4 2 4-2 4-2" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> | |
| <circle cx="9" cy="9" r="1" fill="currentColor"/> | |
| <circle cx="15" cy="9" r="1" fill="currentColor"/> | |
| </symbol> | |
| <!-- Whale Icon --> | |
| <symbol id="icon-whale" viewBox="0 0 24 24"> | |
| <path d="M2 12c0-2.2 1.8-4 4-4h12c2.2 0 4 1.8 4 4v5c0 2.2-1.8 4-4 4H6c-2.2 0-4-1.8-4-4v-5z" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <path d="M6 8V6M10 8V5M14 8V5M18 8V6" stroke="currentColor" stroke-width="2" stroke-linecap="round"/> | |
| <circle cx="8" cy="12" r="1" fill="currentColor"/> | |
| </symbol> | |
| <!-- Database Icon --> | |
| <symbol id="icon-database" viewBox="0 0 24 24"> | |
| <ellipse cx="12" cy="5" rx="9" ry="3" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <path d="M3 5v14c0 1.66 4.03 3 9 3s9-1.34 9-3V5" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <path d="M3 12c0 1.66 4.03 3 9 3s9-1.34 9-3" stroke="currentColor" stroke-width="2"/> | |
| </symbol> | |
| <!-- Rocket Icon --> | |
| <symbol id="icon-rocket" viewBox="0 0 24 24"> | |
| <path d="M9 11L3 17v4l2-2 4-4M15 11l6 6v4l-2-2-4-4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> | |
| <path d="M12 2c3.87 0 7 3.13 7 7v4l-7 7-7-7V9c0-3.87 3.13-7 7-7z" fill="none" stroke="currentColor" stroke-width="2"/> | |
| <circle cx="12" cy="8" r="2" fill="currentColor"/> | |
| </symbol> | |
| </svg> | |
| </head> | |
| <body> | |
| <!-- Progress Indicator --> | |
| <div class="progress-indicator" id="progressIndicator"> | |
| <div class="progress-bar" id="progressBar"></div> | |
| </div> | |
| <!-- Toast Container --> | |
| <div class="toast-container" id="toastContainer"></div> | |
| <!-- Loading Overlay --> | |
| <div class="loading-overlay" id="loadingOverlay"> | |
| <div class="loading-spinner-large"></div> | |
| <div class="loading-text" id="loadingText">Loading...</div> | |
| </div> | |
| <!-- Feedback Overlay --> | |
| <div class="feedback-overlay" id="feedbackOverlay"> | |
| <div class="feedback-card"> | |
| <div class="feedback-icon" id="feedbackIcon"> | |
| <svg width="48" height="48"><use href="#icon-check"></use></svg> | |
| </div> | |
| <div class="feedback-title" id="feedbackTitle">Success!</div> | |
| <div class="feedback-message" id="feedbackMessage">Operation completed successfully</div> | |
| <button class="refresh-btn ripple" onclick="hideFeedback()">Close</button> | |
| </div> | |
| </div> | |
| <!-- Floating Action Button --> | |
| <button class="fab ripple" onclick="scrollToTop()" title="Back to top"> | |
| ↑ | |
| </button> | |
| <!-- WebSocket Status Indicator --> | |
| <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">Connecting...</span> | |
| <div id="online-users-badge" class="badge badge-info badge-pulse" style="margin-left: 10px;">0</div> | |
| </div> | |
| <div class="container"> | |
| <!-- Header --> | |
| <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> | |
| <!-- Tabs --> | |
| <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> | |
| <a href="/ai-tools" class="tab" style="text-decoration: none; display: inline-flex; align-items: center; gap: 8px;"> | |
| <span class="tab-icon"><svg><use href="#icon-hf"></use></svg></span> | |
| AI Tools | |
| </a> | |
| </div> | |
| </div> | |
| <!-- Market Tab --> | |
| <div id="tab-market" class="tab-content active"> | |
| <!-- Stats Grid --> | |
| <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">Online Users</div> | |
| <div class="stat-change positive"> | |
| <svg width="20" height="20"><use href="#icon-market"></use></svg> | |
| <span>Total Sessions: <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> | |
| <!-- Market Table --> | |
| <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="Refresh market data"> | |
| <span class="icon icon-sm"><svg><use href="#icon-refresh"></use></svg></span> | |
| Refresh | |
| </button> | |
| </div> | |
| <!-- Search Bar --> | |
| <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="Search cryptocurrency (e.g., Bitcoin, BTC, Ethereum)..." oninput="filterMarketTable()"> | |
| </div> | |
| <!-- Filter Chips --> | |
| <div class="filter-chips"> | |
| <button class="filter-chip active" onclick="filterByCategory('all')">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> | |
| Gainers | |
| </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> | |
| Losers | |
| </button> | |
| <button class="filter-chip" onclick="filterByCategory('volume')"> | |
| <span class="icon icon-sm"><svg><use href="#icon-volume"></use></svg></span> | |
| High Volume | |
| </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> | |
| <!-- Charts Row --> | |
| <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;"> | |
| <svg width="24" height="24" style="vertical-align: middle; margin-right: 8px;"><use href="#icon-trending-up"></use></svg> | |
| Market Dominance | |
| </div> | |
| <canvas id="dominanceChart"></canvas> | |
| </div> | |
| <div class="chart-container"> | |
| <div class="section-title" style="margin-bottom: 20px;"> | |
| <svg width="24" height="24" style="vertical-align: middle; margin-right: 8px;"><use href="#icon-sentiment"></use></svg> | |
| Fear & Greed Index | |
| </div> | |
| <div style="text-align: center; padding: 20px;"> | |
| <canvas id="gaugeChart"></canvas> | |
| <div style="margin-top: 20px;"> | |
| <div style="font-size: 64px; font-weight: 900; margin-bottom: 10px; line-height: 1;" id="sentimentValue">50</div> | |
| <div style="font-size: 20px; font-weight: 700; margin-bottom: 8px;" id="sentimentText">Neutral</div> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 15px;" id="sentimentDescription"> | |
| Market sentiment is balanced | |
| </div> | |
| <!-- Sentiment Meter Bar --> | |
| <div style="width: 100%; height: 8px; background: rgba(255, 255, 255, 0.1); border-radius: 4px; overflow: hidden; margin-bottom: 15px;"> | |
| <div id="sentimentBar" style="height: 100%; width: 50%; background: linear-gradient(90deg, #ef4444 0%, #f59e0b 25%, #6b7280 50%, #3b82f6 75%, #10b981 100%); border-radius: 4px; transition: width 0.5s ease;"></div> | |
| </div> | |
| <!-- Sentiment Scale Labels --> | |
| <div style="display: flex; justify-content: space-between; font-size: 10px; color: var(--text-secondary); margin-top: 5px;"> | |
| <span>Extreme Fear</span> | |
| <span>Fear</span> | |
| <span>Neutral</span> | |
| <span>Greed</span> | |
| <span>Extreme Greed</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Trending Section --> | |
| <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> | |
| <!-- DeFi Section --> | |
| <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> | |
| <!-- API Monitor Tab --> | |
| <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> | |
| <!-- Advanced Tab --> | |
| <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()"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-export"></use></svg> | |
| Export CSV | |
| </button> | |
| <button class="refresh-btn" onclick="createBackup()"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-refresh"></use></svg> | |
| 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;"> | |
| <svg width="24" height="24" style="vertical-align: middle; margin-right: 8px;"><use href="#icon-market"></use></svg> | |
| 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> | |
| <!-- Admin Tab --> | |
| <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> | |
| <!-- HuggingFace Tab --> | |
| <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> | |
| <!-- Logs Tab --> | |
| <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> | |
| <!-- Filters --> | |
| <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> | |
| <!-- Log Stats --> | |
| <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> | |
| <!-- Logs Table --> | |
| <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> | |
| <!-- Resources Tab --> | |
| <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()"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-refresh"></use></svg> | |
| Refresh | |
| </button> | |
| <button class="refresh-btn" onclick="exportResourcesJSON()" | |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-database"></use></svg> | |
| Export JSON | |
| </button> | |
| <button class="refresh-btn" onclick="exportResourcesCSV()" | |
| style="background: rgba(16, 185, 129, 0.2); color: var(--accent-green);"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-export"></use></svg> | |
| 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> | |
| <!-- Resource Stats --> | |
| <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> | |
| <!-- Category Filter --> | |
| <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="local">🏠 Local Backend Routes</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> | |
| <!-- Resources Grid --> | |
| <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> | |
| <!-- Import Modal --> | |
| <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> | |
| <!-- Reports Tab --> | |
| <div id="tab-reports" class="tab-content"> | |
| <!-- System Status Alerts --> | |
| <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> | |
| <!-- Diagnostics Results --> | |
| <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> | |
| <!-- Pools Tab --> | |
| <div id="tab-pools" class="tab-content"> | |
| <div class="market-section"> | |
| <div class="section-header"> | |
| <div class="section-title"> | |
| <svg width="24" height="24" style="vertical-align: middle; margin-right: 8px;"><use href="#icon-pools"></use></svg> | |
| Source Pool Management | |
| </div> | |
| <div style="display: flex; gap: 10px;"> | |
| <button class="refresh-btn" onclick="showCreatePoolModal()"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-check"></use></svg> | |
| Create Pool | |
| </button> | |
| <button class="refresh-btn" onclick="loadPools()"> | |
| <svg width="16" height="16" style="vertical-align: middle; margin-right: 5px;"><use href="#icon-refresh"></use></svg> | |
| 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> | |
| <!-- Create Pool Modal --> | |
| <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> | |
| <!-- Add Member Modal --> | |
| <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> | |
| // Global variables | |
| let charts = {}; | |
| let wsConnection = null; | |
| let currentTab = 'market'; | |
| // Tab switching | |
| 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'); | |
| // Load data for specific tabs | |
| 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(); | |
| } | |
| } | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| await loadMarketData(); | |
| initCharts(); | |
| connectWebSocket(); | |
| setInterval(() => { | |
| if (currentTab === 'market') loadMarketData(); | |
| else if (currentTab === 'monitor') loadMonitorData(); | |
| }, 60000); | |
| }); | |
| // Market Data Functions | |
| async function loadMarketData() { | |
| try { | |
| // Show loading state | |
| 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);">Loading market data...</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); | |
| // Check if responses are OK | |
| if (!marketRes.ok) throw new Error(`Error fetching market data: ${marketRes.status}`); | |
| if (!statsRes.ok) throw new Error(`Error fetching stats: ${statsRes.status}`); | |
| if (!sentimentRes.ok) throw new Error(`Error fetching sentiment: ${sentimentRes.status}`); | |
| if (!trendingRes.ok) throw new Error(`Error fetching trending: ${trendingRes.status}`); | |
| if (!defiRes.ok) throw new Error(`Error fetching DeFi: ${defiRes.status}`); | |
| const [market, stats, sentiment, trending, defi] = await Promise.all([ | |
| marketRes.json(), | |
| statsRes.json(), | |
| sentimentRes.json(), | |
| trendingRes.json(), | |
| defiRes.json() | |
| ]); | |
| // Validate data with more detailed checks | |
| if (!market || !Array.isArray(market.cryptocurrencies)) { | |
| throw new Error('Invalid market data: cryptocurrencies array not found'); | |
| } | |
| if (!stats || typeof stats !== 'object' || stats === null) { | |
| console.error('Invalid stats:', stats); | |
| throw new Error('Invalid stats: stats object not found'); | |
| } | |
| // Check if stats.market exists and is an object | |
| if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) { | |
| console.error('Invalid stats.market:', stats.market); | |
| console.error('Full stats object:', JSON.stringify(stats, null, 2)); | |
| throw new Error('Invalid stats: stats.market object not found'); | |
| } | |
| if (!sentiment || typeof sentiment !== 'object' || sentiment === null) { | |
| throw new Error('Invalid sentiment data: sentiment object not found'); | |
| } | |
| // Note: sentiment can have different structures: | |
| // - sentiment.fear_greed_index (from /api/sentiment) | |
| // - sentiment.fear_greed_value (from /api/stats) | |
| // So we don't validate the exact structure here | |
| if (!trending || !Array.isArray(trending.trending)) { | |
| throw new Error('Invalid trending data: trending array not found'); | |
| } | |
| if (!defi || typeof defi !== 'object' || defi === null) { | |
| throw new Error('Invalid DeFi data: defi object not found'); | |
| } | |
| // Call updateStats with validated data - double check before calling | |
| if (stats && stats.market && typeof stats.market === 'object' && !Array.isArray(stats.market)) { | |
| updateStats(stats, sentiment); | |
| } else { | |
| console.error('Failed final validation before updateStats:', { stats, sentiment }); | |
| throw new Error('Invalid stats.market data'); | |
| } | |
| 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;">Error loading data</div> | |
| <div style="font-size: 14px; color: var(--text-secondary);">${error.message || 'Unknown error'}</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;">Try Again</button> | |
| </td></tr>`; | |
| } | |
| showToast('❌ Error loading market data: ' + (error.message || 'Unknown error'), 'error'); | |
| } | |
| } | |
| function updateStats(stats, sentiment) { | |
| try { | |
| // More robust validation with detailed checks | |
| if (!stats || typeof stats !== 'object' || stats === null) { | |
| console.warn('updateStats: stats is undefined, null, or not an object', stats); | |
| return; | |
| } | |
| if (!stats.market || typeof stats.market !== 'object' || stats.market === null || Array.isArray(stats.market)) { | |
| console.warn('updateStats: stats.market is invalid', { stats, market: stats.market }); | |
| return; | |
| } | |
| if (!sentiment || typeof sentiment !== 'object' || sentiment === null) { | |
| console.warn('updateStats: sentiment is undefined, null, or not an object', sentiment); | |
| return; | |
| } | |
| // Use safe property access for market data with additional checks | |
| const marketObj = stats.market; | |
| if (!marketObj || typeof marketObj !== 'object' || marketObj === null) { | |
| console.warn('updateStats: marketObj is invalid', marketObj); | |
| return; | |
| } | |
| const mcap = (typeof marketObj.total_market_cap !== 'undefined' && marketObj.total_market_cap !== null) ? marketObj.total_market_cap : 0; | |
| const totalMarketCapEl = document.getElementById('totalMarketCap'); | |
| if (totalMarketCapEl) { | |
| totalMarketCapEl.textContent = '$' + (mcap / 1e12).toFixed(2) + 'T'; | |
| } | |
| const 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) + '%'; | |
| } | |
| // Handle sentiment data - support both structures: | |
| // 1. sentiment.fear_greed_index.value (from /api/sentiment) | |
| // 2. sentiment.fear_greed_value (from /api/stats) | |
| let fg = 50; | |
| let classification = 'Neutral'; | |
| if (sentiment.fear_greed_index && typeof sentiment.fear_greed_index === 'object') { | |
| // Structure from /api/sentiment endpoint | |
| fg = (typeof sentiment.fear_greed_index.value !== 'undefined') ? sentiment.fear_greed_index.value : 50; | |
| classification = sentiment.fear_greed_index.classification || 'Neutral'; | |
| } else if (typeof sentiment.fear_greed_value !== 'undefined') { | |
| // Structure from /api/stats endpoint | |
| fg = sentiment.fear_greed_value; | |
| classification = sentiment.classification || 'Neutral'; | |
| } else if (typeof sentiment.value !== 'undefined') { | |
| // Fallback structure | |
| fg = sentiment.value; | |
| classification = sentiment.classification || 'Neutral'; | |
| } | |
| const 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);">No data found</td></tr>'; | |
| } | |
| return; | |
| } | |
| const tbody = document.getElementById('marketTableBody'); | |
| if (!tbody) return; | |
| // Store data for filtering | |
| marketDataCache = cryptos; | |
| tbody.innerHTML = cryptos.map((crypto, index) => { | |
| const price = crypto.price || 0; | |
| const change24h = crypto.change_24h || 0; | |
| const marketCap = crypto.market_cap || 0; | |
| const volume24h = crypto.volume_24h || 0; | |
| const symbol = crypto.symbol || 'N/A'; | |
| const name = crypto.name || 'Unknown'; | |
| const changeClass = change24h >= 0 ? 'positive' : 'negative'; | |
| const changeIcon = change24h >= 0 ? | |
| '<svg width="16" height="16" style="vertical-align: middle;"><use href="#icon-trending-up"></use></svg>' : | |
| '<svg width="16" height="16" style="vertical-align: middle;"><use href="#icon-trending-down"></use></svg>'; | |
| 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);">Error displaying data</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);">No trending coins found</div>'; | |
| return; | |
| } | |
| grid.innerHTML = trending.map((coin, index) => { | |
| const name = coin.name || 'Unknown'; | |
| 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);">Error displaying trending</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 || 'Unknown'; | |
| 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;"> | |
| ${changeIcon} ${change24h >= 0 ? '+' : ''}${change24h.toFixed(2)}% | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| `; | |
| }).join('') : '<div class="empty-state"><div class="empty-state-icon">📦</div><div>No protocols found</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);">Error displaying DeFi data</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 || market.btc_dominance || 0; | |
| const ethDom = market.global?.eth_dominance || 0; | |
| charts.dominance.data.datasets[0].data = [btcDom, ethDom, Math.max(0, 100 - btcDom - ethDom)]; | |
| charts.dominance.update(); | |
| // Handle different sentiment data structures | |
| let fg = 50; | |
| let classification = 'Neutral'; | |
| if (sentiment.fear_greed_index && typeof sentiment.fear_greed_index === 'object') { | |
| fg = sentiment.fear_greed_index.value || sentiment.fear_greed_index || 50; | |
| classification = sentiment.fear_greed_index.value_classification || sentiment.fear_greed_index.classification || 'Neutral'; | |
| } else if (typeof sentiment.fear_greed_index === 'number') { | |
| fg = sentiment.fear_greed_index; | |
| classification = sentiment.fear_greed_label || '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'; | |
| } | |
| // Ensure fg is between 0-100 | |
| fg = Math.max(0, Math.min(100, fg)); | |
| charts.gauge.data.datasets[0].data = [fg, 100 - fg]; | |
| let barColor = '#6b7280'; | |
| let description = 'Market sentiment is balanced'; | |
| if (fg < 25) { | |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#ef4444'; | |
| barColor = '#ef4444'; | |
| description = 'Extreme fear - investors are very worried, potential buying opportunity'; | |
| } else if (fg < 45) { | |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#f59e0b'; | |
| barColor = '#f59e0b'; | |
| description = 'Fear - market shows concern, cautious sentiment'; | |
| } else if (fg < 55) { | |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#6b7280'; | |
| barColor = '#6b7280'; | |
| description = 'Neutral - balanced market sentiment'; | |
| } else if (fg < 75) { | |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#3b82f6'; | |
| barColor = '#3b82f6'; | |
| description = 'Greed - optimistic market sentiment'; | |
| } else { | |
| charts.gauge.data.datasets[0].backgroundColor[0] = '#10b981'; | |
| barColor = '#10b981'; | |
| description = 'Extreme greed - very optimistic, potential correction risk'; | |
| } | |
| charts.gauge.update(); | |
| const sentimentValueEl = document.getElementById('sentimentValue'); | |
| const sentimentTextEl = document.getElementById('sentimentText'); | |
| const sentimentDescEl = document.getElementById('sentimentDescription'); | |
| const sentimentBarEl = document.getElementById('sentimentBar'); | |
| if (sentimentValueEl) { | |
| sentimentValueEl.textContent = fg; | |
| sentimentValueEl.style.color = barColor; | |
| } | |
| if (sentimentTextEl) { | |
| sentimentTextEl.textContent = classification; | |
| sentimentTextEl.style.color = barColor; | |
| } | |
| if (sentimentDescEl) { | |
| sentimentDescEl.textContent = description; | |
| } | |
| if (sentimentBarEl) { | |
| sentimentBarEl.style.width = fg + '%'; | |
| sentimentBarEl.style.background = `linear-gradient(90deg, #ef4444 0%, #f59e0b 25%, #6b7280 50%, #3b82f6 75%, #10b981 100%)`; | |
| } | |
| } | |
| // استفاده از WebSocket Client جدید | |
| let wsConnectAttempts = 0; | |
| const MAX_WS_CONNECT_ATTEMPTS = 10; // حداکثر 10 تلاش (10 ثانیه) | |
| let wsStatsInterval = null; | |
| function connectWebSocket() { | |
| // WebSocket client از websocket-client.js استفاده میشود | |
| // که به صورت خودکار اتصال برقرار میکند | |
| // بررسی وجود wsClient و متدهای مورد نیاز | |
| if (window.wsClient && typeof window.wsClient.on === 'function' && typeof window.wsClient.requestStats === 'function') { | |
| console.log('✅ WebSocket Client آماده است'); | |
| wsConnectAttempts = 0; // Reset counter on success | |
| // ثبت handler برای بهروزرسانی آمار | |
| window.wsClient.on('stats_update', (message) => { | |
| console.log('📊 Stats update:', message.data); | |
| if (typeof updateOnlineStats === 'function') { | |
| updateOnlineStats(message.data); | |
| } | |
| }); | |
| window.wsClient.on('provider_stats', (message) => { | |
| console.log('📡 Provider stats:', message.data); | |
| if (currentTab === 'monitor' && 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); | |
| // درخواست آمار هر 10 ثانیه (فقط یک بار تنظیم شود) | |
| if (!wsStatsInterval) { | |
| wsStatsInterval = setInterval(() => { | |
| if (window.wsClient && window.wsClient.isConnected) { | |
| window.wsClient.requestStats(); | |
| } | |
| }, 10000); | |
| } | |
| } else { | |
| wsConnectAttempts++; | |
| if (wsConnectAttempts < MAX_WS_CONNECT_ATTEMPTS) { | |
| // فقط هر 5 ثانیه یک بار لاگ کنیم تا console پر نشود | |
| if (wsConnectAttempts % 5 === 0 || wsConnectAttempts === 1) { | |
| console.log(`⏳ در انتظار WebSocket Client... (${wsConnectAttempts}/${MAX_WS_CONNECT_ATTEMPTS})`); | |
| } | |
| setTimeout(connectWebSocket, 1000); | |
| } else { | |
| console.warn('⚠️ WebSocket Client پس از ' + MAX_WS_CONNECT_ATTEMPTS + ' تلاش آماده نشد. ممکن است فایل websocket-client.js لود نشده باشد یا WebSocket پشتیبانی نشود.'); | |
| console.warn('⚠️ بررسی کنید که فایل /static/js/websocket-client.js به درستی لود شده باشد.'); | |
| // تلاش نهایی بعد از 5 ثانیه | |
| setTimeout(() => { | |
| if (!window.wsClient) { | |
| console.warn('⚠️ WebSocket Client غیرفعال است. برخی ویژگیهای real-time ممکن است کار نکنند.'); | |
| console.warn('⚠️ برای فعال کردن WebSocket، صفحه را refresh کنید (Ctrl+F5 برای clear cache).'); | |
| } | |
| }, 5000); | |
| // متوقف کردن تلاشهای بیشتر | |
| return; | |
| } | |
| } | |
| } | |
| // تابع برای بهروزرسانی آمار آنلاین | |
| 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; | |
| } | |
| } | |
| // تابع برای بهروزرسانی نمایش آمار Provider | |
| 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; | |
| } | |
| } | |
| } | |
| // Monitor Functions | |
| async function loadMonitorData() { | |
| try { | |
| // Show loading state | |
| 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') | |
| ]); | |
| // Check if responses are OK | |
| if (!statusRes.ok) throw new Error(`خطا در دریافت وضعیت: ${statusRes.status}`); | |
| if (!providersRes.ok) throw new Error(`خطا در دریافت لیست APIها: ${providersRes.status}`); | |
| const [status, providers] = await Promise.all([ | |
| statusRes.json(), | |
| providersRes.json() | |
| ]); | |
| // Validate data | |
| 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; | |
| } | |
| } | |
| // Advanced Functions | |
| 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); | |
| } | |
| } | |
| // Admin Functions | |
| 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'); | |
| } | |
| // HuggingFace Functions | |
| 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>'; | |
| // Mock data for now | |
| 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>'; | |
| // Mock data for now | |
| 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>`; | |
| // Mock search | |
| 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>`; | |
| // Mock search | |
| 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}`; | |
| } | |
| } | |
| // Pools Functions | |
| 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(''); | |
| } | |
| // Load rotation history | |
| 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); | |
| } | |
| } | |
| // Toast notification function | |
| // Enhanced Toast Notification System | |
| function showToast(message, type = 'info', title = null) { | |
| const toastContainer = document.getElementById('toastContainer') || document.body; | |
| const toast = document.createElement('div'); | |
| toast.className = `toast toast-${type}`; | |
| const icons = { | |
| success: '✅', | |
| error: '❌', | |
| warning: '⚠️', | |
| info: 'ℹ️' | |
| }; | |
| const titles = { | |
| success: 'موفق!', | |
| error: 'خطا!', | |
| warning: 'هشدار!', | |
| info: 'اطلاعیه' | |
| }; | |
| toast.innerHTML = ` | |
| <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); | |
| // Auto remove after 5 seconds | |
| setTimeout(() => { | |
| toast.style.animation = 'toastSlideIn 0.3s reverse'; | |
| setTimeout(() => toast.remove(), 300); | |
| }, 5000); | |
| // Add click to dismiss | |
| toast.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('toast-close') || e.target === toast) { | |
| toast.style.animation = 'toastSlideIn 0.3s reverse'; | |
| setTimeout(() => toast.remove(), 300); | |
| } | |
| }); | |
| } | |
| // Progress Indicator Functions | |
| function showProgress(percent = 0) { | |
| const progressBar = document.getElementById('progressBar'); | |
| if (progressBar) { | |
| progressBar.style.width = percent + '%'; | |
| } | |
| } | |
| function hideProgress() { | |
| const progressBar = document.getElementById('progressBar'); | |
| if (progressBar) { | |
| progressBar.style.width = '0%'; | |
| } | |
| } | |
| // Loading Overlay Functions | |
| function showLoading(message = 'در حال بارگذاری...') { | |
| const overlay = document.getElementById('loadingOverlay'); | |
| const text = document.getElementById('loadingText'); | |
| if (overlay) { | |
| overlay.classList.add('show'); | |
| } | |
| if (text) { | |
| text.textContent = message; | |
| } | |
| } | |
| function hideLoading() { | |
| const overlay = document.getElementById('loadingOverlay'); | |
| if (overlay) { | |
| overlay.classList.remove('show'); | |
| } | |
| } | |
| // Feedback Overlay Functions | |
| function showFeedback(type, title, message) { | |
| const overlay = document.getElementById('feedbackOverlay'); | |
| const icon = document.getElementById('feedbackIcon'); | |
| const titleEl = document.getElementById('feedbackTitle'); | |
| const messageEl = document.getElementById('feedbackMessage'); | |
| if (overlay && icon && titleEl && messageEl) { | |
| const icons = { | |
| success: '✅', | |
| error: '❌', | |
| warning: '⚠️' | |
| }; | |
| icon.textContent = icons[type] || icons.success; | |
| titleEl.textContent = title; | |
| messageEl.textContent = message; | |
| overlay.classList.add('show'); | |
| // Auto hide after 3 seconds | |
| setTimeout(() => { | |
| hideFeedback(); | |
| }, 3000); | |
| } | |
| } | |
| function hideFeedback() { | |
| const overlay = document.getElementById('feedbackOverlay'); | |
| if (overlay) { | |
| overlay.classList.remove('show'); | |
| } | |
| } | |
| // Scroll to Top Function | |
| function scrollToTop() { | |
| window.scrollTo({ | |
| top: 0, | |
| behavior: 'smooth' | |
| }); | |
| } | |
| // Show FAB when scrolling down | |
| let lastScroll = 0; | |
| window.addEventListener('scroll', () => { | |
| const fab = document.querySelector('.fab'); | |
| if (fab) { | |
| const currentScroll = window.pageYOffset; | |
| if (currentScroll > 300) { | |
| fab.style.opacity = '1'; | |
| fab.style.pointerEvents = 'all'; | |
| } else { | |
| fab.style.opacity = '0'; | |
| fab.style.pointerEvents = 'none'; | |
| } | |
| lastScroll = currentScroll; | |
| } | |
| }); | |
| // Filter Market Table Function | |
| let currentFilter = 'all'; | |
| let marketDataCache = []; | |
| function filterMarketTable() { | |
| const searchInput = document.getElementById('marketSearch'); | |
| const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; | |
| const tbody = document.getElementById('marketTableBody'); | |
| if (!tbody) return; | |
| const rows = tbody.querySelectorAll('tr'); | |
| let visibleCount = 0; | |
| // Remove existing no-results row | |
| const existingNoResults = tbody.querySelector('tr[data-no-results]'); | |
| if (existingNoResults) { | |
| existingNoResults.remove(); | |
| } | |
| rows.forEach((row, index) => { | |
| if (row.querySelector('td[colspan]')) { | |
| return; // Skip loading/error rows | |
| } | |
| const cells = row.querySelectorAll('td'); | |
| if (cells.length < 4) return; | |
| const name = cells[1]?.textContent?.toLowerCase() || ''; | |
| const symbol = cells[1]?.querySelector('.crypto-symbol')?.textContent?.toLowerCase() || ''; | |
| const changeText = cells[3]?.textContent || ''; | |
| const changeValue = parseFloat(changeText.replace(/[^0-9.-]/g, '')) || 0; | |
| let matchesSearch = !searchTerm || name.includes(searchTerm) || symbol.includes(searchTerm); | |
| let matchesFilter = true; | |
| if (currentFilter === 'top10') { | |
| matchesFilter = index < 10; | |
| } else if (currentFilter === 'gainers') { | |
| matchesFilter = changeValue > 0; | |
| } else if (currentFilter === 'losers') { | |
| matchesFilter = changeValue < 0; | |
| } else if (currentFilter === 'volume') { | |
| // This would need volume data - for now show all | |
| matchesFilter = true; | |
| } | |
| if (matchesSearch && matchesFilter) { | |
| row.style.display = ''; | |
| visibleCount++; | |
| row.style.animation = `rowSlideIn 0.3s ease-out`; | |
| row.style.animationDelay = `${index * 0.05}s`; | |
| } else { | |
| row.style.display = 'none'; | |
| } | |
| }); | |
| // Show message if no results | |
| if (visibleCount === 0 && rows.length > 0 && !searchTerm && currentFilter === 'all') { | |
| // Don't show message if no search/filter is applied | |
| return; | |
| } | |
| if (visibleCount === 0) { | |
| const noResultsRow = document.createElement('tr'); | |
| noResultsRow.setAttribute('data-no-results', 'true'); | |
| noResultsRow.innerHTML = `<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; | |
| // Update active chip | |
| document.querySelectorAll('.filter-chip').forEach(chip => { | |
| chip.classList.remove('active'); | |
| }); | |
| if (event && event.target) { | |
| event.target.classList.add('active'); | |
| } | |
| filterMarketTable(); | |
| } | |
| // Number Counter Animation | |
| function animateNumber(element, from, to, duration = 1000) { | |
| if (!element) return; | |
| const start = performance.now(); | |
| const difference = to - from; | |
| function update(currentTime) { | |
| const elapsed = currentTime - start; | |
| const progress = Math.min(elapsed / duration, 1); | |
| // Easing function | |
| const easeOutQuart = 1 - Math.pow(1 - progress, 4); | |
| const current = from + (difference * easeOutQuart); | |
| element.textContent = typeof to === 'number' && to >= 1000 | |
| ? current.toLocaleString('fa-IR', { maximumFractionDigits: 2 }) | |
| : current.toFixed(2); | |
| if (progress < 1) { | |
| requestAnimationFrame(update); | |
| } else { | |
| element.classList.add('updated'); | |
| setTimeout(() => element.classList.remove('updated'), 500); | |
| } | |
| } | |
| requestAnimationFrame(update); | |
| } | |
| // Close modals when clicking outside | |
| document.addEventListener('click', (e) => { | |
| if (e.target.classList.contains('modal')) { | |
| e.target.style.display = 'none'; | |
| } | |
| }); | |
| // ===== Log Management Functions ===== | |
| 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(); | |
| // Update stats | |
| 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; | |
| } | |
| // Update table | |
| 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); | |
| } | |
| } | |
| // ===== Resource Management Functions ===== | |
| async function loadResources() { | |
| try { | |
| const category = document.getElementById('resourceCategoryFilter')?.value || ''; | |
| // Fetch main resources data | |
| const response = await fetch('/api/resources'); | |
| const data = await response.json(); | |
| // Fetch local routes from apis endpoint | |
| const apisResponse = await fetch('/api/resources/apis'); | |
| const apisData = await apisResponse.json(); | |
| const localRoutes = apisData?.local_routes?.routes || []; | |
| // Update stats | |
| if (data.success && data.summary && document.getElementById('totalResources')) { | |
| document.getElementById('totalResources').textContent = data.summary.total_resources || 0; | |
| document.getElementById('freeResources').textContent = data.summary.free_resources || 0; | |
| document.getElementById('paidResources').textContent = (data.summary.total_resources - data.summary.free_resources) || 0; | |
| document.getElementById('authResources').textContent = data.summary.local_routes_count || 0; | |
| } | |
| // Determine what to display | |
| const grid = document.getElementById('resourcesGrid'); | |
| let itemsToDisplay = []; | |
| if (category === 'local') { | |
| // Show local routes only | |
| itemsToDisplay = localRoutes; | |
| } else if (category) { | |
| // Filter by category (not implemented in current API but placeholder) | |
| itemsToDisplay = localRoutes.filter(r => r.category === category); | |
| } else { | |
| // Show sample local routes | |
| itemsToDisplay = localRoutes.slice(0, 20); | |
| } | |
| if (itemsToDisplay && itemsToDisplay.length > 0) { | |
| grid.innerHTML = itemsToDisplay.map(item => { | |
| const isLocal = item.category === 'local'; | |
| const method = item.notes?.match(/(GET|POST|WS)/i)?.[0] || 'GET'; | |
| const categoryBadge = isLocal | |
| ? '<span class="badge badge-success">🏠 Local</span>' | |
| : `<span class="badge badge-info">${item.category || 'unknown'}</span>`; | |
| const authType = item.auth?.type || 'none'; | |
| const authBadge = authType === 'none' | |
| ? '<span class="badge badge-success">No Auth</span>' | |
| : '<span class="badge badge-warning">Auth Required</span>'; | |
| const methodBadge = method === 'POST' | |
| ? '<span class="badge" style="background: rgba(245, 158, 11, 0.2); color: var(--accent-yellow);">POST</span>' | |
| : method === 'WS' | |
| ? '<span class="badge" style="background: rgba(139, 92, 246, 0.2); color: var(--accent-purple);">WebSocket</span>' | |
| : '<span class="badge" style="background: rgba(59, 130, 246, 0.2); color: var(--accent-blue);">GET</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;">${item.name || 'Unknown'}</div> | |
| ${categoryBadge} | |
| </div> | |
| <div style="display: flex; gap: 5px; flex-direction: column;"> | |
| ${methodBadge} | |
| ${authBadge} | |
| </div> | |
| </div> | |
| <div style="font-size: 12px; color: var(--text-secondary); margin-bottom: 10px; word-break: break-all; font-family: 'Courier New', monospace;"> | |
| ${item.base_url || 'N/A'} | |
| </div> | |
| ${item.notes ? `<div style="font-size: 11px; color: var(--text-secondary); margin-top: 10px; padding: 8px; background: rgba(255, 255, 255, 0.02); border-radius: 5px;"> | |
| ${item.notes} | |
| </div>` : ''} | |
| ${item.docs_url ? `<div style="margin-top: 10px;"><a href="${item.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 for this category</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: ' + error.message + '</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(); | |
| } | |
| // Import form handler | |
| 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); | |
| } | |
| }); | |
| } | |
| // ===== Reports Functions ===== | |
| async function loadReports() { | |
| await Promise.all([ | |
| loadDiscoveryReport(), | |
| loadModelsReport(), | |
| loadLastDiagnostics() | |
| ]); | |
| } | |
| // Load System Alerts | |
| async function loadSystemAlerts() { | |
| try { | |
| const response = await fetch('/api/diagnostics/last'); | |
| const report = await response.json(); | |
| if (report.message || !report.issues || report.issues.length === 0) { | |
| document.getElementById('systemAlertsSection').style.display = 'none'; | |
| return; | |
| } | |
| displaySystemAlerts(report.issues); | |
| document.getElementById('systemAlertsSection').style.display = 'block'; | |
| } catch (error) { | |
| console.error('Error loading system alerts:', error); | |
| } | |
| } | |
| function displaySystemAlerts(issues) { | |
| const container = document.getElementById('systemAlertsContainer'); | |
| if (!container) return; | |
| const severityConfig = { | |
| 'critical': { | |
| icon: 'icon-error', | |
| color: 'var(--accent-red)', | |
| bg: 'rgba(239, 68, 68, 0.1)', | |
| border: 'rgba(239, 68, 68, 0.3)' | |
| }, | |
| 'warning': { | |
| icon: 'icon-warning', | |
| color: 'var(--accent-yellow)', | |
| bg: 'rgba(245, 158, 11, 0.1)', | |
| border: 'rgba(245, 158, 11, 0.3)' | |
| }, | |
| 'info': { | |
| icon: 'icon-info', | |
| color: 'var(--accent-blue)', | |
| bg: 'rgba(59, 130, 246, 0.1)', | |
| border: 'rgba(59, 130, 246, 0.3)' | |
| } | |
| }; | |
| const solutions = { | |
| 'HF_API_TOKEN': { | |
| title: 'تنظیم متغیر محیطی HF_API_TOKEN', | |
| steps: [ | |
| '1. یک توکن از HuggingFace دریافت کنید:', | |
| ' - به https://huggingface.co/settings/tokens بروید', | |
| ' - یک توکن جدید ایجاد کنید', | |
| '2. توکن را به متغیر محیطی اضافه کنید:', | |
| ' Windows: set HF_API_TOKEN=your_token_here', | |
| ' Linux/Mac: export HF_API_TOKEN=your_token_here', | |
| ' یا در فایل .env: HF_API_TOKEN=your_token_here' | |
| ] | |
| }, | |
| 'resources.json': { | |
| title: 'ایجاد فایل resources.json', | |
| steps: [ | |
| 'این فایل به صورت خودکار ساخته میشود.', | |
| 'اگر نیاز به تنظیمات دستی دارید، میتوانید آن را ایجاد کنید:', | |
| '{', | |
| ' "resources": []', | |
| '}' | |
| ] | |
| }, | |
| 'config.json': { | |
| title: 'ایجاد فایل config.json', | |
| steps: [ | |
| 'این فایل به صورت خودکار ساخته میشود.', | |
| 'اگر نیاز به تنظیمات دستی دارید، میتوانید آن را ایجاد کنید.' | |
| ] | |
| }, | |
| 'HuggingFace API': { | |
| title: 'رفع مشکل اتصال به HuggingFace', | |
| steps: [ | |
| '1. بررسی اتصال اینترنت', | |
| '2. بررسی فایروال و پروکسی', | |
| '3. بررسی DNS settings', | |
| '4. اگر از VPN استفاده میکنید، آن را غیرفعال کنید', | |
| '5. بررسی کنید که https://api.huggingface.co قابل دسترسی باشد' | |
| ] | |
| }, | |
| 'Auto-Discovery': { | |
| title: 'فعالسازی Auto-Discovery Service', | |
| steps: [ | |
| 'برای فعالسازی سرویس Auto-Discovery:', | |
| '1. متغیر محیطی را تنظیم کنید:', | |
| ' export ENABLE_AUTO_DISCOVERY=true', | |
| '2. یا در فایل .env اضافه کنید:', | |
| ' ENABLE_AUTO_DISCOVERY=true', | |
| '3. سرور را restart کنید' | |
| ] | |
| } | |
| }; | |
| let html = ''; | |
| issues.forEach((issue, index) => { | |
| const config = severityConfig[issue.severity] || severityConfig['info']; | |
| const solutionKey = issue.title.includes('HF_API_TOKEN') ? 'HF_API_TOKEN' : | |
| issue.title.includes('resources.json') ? 'resources.json' : | |
| issue.title.includes('config.json') ? 'config.json' : | |
| issue.title.includes('HuggingFace') ? 'HuggingFace API' : | |
| issue.title.includes('Auto-Discovery') ? 'Auto-Discovery' : null; | |
| const solution = solutionKey ? solutions[solutionKey] : null; | |
| html += ` | |
| <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'); | |
| // Update system alerts | |
| 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); | |
| // Load system alerts | |
| if (report.issues && report.issues.length > 0) { | |
| displaySystemAlerts(report.issues); | |
| document.getElementById('systemAlertsSection').style.display = 'block'; | |
| } | |
| } catch (error) { | |
| console.error('Error loading last diagnostics:', error); | |
| } | |
| } | |
| 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> | |
| </html> |