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