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