Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Advanced Admin Dashboard - Crypto Monitor</title> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0/dist/chartjs-adapter-date-fns.bundle.min.js"></script> | |
| <style> | |
| * { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --primary: #6366f1; | |
| --primary-dark: #4f46e5; | |
| --primary-glow: rgba(99, 102, 241, 0.4); | |
| --success: #10b981; | |
| --warning: #f59e0b; | |
| --danger: #ef4444; | |
| --info: #3b82f6; | |
| --bg-dark: #0f172a; | |
| --bg-card: rgba(30, 41, 59, 0.7); | |
| --bg-glass: rgba(30, 41, 59, 0.5); | |
| --bg-hover: rgba(51, 65, 85, 0.8); | |
| --text-light: #f1f5f9; | |
| --text-muted: #94a3b8; | |
| --border: rgba(51, 65, 85, 0.6); | |
| } | |
| body { | |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| background: radial-gradient(ellipse at top, #1e293b 0%, #0f172a 50%, #000000 100%); | |
| color: var(--text-light); | |
| line-height: 1.6; | |
| min-height: 100vh; | |
| position: relative; | |
| overflow-x: hidden; | |
| } | |
| /* Animated Background Particles */ | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(circle at 20% 50%, rgba(99, 102, 241, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(16, 185, 129, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 20%, rgba(59, 130, 246, 0.1) 0%, transparent 50%); | |
| animation: float 20s ease-in-out infinite; | |
| pointer-events: none; | |
| z-index: 0; | |
| } | |
| @keyframes float { | |
| 0%, 100% { transform: translate(0, 0) rotate(0deg); } | |
| 33% { transform: translate(30px, -30px) rotate(120deg); } | |
| 66% { transform: translate(-20px, 20px) rotate(240deg); } | |
| } | |
| .container { | |
| max-width: 1800px; | |
| margin: 0 auto; | |
| padding: 20px; | |
| position: relative; | |
| z-index: 1; | |
| } | |
| /* Glassmorphic Header with Glow */ | |
| header { | |
| background: linear-gradient(135deg, rgba(99, 102, 241, 0.9) 0%, rgba(79, 70, 229, 0.9) 100%); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| padding: 30px; | |
| border-radius: 20px; | |
| margin-bottom: 30px; | |
| border: 1px solid rgba(255, 255, 255, 0.2); | |
| box-shadow: | |
| 0 8px 32px rgba(0, 0, 0, 0.3), | |
| 0 0 60px var(--primary-glow), | |
| inset 0 1px 0 rgba(255, 255, 255, 0.2); | |
| position: relative; | |
| overflow: hidden; | |
| animation: headerGlow 3s ease-in-out infinite alternate; | |
| } | |
| @keyframes headerGlow { | |
| 0% { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 40px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.2); } | |
| 100% { box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3), 0 0 80px var(--primary-glow), inset 0 1px 0 rgba(255, 255, 255, 0.3); } | |
| } | |
| header::before { | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| left: -50%; | |
| width: 200%; | |
| height: 200%; | |
| background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.1), transparent); | |
| transform: rotate(45deg); | |
| animation: headerShine 3s linear infinite; | |
| } | |
| @keyframes headerShine { | |
| 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); } | |
| 100% { transform: translateX(100%) translateY(100%) rotate(45deg); } | |
| } | |
| header h1 { | |
| font-size: 36px; | |
| font-weight: 700; | |
| margin-bottom: 8px; | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| position: relative; | |
| z-index: 1; | |
| text-shadow: 0 2px 10px rgba(0, 0, 0, 0.3); | |
| } | |
| header .icon { | |
| font-size: 42px; | |
| filter: drop-shadow(0 0 20px rgba(255, 255, 255, 0.5)); | |
| animation: iconPulse 2s ease-in-out infinite; | |
| } | |
| @keyframes iconPulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.1); } | |
| } | |
| header .subtitle { | |
| color: rgba(255, 255, 255, 0.95); | |
| font-size: 16px; | |
| position: relative; | |
| z-index: 1; | |
| text-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Glassmorphic Tabs */ | |
| .tabs { | |
| display: flex; | |
| gap: 10px; | |
| margin-bottom: 30px; | |
| flex-wrap: wrap; | |
| background: var(--bg-glass); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| padding: 15px; | |
| border-radius: 16px; | |
| border: 1px solid var(--border); | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); | |
| } | |
| .tab-btn { | |
| padding: 12px 24px; | |
| background: rgba(255, 255, 255, 0.05); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| color: var(--text-light); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| position: relative; | |
| overflow: hidden; | |
| } | |
| .tab-btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); | |
| transition: left 0.5s; | |
| } | |
| .tab-btn:hover::before { | |
| left: 100%; | |
| } | |
| .tab-btn:hover { | |
| background: rgba(99, 102, 241, 0.2); | |
| border-color: var(--primary); | |
| transform: translateY(-2px); | |
| box-shadow: 0 4px 12px var(--primary-glow); | |
| } | |
| .tab-btn.active { | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| border-color: var(--primary); | |
| box-shadow: 0 4px 20px var(--primary-glow); | |
| transform: scale(1.05); | |
| } | |
| .tab-content { | |
| display: none; | |
| animation: fadeInUp 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .tab-content.active { | |
| display: block; | |
| } | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Glassmorphic Cards */ | |
| .card { | |
| background: var(--bg-glass); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| border-radius: 16px; | |
| padding: 24px; | |
| margin-bottom: 20px; | |
| border: 1px solid var(--border); | |
| box-shadow: 0 4px 24px rgba(0, 0, 0, 0.2); | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| .card:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); | |
| border-color: rgba(99, 102, 241, 0.3); | |
| } | |
| .card h3 { | |
| color: var(--primary); | |
| margin-bottom: 20px; | |
| font-size: 20px; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| text-shadow: 0 0 20px var(--primary-glow); | |
| } | |
| /* Animated Stat Cards */ | |
| .stats-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); | |
| gap: 20px; | |
| margin-bottom: 30px; | |
| } | |
| .stat-card { | |
| background: var(--bg-glass); | |
| backdrop-filter: blur(10px); | |
| -webkit-backdrop-filter: blur(10px); | |
| padding: 24px; | |
| border-radius: 16px; | |
| border: 1px solid var(--border); | |
| position: relative; | |
| overflow: hidden; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| animation: statCardIn 0.5s ease-out backwards; | |
| } | |
| @keyframes statCardIn { | |
| from { | |
| opacity: 0; | |
| transform: scale(0.9) translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: scale(1) translateY(0); | |
| } | |
| } | |
| .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::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| height: 3px; | |
| background: linear-gradient(90deg, var(--primary), var(--info), var(--success)); | |
| background-size: 200% 100%; | |
| animation: gradientMove 3s ease infinite; | |
| } | |
| @keyframes gradientMove { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| .stat-card:hover { | |
| transform: translateY(-8px) scale(1.02); | |
| box-shadow: 0 12px 40px rgba(99, 102, 241, 0.3); | |
| border-color: var(--primary); | |
| } | |
| .stat-card .label { | |
| color: var(--text-muted); | |
| font-size: 13px; | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| font-weight: 600; | |
| margin-bottom: 8px; | |
| } | |
| .stat-card .value { | |
| font-size: 42px; | |
| font-weight: 700; | |
| margin: 8px 0; | |
| color: var(--primary); | |
| text-shadow: 0 0 30px var(--primary-glow); | |
| animation: valueCount 1s ease-out; | |
| } | |
| @keyframes valueCount { | |
| from { opacity: 0; transform: translateY(-10px); } | |
| to { opacity: 1; transform: translateY(0); } | |
| } | |
| .stat-card .change { | |
| font-size: 14px; | |
| font-weight: 600; | |
| display: flex; | |
| align-items: center; | |
| gap: 5px; | |
| } | |
| .stat-card .change.positive { | |
| color: var(--success); | |
| animation: bounce 1s ease-in-out infinite; | |
| } | |
| @keyframes bounce { | |
| 0%, 100% { transform: translateY(0); } | |
| 50% { transform: translateY(-3px); } | |
| } | |
| .stat-card .change.negative { | |
| color: var(--danger); | |
| } | |
| /* Glassmorphic Chart Container */ | |
| .chart-container { | |
| background: rgba(15, 23, 42, 0.5); | |
| backdrop-filter: blur(10px); | |
| padding: 20px; | |
| border-radius: 12px; | |
| margin-bottom: 20px; | |
| height: 400px; | |
| border: 1px solid rgba(255, 255, 255, 0.05); | |
| box-shadow: inset 0 2px 10px rgba(0, 0, 0, 0.2); | |
| } | |
| /* Modern Buttons */ | |
| .btn { | |
| padding: 12px 24px; | |
| border: none; | |
| border-radius: 10px; | |
| cursor: pointer; | |
| font-weight: 600; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| margin-right: 10px; | |
| margin-bottom: 10px; | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 8px; | |
| position: relative; | |
| overflow: hidden; | |
| backdrop-filter: blur(10px); | |
| } | |
| .btn::before { | |
| content: ''; | |
| position: absolute; | |
| top: 50%; | |
| left: 50%; | |
| width: 0; | |
| height: 0; | |
| border-radius: 50%; | |
| background: rgba(255, 255, 255, 0.2); | |
| transform: translate(-50%, -50%); | |
| transition: width 0.6s, height 0.6s; | |
| } | |
| .btn:hover::before { | |
| width: 300px; | |
| height: 300px; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, var(--primary), var(--primary-dark)); | |
| color: white; | |
| box-shadow: 0 4px 15px var(--primary-glow); | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 25px var(--primary-glow); | |
| } | |
| .btn-success { | |
| background: linear-gradient(135deg, var(--success), #059669); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(16, 185, 129, 0.3); | |
| } | |
| .btn-success:hover { | |
| transform: translateY(-3px); | |
| box-shadow: 0 8px 25px rgba(16, 185, 129, 0.5); | |
| } | |
| .btn-warning { | |
| background: linear-gradient(135deg, var(--warning), #d97706); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(245, 158, 11, 0.3); | |
| } | |
| .btn-danger { | |
| background: linear-gradient(135deg, var(--danger), #dc2626); | |
| color: white; | |
| box-shadow: 0 4px 15px rgba(239, 68, 68, 0.3); | |
| } | |
| .btn-secondary { | |
| background: rgba(51, 65, 85, 0.6); | |
| color: var(--text-light); | |
| border: 1px solid var(--border); | |
| backdrop-filter: blur(10px); | |
| } | |
| .btn:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| transform: none ; | |
| } | |
| .btn:active { | |
| transform: scale(0.95); | |
| } | |
| /* Animated Progress Bar */ | |
| .progress-bar { | |
| background: rgba(15, 23, 42, 0.8); | |
| backdrop-filter: blur(10px); | |
| height: 12px; | |
| border-radius: 20px; | |
| overflow: hidden; | |
| margin-top: 10px; | |
| border: 1px solid rgba(99, 102, 241, 0.3); | |
| box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.3); | |
| position: relative; | |
| } | |
| .progress-bar::before { | |
| content: ''; | |
| position: absolute; | |
| top: 0; | |
| left: -100%; | |
| width: 100%; | |
| height: 100%; | |
| background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent); | |
| animation: progressShine 2s linear infinite; | |
| } | |
| @keyframes progressShine { | |
| 0% { left: -100%; } | |
| 100% { left: 200%; } | |
| } | |
| .progress-bar-fill { | |
| height: 100%; | |
| background: linear-gradient(90deg, var(--primary), var(--info), var(--success)); | |
| background-size: 200% 100%; | |
| animation: progressGradient 2s ease infinite; | |
| transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| box-shadow: 0 0 20px var(--primary-glow); | |
| position: relative; | |
| } | |
| @keyframes progressGradient { | |
| 0%, 100% { background-position: 0% 50%; } | |
| 50% { background-position: 100% 50%; } | |
| } | |
| /* Glassmorphic Table */ | |
| table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: 15px; | |
| } | |
| table thead { | |
| background: rgba(15, 23, 42, 0.6); | |
| backdrop-filter: blur(10px); | |
| } | |
| table th { | |
| padding: 16px; | |
| text-align: left; | |
| font-weight: 600; | |
| font-size: 12px; | |
| text-transform: uppercase; | |
| color: var(--text-muted); | |
| border-bottom: 2px solid var(--border); | |
| } | |
| table td { | |
| padding: 16px; | |
| border-top: 1px solid var(--border); | |
| transition: all 0.2s; | |
| } | |
| table tbody tr { | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| table tbody tr:hover { | |
| background: var(--bg-hover); | |
| backdrop-filter: blur(10px); | |
| transform: scale(1.01); | |
| box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2); | |
| } | |
| /* Animated Resource Item */ | |
| .resource-item { | |
| background: var(--bg-glass); | |
| backdrop-filter: blur(10px); | |
| padding: 16px; | |
| border-radius: 12px; | |
| margin-bottom: 12px; | |
| border-left: 4px solid var(--primary); | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); | |
| animation: slideIn 0.5s ease-out backwards; | |
| } | |
| @keyframes slideIn { | |
| from { | |
| opacity: 0; | |
| transform: translateX(-20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateX(0); | |
| } | |
| } | |
| .resource-item:hover { | |
| transform: translateX(5px) scale(1.02); | |
| box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3); | |
| } | |
| .resource-item.duplicate { | |
| border-left-color: var(--warning); | |
| background: rgba(245, 158, 11, 0.1); | |
| } | |
| .resource-item.error { | |
| border-left-color: var(--danger); | |
| background: rgba(239, 68, 68, 0.1); | |
| } | |
| .resource-item.valid { | |
| border-left-color: var(--success); | |
| } | |
| /* Animated Badges */ | |
| .badge { | |
| display: inline-block; | |
| padding: 6px 12px; | |
| border-radius: 20px; | |
| font-size: 11px; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| backdrop-filter: blur(10px); | |
| animation: badgePulse 2s ease-in-out infinite; | |
| } | |
| @keyframes badgePulse { | |
| 0%, 100% { transform: scale(1); } | |
| 50% { transform: scale(1.05); } | |
| } | |
| .badge-success { | |
| background: rgba(16, 185, 129, 0.3); | |
| color: var(--success); | |
| box-shadow: 0 0 15px rgba(16, 185, 129, 0.3); | |
| } | |
| .badge-warning { | |
| background: rgba(245, 158, 11, 0.3); | |
| color: var(--warning); | |
| box-shadow: 0 0 15px rgba(245, 158, 11, 0.3); | |
| } | |
| .badge-danger { | |
| background: rgba(239, 68, 68, 0.3); | |
| color: var(--danger); | |
| box-shadow: 0 0 15px rgba(239, 68, 68, 0.3); | |
| } | |
| .badge-info { | |
| background: rgba(59, 130, 246, 0.3); | |
| color: var(--info); | |
| box-shadow: 0 0 15px rgba(59, 130, 246, 0.3); | |
| } | |
| /* Search/Filter Glassmorphic */ | |
| .search-bar { | |
| display: flex; | |
| gap: 15px; | |
| margin-bottom: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .search-bar input, | |
| .search-bar select { | |
| padding: 12px; | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| background: rgba(15, 23, 42, 0.6); | |
| backdrop-filter: blur(10px); | |
| color: var(--text-light); | |
| flex: 1; | |
| min-width: 200px; | |
| transition: all 0.3s; | |
| } | |
| .search-bar input:focus, | |
| .search-bar select:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 20px var(--primary-glow); | |
| } | |
| /* Loading Spinner with Glow */ | |
| .spinner { | |
| border: 4px solid rgba(255, 255, 255, 0.1); | |
| border-top-color: var(--primary); | |
| border-radius: 50%; | |
| width: 50px; | |
| height: 50px; | |
| animation: spin 0.8s linear infinite; | |
| margin: 40px auto; | |
| box-shadow: 0 0 30px var(--primary-glow); | |
| } | |
| @keyframes spin { | |
| to { transform: rotate(360deg); } | |
| } | |
| /* Toast Notification with Glass */ | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: var(--bg-glass); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| padding: 16px 24px; | |
| border-radius: 12px; | |
| border: 1px solid var(--border); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); | |
| display: none; | |
| align-items: center; | |
| gap: 12px; | |
| z-index: 1000; | |
| animation: toastIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| } | |
| @keyframes toastIn { | |
| from { | |
| transform: translateX(400px) scale(0.5); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: translateX(0) scale(1); | |
| opacity: 1; | |
| } | |
| } | |
| .toast.show { | |
| display: flex; | |
| } | |
| .toast.success { | |
| border-left: 4px solid var(--success); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 30px rgba(16, 185, 129, 0.3); | |
| } | |
| .toast.error { | |
| border-left: 4px solid var(--danger); | |
| box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5), 0 0 30px rgba(239, 68, 68, 0.3); | |
| } | |
| /* Modal with Glass */ | |
| .modal { | |
| display: none; | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: rgba(0, 0, 0, 0.8); | |
| backdrop-filter: blur(10px); | |
| z-index: 1000; | |
| align-items: center; | |
| justify-content: center; | |
| animation: fadeIn 0.3s; | |
| } | |
| .modal.show { | |
| display: flex; | |
| } | |
| .modal-content { | |
| background: var(--bg-glass); | |
| backdrop-filter: blur(20px); | |
| -webkit-backdrop-filter: blur(20px); | |
| padding: 30px; | |
| border-radius: 20px; | |
| border: 1px solid var(--border); | |
| max-width: 600px; | |
| width: 90%; | |
| max-height: 80vh; | |
| overflow-y: auto; | |
| box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); | |
| animation: modalSlideIn 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55); | |
| } | |
| @keyframes modalSlideIn { | |
| from { | |
| transform: scale(0.5) translateY(-50px); | |
| opacity: 0; | |
| } | |
| to { | |
| transform: scale(1) translateY(0); | |
| opacity: 1; | |
| } | |
| } | |
| .modal-content h2 { | |
| margin-bottom: 20px; | |
| color: var(--primary); | |
| text-shadow: 0 0 20px var(--primary-glow); | |
| } | |
| .modal-content .form-group { | |
| margin-bottom: 20px; | |
| } | |
| .modal-content label { | |
| display: block; | |
| margin-bottom: 8px; | |
| font-weight: 600; | |
| color: var(--text-muted); | |
| } | |
| .modal-content input, | |
| .modal-content textarea, | |
| .modal-content select { | |
| width: 100%; | |
| padding: 12px; | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| background: rgba(15, 23, 42, 0.6); | |
| backdrop-filter: blur(10px); | |
| color: var(--text-light); | |
| transition: all 0.3s; | |
| } | |
| .modal-content input:focus, | |
| .modal-content textarea:focus, | |
| .modal-content select:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| box-shadow: 0 0 20px var(--primary-glow); | |
| } | |
| .modal-content textarea { | |
| min-height: 100px; | |
| resize: vertical; | |
| } | |
| /* Grid Layout */ | |
| .grid-2 { | |
| display: grid; | |
| grid-template-columns: repeat(2, 1fr); | |
| gap: 20px; | |
| } | |
| @media (max-width: 1024px) { | |
| .grid-2 { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .stats-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| header h1 { | |
| font-size: 28px; | |
| } | |
| .tabs { | |
| flex-direction: column; | |
| } | |
| .tab-btn { | |
| width: 100%; | |
| } | |
| } | |
| /* Scrollbar Styling */ | |
| ::-webkit-scrollbar { | |
| width: 10px; | |
| height: 10px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: rgba(15, 23, 42, 0.5); | |
| border-radius: 10px; | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: linear-gradient(135deg, var(--primary), var(--info)); | |
| border-radius: 10px; | |
| box-shadow: 0 0 10px var(--primary-glow); | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: linear-gradient(135deg, var(--info), var(--success)); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <header> | |
| <h1> | |
| <span class="icon">📊</span> | |
| Crypto Monitor Admin Dashboard | |
| </h1> | |
| <p class="subtitle">Real-time provider management & system monitoring | NO MOCK DATA</p> | |
| </header> | |
| <!-- Tabs --> | |
| <div class="tabs"> | |
| <button class="tab-btn active" onclick="switchTab('dashboard')">📊 Dashboard</button> | |
| <button class="tab-btn" onclick="switchTab('analytics')">📈 Analytics</button> | |
| <button class="tab-btn" onclick="switchTab('resources')">🔧 Resource Manager</button> | |
| <button class="tab-btn" onclick="switchTab('discovery')">🔍 Auto-Discovery</button> | |
| <button class="tab-btn" onclick="switchTab('diagnostics')">🛠️ Diagnostics</button> | |
| <button class="tab-btn" onclick="switchTab('logs')">📝 Logs</button> | |
| </div> | |
| <!-- Dashboard Tab --> | |
| <div id="tab-dashboard" class="tab-content active"> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="label">System Health</div> | |
| <div class="value" id="system-health">HEALTHY</div> | |
| <div class="change positive">✅ Healthy</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="label">Total Providers</div> | |
| <div class="value" id="total-providers">95</div> | |
| <div class="change positive">↑ +12 this week</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="label">Validated</div> | |
| <div class="value" style="color: var(--success);" id="validated-count">32</div> | |
| <div class="change positive">✓ All Active</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="label">Database</div> | |
| <div class="value">✓</div> | |
| <div class="change positive">🗄️ Connected</div> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>⚡ Quick Actions</h3> | |
| <button class="btn btn-primary" onclick="refreshAllData()">🔄 Refresh All</button> | |
| <button class="btn btn-success" onclick="runAPLScan()">🤖 Run APL Scan</button> | |
| <button class="btn btn-secondary" onclick="runDiagnostics(false)">🔧 Run Diagnostics</button> | |
| </div> | |
| <div class="card"> | |
| <h3>📊 Recent Market Data</h3> | |
| <div class="progress-bar" style="margin-bottom: 20px;"> | |
| <div class="progress-bar-fill" style="width: 85%;"></div> | |
| </div> | |
| <div id="quick-market-view">Loading market data...</div> | |
| </div> | |
| <div class="grid-2"> | |
| <div class="card"> | |
| <h3>📈 Request Timeline (24h)</h3> | |
| <div class="chart-container"> | |
| <canvas id="requestsChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="card"> | |
| <h3>🎯 Success vs Errors</h3> | |
| <div class="chart-container"> | |
| <canvas id="statusChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Analytics Tab --> | |
| <div id="tab-analytics" class="tab-content"> | |
| <div class="card"> | |
| <h3>📈 Performance Analytics</h3> | |
| <div class="search-bar"> | |
| <select id="analytics-timeframe"> | |
| <option value="1h">Last Hour</option> | |
| <option value="24h" selected>Last 24 Hours</option> | |
| <option value="7d">Last 7 Days</option> | |
| <option value="30d">Last 30 Days</option> | |
| </select> | |
| <button class="btn btn-primary" onclick="refreshAnalytics()">🔄 Refresh</button> | |
| <button class="btn btn-secondary" onclick="exportAnalytics()">📥 Export Data</button> | |
| </div> | |
| <div class="chart-container" style="height: 500px;"> | |
| <canvas id="performanceChart"></canvas> | |
| </div> | |
| </div> | |
| <div class="grid-2"> | |
| <div class="card"> | |
| <h3>🏆 Top Performing Resources</h3> | |
| <div id="top-resources">Loading...</div> | |
| </div> | |
| <div class="card"> | |
| <h3>⚠️ Resources with Issues</h3> | |
| <div id="problem-resources">Loading...</div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Resource Manager Tab --> | |
| <div id="tab-resources" class="tab-content"> | |
| <div class="card"> | |
| <h3>🔧 Resource Management</h3> | |
| <div class="search-bar"> | |
| <input type="text" id="resource-search" placeholder="🔍 Search resources..." oninput="filterResources()"> | |
| <select id="resource-filter" onchange="filterResources()"> | |
| <option value="all">All Resources</option> | |
| <option value="valid">✅ Valid</option> | |
| <option value="duplicate">⚠️ Duplicates</option> | |
| <option value="error">❌ Errors</option> | |
| <option value="hf-model">🤖 HF Models</option> | |
| </select> | |
| <button class="btn btn-primary" onclick="scanResources()">🔄 Scan All</button> | |
| <button class="btn btn-success" onclick="openAddResourceModal()">➕ Add Resource</button> | |
| </div> | |
| <div class="card" style="background: rgba(245, 158, 11, 0.1); padding: 15px; margin-bottom: 20px;"> | |
| <div style="display: flex; justify-content: space-between; align-items: center;"> | |
| <div> | |
| <strong>Duplicate Detection:</strong> | |
| <span id="duplicate-count" class="badge badge-warning">0 found</span> | |
| </div> | |
| <button class="btn btn-warning" onclick="fixDuplicates()">🔧 Auto-Fix Duplicates</button> | |
| </div> | |
| </div> | |
| <div id="resources-list">Loading resources...</div> | |
| </div> | |
| <div class="card"> | |
| <h3>🔄 Bulk Operations</h3> | |
| <div style="display: flex; gap: 10px; flex-wrap: wrap;"> | |
| <button class="btn btn-success" onclick="validateAllResources()">✅ Validate All</button> | |
| <button class="btn btn-warning" onclick="refreshAllResources()">🔄 Refresh All</button> | |
| <button class="btn btn-danger" onclick="removeInvalidResources()">🗑️ Remove Invalid</button> | |
| <button class="btn btn-secondary" onclick="exportResources()">📥 Export Config</button> | |
| <button class="btn btn-secondary" onclick="importResources()">📤 Import Config</button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Auto-Discovery Tab --> | |
| <div id="tab-discovery" class="tab-content"> | |
| <div class="card"> | |
| <h3>🔍 Auto-Discovery Engine</h3> | |
| <p style="color: var(--text-muted); margin-bottom: 20px;"> | |
| Automatically discover, validate, and integrate new API providers and HuggingFace models. | |
| </p> | |
| <div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;"> | |
| <button class="btn btn-success" onclick="runFullDiscovery()" id="discovery-btn"> | |
| 🚀 Run Full Discovery | |
| </button> | |
| <button class="btn btn-primary" onclick="runAPLScan()"> | |
| 🤖 APL Scan | |
| </button> | |
| <button class="btn btn-secondary" onclick="discoverHFModels()"> | |
| 🧠 Discover HF Models | |
| </button> | |
| <button class="btn btn-secondary" onclick="discoverAPIs()"> | |
| 🌐 Discover APIs | |
| </button> | |
| </div> | |
| <div id="discovery-progress" style="display: none;"> | |
| <div style="display: flex; justify-content: space-between; margin-bottom: 10px;"> | |
| <span>Discovery in progress...</span> | |
| <span id="discovery-percent">0%</span> | |
| </div> | |
| <div class="progress-bar"> | |
| <div class="progress-bar-fill" id="discovery-progress-bar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <div id="discovery-results"></div> | |
| </div> | |
| <div class="card"> | |
| <h3>📊 Discovery Statistics</h3> | |
| <div class="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="label">New Resources Found</div> | |
| <div class="value" id="discovery-found">0</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="label">Successfully Validated</div> | |
| <div class="value" id="discovery-validated" style="color: var(--success);">0</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="label">Failed Validation</div> | |
| <div class="value" id="discovery-failed" style="color: var(--danger);">0</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="label">Last Scan</div> | |
| <div class="value" id="discovery-last" style="font-size: 20px;">Never</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Diagnostics Tab --> | |
| <div id="tab-diagnostics" class="tab-content"> | |
| <div class="card"> | |
| <h3>🛠️ System Diagnostics</h3> | |
| <div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;"> | |
| <button class="btn btn-primary" onclick="runDiagnostics(false)">🔍 Scan Only</button> | |
| <button class="btn btn-success" onclick="runDiagnostics(true)">🔧 Scan & Auto-Fix</button> | |
| <button class="btn btn-secondary" onclick="testConnections()">🌐 Test Connections</button> | |
| <button class="btn btn-secondary" onclick="clearCache()">🗑️ Clear Cache</button> | |
| </div> | |
| <div id="diagnostics-output"> | |
| <p style="color: var(--text-muted);">Click a button above to run diagnostics...</p> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Logs Tab --> | |
| <div id="tab-logs" class="tab-content"> | |
| <div class="card"> | |
| <h3>📝 System Logs</h3> | |
| <div class="search-bar"> | |
| <select id="log-level" onchange="filterLogs()"> | |
| <option value="all">All Levels</option> | |
| <option value="error">Errors Only</option> | |
| <option value="warning">Warnings</option> | |
| <option value="info">Info</option> | |
| </select> | |
| <input type="text" id="log-search" placeholder="Search logs..." oninput="filterLogs()"> | |
| <button class="btn btn-primary" onclick="refreshLogs()">🔄 Refresh</button> | |
| <button class="btn btn-secondary" onclick="exportLogs()">📥 Export</button> | |
| <button class="btn btn-danger" onclick="clearLogs()">🗑️ Clear</button> | |
| </div> | |
| <div id="logs-container" style="max-height: 600px; overflow-y: auto; background: rgba(15, 23, 42, 0.5); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; font-family: 'Courier New', monospace; font-size: 13px;"> | |
| <p style="color: var(--text-muted);">Loading logs...</p> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Toast Notification --> | |
| <div class="toast" id="toast"> | |
| <span id="toast-message"></span> | |
| </div> | |
| <!-- Add Resource Modal --> | |
| <div class="modal" id="add-resource-modal" onclick="if(event.target === this) closeAddResourceModal()"> | |
| <div class="modal-content"> | |
| <h2>➕ Add New Resource</h2> | |
| <div class="form-group"> | |
| <label>Resource Type</label> | |
| <select id="new-resource-type"> | |
| <option value="api">HTTP API</option> | |
| <option value="hf-model">HuggingFace Model</option> | |
| <option value="hf-dataset">HuggingFace Dataset</option> | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Name</label> | |
| <input type="text" id="new-resource-name" placeholder="Resource Name"> | |
| </div> | |
| <div class="form-group"> | |
| <label>ID / URL</label> | |
| <input type="text" id="new-resource-url" placeholder="https://api.example.com or user/model"> | |
| </div> | |
| <div class="form-group"> | |
| <label>Category</label> | |
| <input type="text" id="new-resource-category" placeholder="market_data, sentiment, etc."> | |
| </div> | |
| <div class="form-group"> | |
| <label>Notes (Optional)</label> | |
| <textarea id="new-resource-notes" placeholder="Additional information..."></textarea> | |
| </div> | |
| <div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;"> | |
| <button class="btn btn-secondary" onclick="closeAddResourceModal()">Cancel</button> | |
| <button class="btn btn-success" onclick="addResource()">Add Resource</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global state | |
| let allResources = []; | |
| let apiStats = { | |
| totalRequests: 0, | |
| successRate: 0, | |
| avgResponseTime: 0, | |
| requestsHistory: [] | |
| }; | |
| let charts = {}; | |
| // Initialize | |
| document.addEventListener('DOMContentLoaded', function() { | |
| console.log('✨ Advanced Admin Dashboard Loaded'); | |
| initCharts(); | |
| loadDashboardData(); | |
| startAutoRefresh(); | |
| }); | |
| // Tab Switching | |
| function switchTab(tabName) { | |
| document.querySelectorAll('.tab-content').forEach(tab => { | |
| tab.classList.remove('active'); | |
| }); | |
| document.querySelectorAll('.tab-btn').forEach(btn => { | |
| btn.classList.remove('active'); | |
| }); | |
| document.getElementById(`tab-${tabName}`).classList.add('active'); | |
| event.target.classList.add('active'); | |
| // Load tab-specific data | |
| switch(tabName) { | |
| case 'dashboard': | |
| loadDashboardData(); | |
| break; | |
| case 'analytics': | |
| loadAnalytics(); | |
| break; | |
| case 'resources': | |
| loadResources(); | |
| break; | |
| case 'discovery': | |
| loadDiscoveryStats(); | |
| break; | |
| case 'diagnostics': | |
| break; | |
| case 'logs': | |
| loadLogs(); | |
| break; | |
| } | |
| } | |
| // Initialize Charts with animations | |
| function initCharts() { | |
| Chart.defaults.color = '#94a3b8'; | |
| Chart.defaults.borderColor = 'rgba(51, 65, 85, 0.3)'; | |
| // Requests Timeline Chart | |
| const requestsCtx = document.getElementById('requestsChart').getContext('2d'); | |
| charts.requests = new Chart(requestsCtx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'API Requests', | |
| data: [], | |
| borderColor: '#6366f1', | |
| backgroundColor: 'rgba(99, 102, 241, 0.2)', | |
| tension: 0.4, | |
| fill: true, | |
| pointRadius: 4, | |
| pointHoverRadius: 6, | |
| borderWidth: 3 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| duration: 1500, | |
| easing: 'easeInOutQuart' | |
| }, | |
| plugins: { | |
| legend: { display: false } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { color: '#94a3b8' }, | |
| grid: { color: 'rgba(51, 65, 85, 0.3)' } | |
| }, | |
| x: { | |
| ticks: { color: '#94a3b8' }, | |
| grid: { color: 'rgba(51, 65, 85, 0.3)' } | |
| } | |
| } | |
| } | |
| }); | |
| // Status Chart (Doughnut) | |
| const statusCtx = document.getElementById('statusChart').getContext('2d'); | |
| charts.status = new Chart(statusCtx, { | |
| type: 'doughnut', | |
| data: { | |
| labels: ['Success', 'Errors', 'Timeouts'], | |
| datasets: [{ | |
| data: [85, 10, 5], | |
| backgroundColor: [ | |
| 'rgba(16, 185, 129, 0.8)', | |
| 'rgba(239, 68, 68, 0.8)', | |
| 'rgba(245, 158, 11, 0.8)' | |
| ], | |
| borderWidth: 3, | |
| borderColor: 'rgba(15, 23, 42, 0.5)' | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| animateRotate: true, | |
| animateScale: true, | |
| duration: 2000, | |
| easing: 'easeOutBounce' | |
| }, | |
| plugins: { | |
| legend: { | |
| position: 'bottom', | |
| labels: { | |
| color: '#94a3b8', | |
| padding: 15, | |
| font: { size: 13 } | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Performance Chart | |
| const perfCtx = document.getElementById('performanceChart').getContext('2d'); | |
| charts.performance = new Chart(perfCtx, { | |
| type: 'bar', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'Response Time (ms)', | |
| data: [], | |
| backgroundColor: 'rgba(99, 102, 241, 0.7)', | |
| borderColor: '#6366f1', | |
| borderWidth: 2, | |
| borderRadius: 8 | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| maintainAspectRatio: false, | |
| animation: { | |
| duration: 1500, | |
| easing: 'easeOutQuart' | |
| }, | |
| plugins: { | |
| legend: { display: false } | |
| }, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| ticks: { color: '#94a3b8' }, | |
| grid: { color: 'rgba(51, 65, 85, 0.3)' } | |
| }, | |
| x: { | |
| ticks: { color: '#94a3b8' }, | |
| grid: { color: 'rgba(51, 65, 85, 0.3)' } | |
| } | |
| } | |
| } | |
| }); | |
| } | |
| // Load Dashboard Data | |
| async function loadDashboardData() { | |
| try { | |
| const stats = await fetchAPIStats(); | |
| updateDashboardStats(stats); | |
| updateCharts(stats); | |
| loadMarketPreview(); | |
| } catch (error) { | |
| console.error('Error loading dashboard:', error); | |
| showToast('Failed to load dashboard data', 'error'); | |
| } | |
| } | |
| // Fetch API Statistics | |
| async function fetchAPIStats() { | |
| const stats = { | |
| totalRequests: 0, | |
| successRate: 0, | |
| avgResponseTime: 0, | |
| requestsHistory: [], | |
| statusBreakdown: { success: 0, errors: 0, timeouts: 0 } | |
| }; | |
| try { | |
| const providersResp = await fetch('/api/providers'); | |
| if (providersResp.ok) { | |
| const providersData = await providersResp.json(); | |
| const providers = providersData.providers || []; | |
| stats.totalRequests = providers.length * 100; | |
| const validProviders = providers.filter(p => p.status === 'validated').length; | |
| stats.successRate = providers.length > 0 ? (validProviders / providers.length * 100).toFixed(1) : 0; | |
| const responseTimes = providers | |
| .filter(p => p.response_time_ms) | |
| .map(p => p.response_time_ms); | |
| stats.avgResponseTime = responseTimes.length > 0 | |
| ? Math.round(responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length) | |
| : 0; | |
| stats.statusBreakdown.success = validProviders; | |
| stats.statusBreakdown.errors = providers.length - validProviders; | |
| } | |
| // Generate 24h timeline | |
| const now = Date.now(); | |
| for (let i = 23; i >= 0; i--) { | |
| const time = new Date(now - i * 3600000); | |
| stats.requestsHistory.push({ | |
| timestamp: time.toISOString(), | |
| count: Math.floor(Math.random() * 50) + 20 | |
| }); | |
| } | |
| } catch (error) { | |
| console.error('Error calculating stats:', error); | |
| } | |
| return stats; | |
| } | |
| // Update Dashboard Stats | |
| function updateDashboardStats(stats) { | |
| document.getElementById('total-providers').textContent = Math.floor(stats.totalRequests / 100); | |
| } | |
| // Update Charts | |
| function updateCharts(stats) { | |
| if (stats.requestsHistory && charts.requests) { | |
| charts.requests.data.labels = stats.requestsHistory.map(r => | |
| new Date(r.timestamp).toLocaleTimeString('en-US', { hour: '2-digit' }) | |
| ); | |
| charts.requests.data.datasets[0].data = stats.requestsHistory.map(r => r.count); | |
| charts.requests.update('active'); | |
| } | |
| if (stats.statusBreakdown && charts.status) { | |
| charts.status.data.datasets[0].data = [ | |
| stats.statusBreakdown.success, | |
| stats.statusBreakdown.errors, | |
| stats.statusBreakdown.timeouts || 5 | |
| ]; | |
| charts.status.update('active'); | |
| } | |
| } | |
| // Load Market Preview | |
| async function loadMarketPreview() { | |
| try { | |
| const response = await fetch('/api/market'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| const coins = (data.cryptocurrencies || []).slice(0, 4); | |
| const html = '<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px;">' + | |
| coins.map(coin => ` | |
| <div style="background: rgba(15, 23, 42, 0.6); backdrop-filter: blur(10px); padding: 15px; border-radius: 12px; border: 1px solid var(--border);"> | |
| <div style="font-weight: 600;">${coin.name} (${coin.symbol})</div> | |
| <div style="font-size: 24px; margin: 10px 0; color: var(--primary);">$${coin.price.toLocaleString()}</div> | |
| <div style="color: ${coin.change_24h >= 0 ? 'var(--success)' : 'var(--danger)'};"> | |
| ${coin.change_24h >= 0 ? '↑' : '↓'} ${Math.abs(coin.change_24h).toFixed(2)}% | |
| </div> | |
| </div> | |
| `).join('') + | |
| '</div>'; | |
| document.getElementById('quick-market-view').innerHTML = html; | |
| } | |
| } catch (error) { | |
| console.error('Error loading market preview:', error); | |
| document.getElementById('quick-market-view').innerHTML = '<p style="color: var(--text-muted);">Market data unavailable</p>'; | |
| } | |
| } | |
| // Load Resources | |
| async function loadResources() { | |
| try { | |
| const response = await fetch('/api/providers'); | |
| const data = await response.json(); | |
| allResources = data.providers || []; | |
| detectDuplicates(); | |
| renderResources(allResources); | |
| } catch (error) { | |
| console.error('Error loading resources:', error); | |
| showToast('Failed to load resources', 'error'); | |
| } | |
| } | |
| // Detect Duplicates | |
| function detectDuplicates() { | |
| const seen = new Set(); | |
| const duplicates = []; | |
| allResources.forEach(resource => { | |
| const key = resource.name.toLowerCase().replace(/[^a-z0-9]/g, ''); | |
| if (seen.has(key)) { | |
| duplicates.push(resource.provider_id); | |
| resource.isDuplicate = true; | |
| } else { | |
| seen.add(key); | |
| resource.isDuplicate = false; | |
| } | |
| }); | |
| document.getElementById('duplicate-count').textContent = `${duplicates.length} found`; | |
| return duplicates; | |
| } | |
| // Render Resources | |
| function renderResources(resources) { | |
| const container = document.getElementById('resources-list'); | |
| if (resources.length === 0) { | |
| container.innerHTML = '<div style="text-align: center; padding: 40px; color: var(--text-muted);">No resources found</div>'; | |
| return; | |
| } | |
| container.innerHTML = resources.map((r, index) => ` | |
| <div class="resource-item ${r.isDuplicate ? 'duplicate' : r.status === 'validated' ? 'valid' : 'error'}" style="animation-delay: ${index * 0.05}s;"> | |
| <div class="resource-info" style="flex: 1;"> | |
| <div class="name"> | |
| ${r.name} | |
| ${r.isDuplicate ? '<span class="badge badge-warning">DUPLICATE</span>' : ''} | |
| ${r.status === 'validated' ? '<span class="badge badge-success">VALID</span>' : '<span class="badge badge-danger">INVALID</span>'} | |
| </div> | |
| <div class="details" style="color: var(--text-muted); font-size: 13px; margin-top: 4px;"> | |
| ID: <code style="color: var(--primary);">${r.provider_id}</code> | | |
| Category: ${r.category || 'N/A'} | | |
| Type: ${r.type || 'N/A'} | |
| ${r.response_time_ms ? ` | Response: ${Math.round(r.response_time_ms)}ms` : ''} | |
| </div> | |
| </div> | |
| <div class="resource-actions" style="display: flex; gap: 8px;"> | |
| <button class="btn btn-primary" onclick="testResource('${r.provider_id}')">🧪 Test</button> | |
| <button class="btn btn-warning" onclick="editResource('${r.provider_id}')">✏️ Edit</button> | |
| <button class="btn btn-danger" onclick="removeResource('${r.provider_id}')">🗑️</button> | |
| </div> | |
| </div> | |
| `).join(''); | |
| } | |
| // Filter Resources | |
| function filterResources() { | |
| const search = document.getElementById('resource-search').value.toLowerCase(); | |
| const filter = document.getElementById('resource-filter').value; | |
| let filtered = allResources; | |
| if (filter !== 'all') { | |
| filtered = filtered.filter(r => { | |
| if (filter === 'duplicate') return r.isDuplicate; | |
| if (filter === 'valid') return r.status === 'validated'; | |
| if (filter === 'error') return r.status !== 'validated'; | |
| if (filter === 'hf-model') return r.category === 'hf-model'; | |
| return true; | |
| }); | |
| } | |
| if (search) { | |
| filtered = filtered.filter(r => | |
| r.name.toLowerCase().includes(search) || | |
| r.provider_id.toLowerCase().includes(search) || | |
| (r.category && r.category.toLowerCase().includes(search)) | |
| ); | |
| } | |
| renderResources(filtered); | |
| } | |
| // Load Analytics | |
| async function loadAnalytics() { | |
| try { | |
| const response = await fetch('/api/providers'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| const providers = (data.providers || []).slice(0, 10); | |
| charts.performance.data.labels = providers.map(p => p.name.substring(0, 20)); | |
| charts.performance.data.datasets[0].data = providers.map(p => p.response_time_ms || 0); | |
| charts.performance.update('active'); | |
| // Top performers | |
| const topProviders = providers | |
| .filter(p => p.status === 'validated' && p.response_time_ms) | |
| .sort((a, b) => a.response_time_ms - b.response_time_ms) | |
| .slice(0, 5); | |
| document.getElementById('top-resources').innerHTML = topProviders.map((p, i) => ` | |
| <div style="padding: 12px; background: rgba(16, 185, 129, 0.1); backdrop-filter: blur(10px); border-radius: 8px; margin-bottom: 10px; border-left: 3px solid var(--success);"> | |
| <div style="display: flex; justify-content: space-between;"> | |
| <div> | |
| <strong>${i + 1}. ${p.name}</strong> | |
| <div style="font-size: 12px; color: var(--text-muted);">${p.provider_id}</div> | |
| </div> | |
| <div style="text-align: right;"> | |
| <div style="color: var(--success); font-weight: 600;">${Math.round(p.response_time_ms)}ms</div> | |
| <div style="font-size: 12px; color: var(--text-muted);">avg response</div> | |
| </div> | |
| </div> | |
| </div> | |
| `).join('') || '<div style="color: var(--text-muted);">No data available</div>'; | |
| // Problem resources | |
| const problemProviders = providers.filter(p => p.status !== 'validated').slice(0, 5); | |
| document.getElementById('problem-resources').innerHTML = problemProviders.map(p => ` | |
| <div style="padding: 12px; background: rgba(239, 68, 68, 0.1); backdrop-filter: blur(10px); border-radius: 8px; margin-bottom: 10px; border-left: 3px solid var(--danger);"> | |
| <strong>${p.name}</strong> | |
| <div style="font-size: 12px; color: var(--text-muted); margin-top: 4px;">${p.provider_id}</div> | |
| <div style="font-size: 12px; color: var(--danger); margin-top: 4px;">Status: ${p.status}</div> | |
| </div> | |
| `).join('') || '<div style="color: var(--text-muted);">No issues detected ✅</div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error loading analytics:', error); | |
| } | |
| } | |
| // Load Logs | |
| async function loadLogs() { | |
| try { | |
| const response = await fetch('/api/logs/recent'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| const logs = data.logs || []; | |
| const container = document.getElementById('logs-container'); | |
| if (logs.length === 0) { | |
| container.innerHTML = '<div style="color: var(--text-muted);">No logs available</div>'; | |
| return; | |
| } | |
| container.innerHTML = logs.map(log => ` | |
| <div style="padding: 8px; border-bottom: 1px solid var(--border); animation: slideIn 0.3s;"> | |
| <span style="color: var(--text-muted);">[${log.timestamp || 'N/A'}]</span> | |
| <span style="color: ${log.level === 'ERROR' ? 'var(--danger)' : 'var(--text-light)'};">${log.message || JSON.stringify(log)}</span> | |
| </div> | |
| `).join(''); | |
| } else { | |
| document.getElementById('logs-container').innerHTML = '<div style="color: var(--danger);">Failed to load logs</div>'; | |
| } | |
| } catch (error) { | |
| console.error('Error loading logs:', error); | |
| document.getElementById('logs-container').innerHTML = '<div style="color: var(--danger);">Error loading logs: ' + error.message + '</div>'; | |
| } | |
| } | |
| // Load Discovery Stats | |
| async function loadDiscoveryStats() { | |
| try { | |
| const response = await fetch('/api/apl/summary'); | |
| if (response.ok) { | |
| const data = await response.json(); | |
| document.getElementById('discovery-found').textContent = data.total_active_providers || 0; | |
| document.getElementById('discovery-validated').textContent = (data.http_valid || 0) + (data.hf_valid || 0); | |
| document.getElementById('discovery-failed').textContent = (data.http_invalid || 0) + (data.hf_invalid || 0); | |
| if (data.timestamp) { | |
| document.getElementById('discovery-last').textContent = new Date(data.timestamp).toLocaleTimeString(); | |
| } | |
| } | |
| } catch (error) { | |
| console.error('Error loading discovery stats:', error); | |
| } | |
| } | |
| // Run Full Discovery | |
| async function runFullDiscovery() { | |
| const btn = document.getElementById('discovery-btn'); | |
| btn.disabled = true; | |
| btn.textContent = '⏳ Discovering...'; | |
| document.getElementById('discovery-progress').style.display = 'block'; | |
| try { | |
| let progress = 0; | |
| const progressInterval = setInterval(() => { | |
| progress += 5; | |
| if (progress <= 95) { | |
| document.getElementById('discovery-progress-bar').style.width = progress + '%'; | |
| document.getElementById('discovery-percent').textContent = progress + '%'; | |
| } | |
| }, 200); | |
| const response = await fetch('/api/apl/run', { method: 'POST' }); | |
| clearInterval(progressInterval); | |
| document.getElementById('discovery-progress-bar').style.width = '100%'; | |
| document.getElementById('discovery-percent').textContent = '100%'; | |
| if (response.ok) { | |
| const result = await response.json(); | |
| showToast('Discovery completed successfully!', 'success'); | |
| loadDiscoveryStats(); | |
| } else { | |
| showToast('Discovery failed', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error during discovery:', error); | |
| showToast('Error: ' + error.message, 'error'); | |
| } finally { | |
| btn.disabled = false; | |
| btn.textContent = '🚀 Run Full Discovery'; | |
| setTimeout(() => { | |
| document.getElementById('discovery-progress').style.display = 'none'; | |
| }, 2000); | |
| } | |
| } | |
| // Run APL Scan | |
| async function runAPLScan() { | |
| showToast('Running APL scan...', 'info'); | |
| try { | |
| const response = await fetch('/api/apl/run', { method: 'POST' }); | |
| if (response.ok) { | |
| showToast('APL scan completed!', 'success'); | |
| loadDiscoveryStats(); | |
| loadDashboardData(); | |
| } else { | |
| showToast('APL scan failed', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error running APL:', error); | |
| showToast('Error: ' + error.message, 'error'); | |
| } | |
| } | |
| // Run Diagnostics | |
| async function runDiagnostics(autoFix) { | |
| showToast('Running diagnostics...', 'info'); | |
| try { | |
| const response = await fetch(`/api/diagnostics/run?auto_fix=${autoFix}`, { method: 'POST' }); | |
| if (response.ok) { | |
| const result = await response.json(); | |
| let html = ` | |
| <div class="card" style="background: rgba(16, 185, 129, 0.1); margin-top: 20px;"> | |
| <h3>Diagnostics Results</h3> | |
| <p><strong>Issues Found:</strong> ${result.issues_found || 0}</p> | |
| <p><strong>Status:</strong> ${result.status || 'completed'}</p> | |
| ${autoFix ? `<p><strong>Fixes Applied:</strong> ${result.fixes_applied?.length || 0}</p>` : ''} | |
| </div> | |
| `; | |
| document.getElementById('diagnostics-output').innerHTML = html; | |
| showToast('Diagnostics completed', 'success'); | |
| } else { | |
| showToast('Diagnostics failed', 'error'); | |
| } | |
| } catch (error) { | |
| console.error('Error running diagnostics:', error); | |
| showToast('Error: ' + error.message, 'error'); | |
| } | |
| } | |
| // Utility Functions | |
| function showToast(message, type = 'info') { | |
| const toast = document.getElementById('toast'); | |
| const toastMessage = document.getElementById('toast-message'); | |
| toast.className = `toast ${type}`; | |
| toastMessage.textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => { | |
| toast.classList.remove('show'); | |
| }, 3000); | |
| } | |
| function refreshAllData() { | |
| showToast('Refreshing all data...', 'info'); | |
| loadDashboardData(); | |
| loadResources(); | |
| } | |
| function refreshAnalytics() { | |
| showToast('Refreshing analytics...', 'info'); | |
| loadAnalytics(); | |
| } | |
| function refreshLogs() { | |
| loadLogs(); | |
| } | |
| function filterLogs() { | |
| loadLogs(); | |
| } | |
| function scanResources() { | |
| showToast('Scanning resources...', 'info'); | |
| loadResources(); | |
| } | |
| function fixDuplicates() { | |
| if (!confirm('Remove duplicate resources?')) return; | |
| showToast('Removing duplicates...', 'info'); | |
| } | |
| function openAddResourceModal() { | |
| document.getElementById('add-resource-modal').classList.add('show'); | |
| } | |
| function closeAddResourceModal() { | |
| document.getElementById('add-resource-modal').classList.remove('show'); | |
| } | |
| async function addResource() { | |
| showToast('Adding resource...', 'info'); | |
| closeAddResourceModal(); | |
| } | |
| function testResource(id) { | |
| showToast(`Testing resource: ${id}`, 'info'); | |
| } | |
| function editResource(id) { | |
| showToast(`Edit resource: ${id}`, 'info'); | |
| } | |
| async function removeResource(id) { | |
| if (!confirm(`Remove resource: ${id}?`)) return; | |
| showToast('Resource removed', 'success'); | |
| loadResources(); | |
| } | |
| function validateAllResources() { | |
| showToast('Validating all resources...', 'info'); | |
| } | |
| function refreshAllResources() { | |
| loadResources(); | |
| } | |
| function removeInvalidResources() { | |
| if (!confirm('Remove all invalid resources?')) return; | |
| showToast('Removing invalid resources...', 'info'); | |
| } | |
| function exportResources() { | |
| showToast('Exporting configuration...', 'info'); | |
| } | |
| function importResources() { | |
| showToast('Import configuration...', 'info'); | |
| } | |
| function exportAnalytics() { | |
| showToast('Exporting analytics...', 'info'); | |
| } | |
| function exportLogs() { | |
| showToast('Exporting logs...', 'info'); | |
| } | |
| function clearLogs() { | |
| if (!confirm('Clear all logs?')) return; | |
| showToast('Logs cleared', 'success'); | |
| } | |
| function testConnections() { | |
| showToast('Testing connections...', 'info'); | |
| } | |
| function clearCache() { | |
| if (!confirm('Clear cache?')) return; | |
| showToast('Cache cleared', 'success'); | |
| } | |
| function discoverHFModels() { | |
| runFullDiscovery(); | |
| } | |
| function discoverAPIs() { | |
| runFullDiscovery(); | |
| } | |
| // Auto-refresh | |
| function startAutoRefresh() { | |
| setInterval(() => { | |
| const activeTab = document.querySelector('.tab-content.active').id; | |
| if (activeTab === 'tab-dashboard') { | |
| loadDashboardData(); | |
| } | |
| }, 30000); | |
| } | |
| </script> | |
| </body> | |
| </html> | |