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