Really-amin commited on
Commit
7f64a72
·
verified ·
1 Parent(s): 48688a6

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +2352 -0
index.html ADDED
@@ -0,0 +1,2352 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Crypto API Monitor - Real-time Dashboard</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ <style>
10
+ :root {
11
+ --bg-primary: #ffffff;
12
+ --bg-secondary: #f8f9ff;
13
+ --bg-card: #ffffff;
14
+ --bg-hover: #f3f4ff;
15
+ --text-primary: #1e1b4b;
16
+ --text-secondary: #4c4380;
17
+ --text-muted: #7c3aed;
18
+ --accent-primary: #8b5cf6;
19
+ --accent-secondary: #a78bfa;
20
+ --accent-tertiary: #c084fc;
21
+ --purple-glow: rgba(139, 92, 246, 0.5);
22
+ --success: #10b981;
23
+ --success-bg: #d1fae5;
24
+ --warning: #f59e0b;
25
+ --warning-bg: #fef3c7;
26
+ --danger: #ef4444;
27
+ --danger-bg: #fee2e2;
28
+ --info: #06b6d4;
29
+ --info-bg: #cffafe;
30
+ --border: rgba(139, 92, 246, 0.2);
31
+ --shadow-sm: 0 2px 8px rgba(139, 92, 246, 0.08);
32
+ --shadow: 0 4px 16px rgba(139, 92, 246, 0.12);
33
+ --shadow-lg: 0 10px 40px rgba(139, 92, 246, 0.15);
34
+ --radius: 16px;
35
+ --radius-lg: 24px;
36
+ --transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
37
+ }
38
+
39
+ * {
40
+ margin: 0;
41
+ padding: 0;
42
+ box-sizing: border-box;
43
+ }
44
+
45
+ body {
46
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
47
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9ff 50%, #f0f4ff 100%);
48
+ background-attachment: fixed;
49
+ color: var(--text-primary);
50
+ line-height: 1.6;
51
+ min-height: 100vh;
52
+ }
53
+
54
+ body::before {
55
+ content: '';
56
+ position: fixed;
57
+ top: 0;
58
+ left: 0;
59
+ right: 0;
60
+ bottom: 0;
61
+ background:
62
+ radial-gradient(circle at 20% 30%, rgba(139, 92, 246, 0.05) 0%, transparent 50%),
63
+ radial-gradient(circle at 80% 70%, rgba(168, 85, 247, 0.05) 0%, transparent 50%);
64
+ pointer-events: none;
65
+ z-index: 0;
66
+ }
67
+
68
+ .container {
69
+ max-width: 1600px;
70
+ margin: 0 auto;
71
+ padding: 24px;
72
+ position: relative;
73
+ z-index: 1;
74
+ }
75
+
76
+ /* Header */
77
+ .header {
78
+ background: var(--bg-card);
79
+ border: 2px solid var(--border);
80
+ border-radius: var(--radius-lg);
81
+ padding: 28px;
82
+ margin-bottom: 24px;
83
+ box-shadow: var(--shadow-lg);
84
+ }
85
+
86
+ .header-top {
87
+ display: flex;
88
+ align-items: center;
89
+ justify-content: space-between;
90
+ flex-wrap: wrap;
91
+ gap: 20px;
92
+ margin-bottom: 24px;
93
+ }
94
+
95
+ .logo {
96
+ display: flex;
97
+ align-items: center;
98
+ gap: 16px;
99
+ }
100
+
101
+ .logo-icon {
102
+ width: 64px;
103
+ height: 64px;
104
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
105
+ border-radius: 20px;
106
+ display: flex;
107
+ align-items: center;
108
+ justify-content: center;
109
+ box-shadow: 0 10px 30px rgba(139, 92, 246, 0.4);
110
+ position: relative;
111
+ overflow: hidden;
112
+ }
113
+
114
+ .logo-icon::after {
115
+ content: '';
116
+ position: absolute;
117
+ top: -50%;
118
+ left: -50%;
119
+ right: -50%;
120
+ bottom: -50%;
121
+ background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.3), transparent);
122
+ animation: shimmer 3s infinite;
123
+ }
124
+
125
+ @keyframes shimmer {
126
+ 0% { transform: translateX(-100%) translateY(-100%) rotate(45deg); }
127
+ 100% { transform: translateX(100%) translateY(100%) rotate(45deg); }
128
+ }
129
+
130
+ .logo-text h1 {
131
+ font-size: 32px;
132
+ font-weight: 900;
133
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
134
+ -webkit-background-clip: text;
135
+ -webkit-text-fill-color: transparent;
136
+ background-clip: text;
137
+ margin-bottom: 4px;
138
+ letter-spacing: -0.5px;
139
+ }
140
+
141
+ .logo-text p {
142
+ font-size: 14px;
143
+ color: var(--text-muted);
144
+ font-weight: 600;
145
+ }
146
+
147
+ .header-actions {
148
+ display: flex;
149
+ gap: 12px;
150
+ align-items: center;
151
+ flex-wrap: wrap;
152
+ }
153
+
154
+ .status-badge {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 10px;
158
+ padding: 12px 20px;
159
+ border-radius: 999px;
160
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(16, 185, 129, 0.08) 100%);
161
+ border: 2px solid rgba(16, 185, 129, 0.4);
162
+ font-size: 14px;
163
+ font-weight: 700;
164
+ color: var(--success);
165
+ box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
166
+ text-transform: uppercase;
167
+ letter-spacing: 0.5px;
168
+ }
169
+
170
+ .status-dot {
171
+ width: 10px;
172
+ height: 10px;
173
+ border-radius: 50%;
174
+ background: var(--success);
175
+ animation: pulse-glow 2s infinite;
176
+ }
177
+
178
+ @keyframes pulse-glow {
179
+ 0%, 100% {
180
+ box-shadow: 0 0 0 0 rgba(16, 185, 129, 0.7),
181
+ 0 0 10px rgba(16, 185, 129, 0.5);
182
+ }
183
+ 50% {
184
+ box-shadow: 0 0 0 8px rgba(16, 185, 129, 0),
185
+ 0 0 20px rgba(16, 185, 129, 0.3);
186
+ }
187
+ }
188
+
189
+ .connection-status {
190
+ display: flex;
191
+ align-items: center;
192
+ gap: 8px;
193
+ padding: 10px 16px;
194
+ border-radius: 999px;
195
+ background: var(--bg-card);
196
+ border: 2px solid var(--border);
197
+ font-size: 12px;
198
+ font-weight: 700;
199
+ }
200
+
201
+ .connection-status.connected { border-color: var(--success); color: var(--success); }
202
+ .connection-status.disconnected { border-color: var(--danger); color: var(--danger); }
203
+ .connection-status.connecting { border-color: var(--warning); color: var(--warning); }
204
+
205
+ .btn {
206
+ padding: 14px 28px;
207
+ border-radius: 14px;
208
+ border: none;
209
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
210
+ color: white;
211
+ font-family: inherit;
212
+ font-size: 14px;
213
+ font-weight: 700;
214
+ cursor: pointer;
215
+ transition: var(--transition);
216
+ display: inline-flex;
217
+ align-items: center;
218
+ gap: 10px;
219
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.3);
220
+ text-transform: uppercase;
221
+ letter-spacing: 0.5px;
222
+ position: relative;
223
+ overflow: hidden;
224
+ }
225
+
226
+ .btn::before {
227
+ content: '';
228
+ position: absolute;
229
+ top: 0;
230
+ left: -100%;
231
+ width: 100%;
232
+ height: 100%;
233
+ background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
234
+ transition: left 0.5s;
235
+ }
236
+
237
+ .btn:hover {
238
+ transform: translateY(-3px);
239
+ box-shadow: 0 8px 24px rgba(139, 92, 246, 0.5);
240
+ }
241
+
242
+ .btn:hover::before {
243
+ left: 100%;
244
+ }
245
+
246
+ .btn-secondary {
247
+ background: white;
248
+ color: var(--accent-primary);
249
+ border: 2px solid var(--border);
250
+ box-shadow: var(--shadow-sm);
251
+ }
252
+
253
+ .btn-secondary:hover {
254
+ background: var(--bg-hover);
255
+ border-color: var(--accent-primary);
256
+ }
257
+
258
+ .btn-icon {
259
+ padding: 12px;
260
+ width: 44px;
261
+ height: 44px;
262
+ }
263
+
264
+ .icon {
265
+ width: 20px;
266
+ height: 20px;
267
+ stroke: currentColor;
268
+ stroke-width: 2.5;
269
+ stroke-linecap: round;
270
+ stroke-linejoin: round;
271
+ fill: none;
272
+ }
273
+
274
+ .icon-lg {
275
+ width: 26px;
276
+ height: 26px;
277
+ }
278
+
279
+ /* KPI Cards */
280
+ .kpi-grid {
281
+ display: grid;
282
+ grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
283
+ gap: 20px;
284
+ margin-bottom: 24px;
285
+ }
286
+
287
+ .kpi-card {
288
+ background: var(--bg-card);
289
+ border: 2px solid var(--border);
290
+ border-radius: var(--radius-lg);
291
+ padding: 32px;
292
+ transition: var(--transition);
293
+ box-shadow: var(--shadow);
294
+ position: relative;
295
+ overflow: hidden;
296
+ cursor: pointer;
297
+ }
298
+
299
+ .kpi-card::before {
300
+ content: '';
301
+ position: absolute;
302
+ top: 0;
303
+ left: 0;
304
+ right: 0;
305
+ height: 6px;
306
+ background: linear-gradient(90deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
307
+ transform: scaleX(0);
308
+ transform-origin: left;
309
+ transition: transform 0.5s cubic-bezier(0.4, 0, 0.2, 1);
310
+ }
311
+
312
+ .kpi-card:hover {
313
+ transform: translateY(-8px) scale(1.02);
314
+ box-shadow: 0 16px 48px rgba(139, 92, 246, 0.25);
315
+ border-color: var(--accent-primary);
316
+ }
317
+
318
+ .kpi-card:hover::before {
319
+ transform: scaleX(1);
320
+ }
321
+
322
+ .kpi-header {
323
+ display: flex;
324
+ align-items: center;
325
+ justify-content: space-between;
326
+ margin-bottom: 20px;
327
+ }
328
+
329
+ .kpi-label {
330
+ font-size: 12px;
331
+ color: var(--text-muted);
332
+ font-weight: 800;
333
+ text-transform: uppercase;
334
+ letter-spacing: 1.2px;
335
+ }
336
+
337
+ .kpi-icon-wrapper {
338
+ width: 64px;
339
+ height: 64px;
340
+ border-radius: 18px;
341
+ display: flex;
342
+ align-items: center;
343
+ justify-content: center;
344
+ transition: var(--transition);
345
+ box-shadow: var(--shadow);
346
+ }
347
+
348
+ .kpi-card:hover .kpi-icon-wrapper {
349
+ transform: rotate(-5deg) scale(1.15);
350
+ box-shadow: 0 10px 30px rgba(139, 92, 246, 0.3);
351
+ }
352
+
353
+ .kpi-value {
354
+ font-size: 48px;
355
+ font-weight: 900;
356
+ margin-bottom: 16px;
357
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 50%, #c084fc 100%);
358
+ -webkit-background-clip: text;
359
+ -webkit-text-fill-color: transparent;
360
+ background-clip: text;
361
+ line-height: 1;
362
+ animation: countUp 0.6s ease-out;
363
+ letter-spacing: -2px;
364
+ }
365
+
366
+ @keyframes countUp {
367
+ from { opacity: 0; transform: translateY(20px); }
368
+ to { opacity: 1; transform: translateY(0); }
369
+ }
370
+
371
+ .kpi-trend {
372
+ display: flex;
373
+ align-items: center;
374
+ gap: 10px;
375
+ font-size: 13px;
376
+ font-weight: 700;
377
+ padding: 8px 16px;
378
+ border-radius: 12px;
379
+ width: fit-content;
380
+ text-transform: uppercase;
381
+ letter-spacing: 0.5px;
382
+ }
383
+
384
+ .trend-up {
385
+ color: var(--success);
386
+ background: var(--success-bg);
387
+ border: 2px solid var(--success);
388
+ }
389
+
390
+ .trend-down {
391
+ color: var(--danger);
392
+ background: var(--danger-bg);
393
+ border: 2px solid var(--danger);
394
+ }
395
+
396
+ .trend-neutral {
397
+ color: var(--info);
398
+ background: var(--info-bg);
399
+ border: 2px solid var(--info);
400
+ }
401
+
402
+ /* Tabs */
403
+ .tabs {
404
+ display: flex;
405
+ gap: 6px;
406
+ margin-bottom: 24px;
407
+ overflow-x: auto;
408
+ padding: 8px;
409
+ background: var(--bg-card);
410
+ border-radius: var(--radius-lg);
411
+ border: 2px solid var(--border);
412
+ box-shadow: var(--shadow-sm);
413
+ }
414
+
415
+ .tab {
416
+ padding: 12px 20px;
417
+ border-radius: 12px;
418
+ background: transparent;
419
+ border: none;
420
+ color: var(--text-secondary);
421
+ cursor: pointer;
422
+ transition: all 0.25s;
423
+ white-space: nowrap;
424
+ font-weight: 700;
425
+ font-size: 13px;
426
+ display: flex;
427
+ align-items: center;
428
+ gap: 8px;
429
+ }
430
+
431
+ .tab:hover:not(.active) {
432
+ background: var(--bg-hover);
433
+ color: var(--text-primary);
434
+ }
435
+
436
+ .tab.active {
437
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
438
+ color: white;
439
+ box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
440
+ transform: scale(1.05);
441
+ }
442
+
443
+ .tab .icon {
444
+ width: 16px;
445
+ height: 16px;
446
+ }
447
+
448
+ /* Tab Content */
449
+ .tab-content {
450
+ display: none;
451
+ animation: fadeIn 0.4s ease;
452
+ }
453
+
454
+ .tab-content.active {
455
+ display: block;
456
+ }
457
+
458
+ @keyframes fadeIn {
459
+ from { opacity: 0; transform: translateY(20px); }
460
+ to { opacity: 1; transform: translateY(0); }
461
+ }
462
+
463
+ /* Card */
464
+ .card {
465
+ background: var(--bg-card);
466
+ border: 2px solid var(--border);
467
+ border-radius: var(--radius-lg);
468
+ padding: 28px;
469
+ margin-bottom: 24px;
470
+ box-shadow: var(--shadow);
471
+ transition: var(--transition);
472
+ }
473
+
474
+ .card:hover {
475
+ box-shadow: var(--shadow-lg);
476
+ }
477
+
478
+ .card-header {
479
+ display: flex;
480
+ align-items: center;
481
+ justify-content: space-between;
482
+ margin-bottom: 24px;
483
+ padding-bottom: 16px;
484
+ border-bottom: 2px solid var(--border);
485
+ }
486
+
487
+ .card-title {
488
+ font-size: 20px;
489
+ font-weight: 800;
490
+ display: flex;
491
+ align-items: center;
492
+ gap: 12px;
493
+ color: var(--text-primary);
494
+ }
495
+
496
+ .card-actions {
497
+ display: flex;
498
+ gap: 8px;
499
+ }
500
+
501
+ /* Table */
502
+ .table-container {
503
+ overflow-x: auto;
504
+ border-radius: var(--radius);
505
+ border: 2px solid var(--border);
506
+ }
507
+
508
+ .table {
509
+ width: 100%;
510
+ border-collapse: collapse;
511
+ }
512
+
513
+ .table thead {
514
+ background: linear-gradient(135deg, #8b5cf6 0%, #a78bfa 100%);
515
+ }
516
+
517
+ .table thead th {
518
+ color: white;
519
+ font-weight: 700;
520
+ font-size: 13px;
521
+ text-align: left;
522
+ padding: 16px;
523
+ text-transform: uppercase;
524
+ letter-spacing: 0.8px;
525
+ }
526
+
527
+ .table tbody tr {
528
+ transition: var(--transition);
529
+ border-bottom: 1px solid var(--border);
530
+ }
531
+
532
+ .table tbody tr:hover {
533
+ background: var(--bg-hover);
534
+ }
535
+
536
+ .table tbody td {
537
+ padding: 16px;
538
+ font-size: 14px;
539
+ color: var(--text-secondary);
540
+ }
541
+
542
+ /* Badge */
543
+ .badge {
544
+ display: inline-flex;
545
+ align-items: center;
546
+ gap: 6px;
547
+ padding: 6px 12px;
548
+ border-radius: 999px;
549
+ font-size: 12px;
550
+ font-weight: 700;
551
+ white-space: nowrap;
552
+ }
553
+
554
+ .badge-success {
555
+ background: var(--success-bg);
556
+ color: var(--success);
557
+ border: 2px solid var(--success);
558
+ }
559
+
560
+ .badge-warning {
561
+ background: var(--warning-bg);
562
+ color: var(--warning);
563
+ border: 2px solid var(--warning);
564
+ }
565
+
566
+ .badge-danger {
567
+ background: var(--danger-bg);
568
+ color: var(--danger);
569
+ border: 2px solid var(--danger);
570
+ }
571
+
572
+ .badge-info {
573
+ background: var(--info-bg);
574
+ color: var(--info);
575
+ border: 2px solid var(--info);
576
+ }
577
+
578
+ /* Progress Bar */
579
+ .progress {
580
+ height: 12px;
581
+ background: var(--bg-hover);
582
+ border-radius: 999px;
583
+ overflow: hidden;
584
+ margin: 8px 0;
585
+ border: 2px solid var(--border);
586
+ }
587
+
588
+ .progress-bar {
589
+ height: 100%;
590
+ background: linear-gradient(90deg, #8b5cf6, #a78bfa);
591
+ border-radius: 999px;
592
+ transition: width 0.5s ease;
593
+ }
594
+
595
+ .progress-bar.success {
596
+ background: linear-gradient(90deg, var(--success), #34d399);
597
+ }
598
+
599
+ .progress-bar.warning {
600
+ background: linear-gradient(90deg, var(--warning), #fbbf24);
601
+ }
602
+
603
+ .progress-bar.danger {
604
+ background: linear-gradient(90deg, var(--danger), #f87171);
605
+ }
606
+
607
+ /* Chart Container */
608
+ .chart-container {
609
+ position: relative;
610
+ height: 320px;
611
+ margin: 20px 0;
612
+ background: var(--bg-secondary);
613
+ border-radius: var(--radius);
614
+ padding: 16px;
615
+ border: 2px solid var(--border);
616
+ }
617
+
618
+ /* Loading */
619
+ .loading-overlay {
620
+ position: fixed;
621
+ top: 0;
622
+ left: 0;
623
+ right: 0;
624
+ bottom: 0;
625
+ background: rgba(255, 255, 255, 0.95);
626
+ backdrop-filter: blur(8px);
627
+ display: none;
628
+ align-items: center;
629
+ justify-content: center;
630
+ z-index: 9999;
631
+ }
632
+
633
+ .loading-overlay.active {
634
+ display: flex;
635
+ }
636
+
637
+ .spinner {
638
+ width: 60px;
639
+ height: 60px;
640
+ border: 6px solid var(--border);
641
+ border-top-color: var(--accent-primary);
642
+ border-radius: 50%;
643
+ animation: spin 0.8s linear infinite;
644
+ }
645
+
646
+ @keyframes spin {
647
+ to { transform: rotate(360deg); }
648
+ }
649
+
650
+ .loading-inline {
651
+ display: flex;
652
+ align-items: center;
653
+ justify-content: center;
654
+ padding: 40px;
655
+ color: var(--text-muted);
656
+ }
657
+
658
+ .spinner-inline {
659
+ width: 32px;
660
+ height: 32px;
661
+ border: 3px solid var(--border);
662
+ border-top-color: var(--accent-primary);
663
+ border-radius: 50%;
664
+ animation: spin 0.8s linear infinite;
665
+ margin-right: 12px;
666
+ }
667
+
668
+ /* Toast */
669
+ .toast-container {
670
+ position: fixed;
671
+ bottom: 24px;
672
+ right: 24px;
673
+ z-index: 10000;
674
+ display: flex;
675
+ flex-direction: column;
676
+ gap: 12px;
677
+ max-width: 400px;
678
+ }
679
+
680
+ .toast {
681
+ padding: 16px 20px;
682
+ border-radius: var(--radius);
683
+ background: var(--bg-card);
684
+ border: 2px solid var(--border);
685
+ box-shadow: var(--shadow-lg);
686
+ display: flex;
687
+ align-items: center;
688
+ gap: 12px;
689
+ animation: slideInRight 0.3s ease;
690
+ min-width: 300px;
691
+ }
692
+
693
+ @keyframes slideInRight {
694
+ from { transform: translateX(400px); opacity: 0; }
695
+ to { transform: translateX(0); opacity: 1; }
696
+ }
697
+
698
+ .toast.success { border-color: var(--success); background: var(--success-bg); }
699
+ .toast.error { border-color: var(--danger); background: var(--danger-bg); }
700
+ .toast.warning { border-color: var(--warning); background: var(--warning-bg); }
701
+ .toast.info { border-color: var(--info); background: var(--info-bg); }
702
+
703
+ .toast-content { flex: 1; }
704
+ .toast-title { font-weight: 700; font-size: 14px; margin-bottom: 2px; }
705
+ .toast-message { font-size: 13px; color: var(--text-secondary); }
706
+
707
+ /* Alert */
708
+ .alert {
709
+ padding: 18px 24px;
710
+ border-radius: var(--radius);
711
+ margin-bottom: 16px;
712
+ display: flex;
713
+ align-items: flex-start;
714
+ gap: 14px;
715
+ border-left: 6px solid;
716
+ box-shadow: var(--shadow-sm);
717
+ }
718
+
719
+ .alert-success { background: var(--success-bg); border-color: var(--success); color: var(--success); }
720
+ .alert-warning { background: var(--warning-bg); border-color: var(--warning); color: var(--warning); }
721
+ .alert-danger { background: var(--danger-bg); border-color: var(--danger); color: var(--danger); }
722
+ .alert-info { background: var(--info-bg); border-color: var(--info); color: var(--info); }
723
+
724
+ .alert-content { flex: 1; }
725
+ .alert-title { font-weight: 800; margin-bottom: 6px; font-size: 15px; }
726
+ .alert-message { font-size: 14px; opacity: 0.9; }
727
+
728
+ /* Grid */
729
+ .grid { display: grid; gap: 20px; }
730
+ .grid-2 { grid-template-columns: repeat(auto-fit, minmax(400px, 1fr)); }
731
+ .grid-3 { grid-template-columns: repeat(auto-fit, minmax(320px, 1fr)); }
732
+
733
+ /* Input */
734
+ .input {
735
+ width: 100%;
736
+ padding: 12px 16px;
737
+ border-radius: var(--radius);
738
+ border: 2px solid var(--border);
739
+ background: var(--bg-card);
740
+ color: var(--text-primary);
741
+ font-family: inherit;
742
+ font-size: 14px;
743
+ transition: var(--transition);
744
+ }
745
+
746
+ .input:focus {
747
+ outline: none;
748
+ border-color: var(--accent-primary);
749
+ box-shadow: 0 0 0 4px rgba(139, 92, 246, 0.1);
750
+ }
751
+
752
+ /* Responsive */
753
+ @media (max-width: 768px) {
754
+ .container { padding: 16px; }
755
+ .header-top { flex-direction: column; align-items: flex-start; }
756
+ .kpi-grid { grid-template-columns: 1fr; }
757
+ .grid-2, .grid-3 { grid-template-columns: 1fr; }
758
+ .card-header { flex-direction: column; align-items: flex-start; gap: 16px; }
759
+ .card-actions { width: 100%; justify-content: flex-end; }
760
+ }
761
+ </style>
762
+ </head>
763
+ <body>
764
+ <div class="loading-overlay" id="loadingOverlay">
765
+ <div class="spinner"></div>
766
+ </div>
767
+
768
+ <div class="toast-container" id="toastContainer"></div>
769
+
770
+ <div class="container">
771
+ <div class="header">
772
+ <div class="header-top">
773
+ <div class="logo">
774
+ <div class="logo-icon">
775
+ <svg class="icon icon-lg" style="stroke: white;">
776
+ <circle cx="12" cy="12" r="10"></circle>
777
+ <path d="M12 6v6l4 2"></path>
778
+ </svg>
779
+ </div>
780
+ <div class="logo-text">
781
+ <h1>Crypto API Monitor</h1>
782
+ <p>Real-time Cryptocurrency API Resource Monitoring</p>
783
+ </div>
784
+ </div>
785
+ <div class="header-actions">
786
+ <div class="connection-status" id="wsStatus">
787
+ <span class="status-dot"></span>
788
+ <span id="wsStatusText">Connecting...</span>
789
+ </div>
790
+ <div class="status-badge" id="systemStatus">
791
+ <span class="status-dot"></span>
792
+ <span id="systemStatusText">System Active</span>
793
+ </div>
794
+ <button class="btn" onclick="refreshAll()">
795
+ <svg class="icon">
796
+ <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"></path>
797
+ </svg>
798
+ Refresh All
799
+ </button>
800
+ </div>
801
+ </div>
802
+
803
+ <div class="kpi-grid" id="kpiGrid">
804
+ <div class="kpi-card">
805
+ <div class="kpi-header">
806
+ <span class="kpi-label">Total APIs</span>
807
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(59, 130, 246, 0.15) 0%, rgba(37, 99, 235, 0.1) 100%);">
808
+ <svg class="icon icon-lg" style="stroke: #3b82f6;">
809
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
810
+ <line x1="3" y1="9" x2="21" y2="9"></line>
811
+ <line x1="9" y1="21" x2="9" y2="9"></line>
812
+ </svg>
813
+ </div>
814
+ </div>
815
+ <div class="kpi-value" id="kpiTotalAPIs">--</div>
816
+ <div class="kpi-trend trend-neutral">
817
+ <svg class="icon" style="width: 16px; height: 16px;">
818
+ <path d="M12 20V10M18 20V4M6 20v-4"></path>
819
+ </svg>
820
+ <span id="kpiTotalTrend">Loading...</span>
821
+ </div>
822
+ </div>
823
+
824
+ <div class="kpi-card">
825
+ <div class="kpi-header">
826
+ <span class="kpi-label">Online</span>
827
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(16, 185, 129, 0.15) 0%, rgba(5, 150, 105, 0.1) 100%);">
828
+ <svg class="icon icon-lg" style="stroke: #10b981;">
829
+ <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
830
+ <polyline points="9 12 11 14 15 10"></polyline>
831
+ </svg>
832
+ </div>
833
+ </div>
834
+ <div class="kpi-value" id="kpiOnline">--</div>
835
+ <div class="kpi-trend trend-up">
836
+ <svg class="icon" style="width: 16px; height: 16px;">
837
+ <line x1="12" y1="19" x2="12" y2="5"></line>
838
+ <polyline points="5 12 12 5 19 12"></polyline>
839
+ </svg>
840
+ <span id="kpiOnlineTrend">Loading...</span>
841
+ </div>
842
+ </div>
843
+
844
+ <div class="kpi-card">
845
+ <div class="kpi-header">
846
+ <span class="kpi-label">Avg Response</span>
847
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(245, 158, 11, 0.15) 0%, rgba(217, 119, 6, 0.1) 100%);">
848
+ <svg class="icon icon-lg" style="stroke: #f59e0b;">
849
+ <path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"></path>
850
+ </svg>
851
+ </div>
852
+ </div>
853
+ <div class="kpi-value" id="kpiAvgResponse" style="font-size: 32px;">--</div>
854
+ <div class="kpi-trend trend-down">
855
+ <svg class="icon" style="width: 16px; height: 16px;">
856
+ <line x1="12" y1="5" x2="12" y2="19"></line>
857
+ <polyline points="19 12 12 19 5 12"></polyline>
858
+ </svg>
859
+ <span id="kpiResponseTrend">Loading...</span>
860
+ </div>
861
+ </div>
862
+
863
+ <div class="kpi-card">
864
+ <div class="kpi-header">
865
+ <span class="kpi-label">Last Update</span>
866
+ <div class="kpi-icon-wrapper" style="background: linear-gradient(135deg, rgba(139, 92, 246, 0.15) 0%, rgba(124, 58, 237, 0.1) 100%);">
867
+ <svg class="icon icon-lg" style="stroke: #8b5cf6;">
868
+ <circle cx="12" cy="12" r="10"></circle>
869
+ <polyline points="12 6 12 12 16 14"></polyline>
870
+ </svg>
871
+ </div>
872
+ </div>
873
+ <div class="kpi-value" id="kpiLastUpdate" style="font-size: 20px; line-height: 1.2;">--</div>
874
+ <div class="kpi-trend trend-neutral">
875
+ <svg class="icon" style="width: 16px; height: 16px;">
876
+ <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"></path>
877
+ </svg>
878
+ <span>Auto-refresh enabled</span>
879
+ </div>
880
+ </div>
881
+ </div>
882
+ </div>
883
+
884
+ <div class="tabs">
885
+ <div class="tab active" onclick="switchTab(event, 'dashboard')">
886
+ <svg class="icon">
887
+ <rect x="3" y="3" width="7" height="7"></rect>
888
+ <rect x="14" y="3" width="7" height="7"></rect>
889
+ <rect x="14" y="14" width="7" height="7"></rect>
890
+ <rect x="3" y="14" width="7" height="7"></rect>
891
+ </svg>
892
+ <span>Dashboard</span>
893
+ </div>
894
+ <div class="tab" onclick="switchTab(event, 'providers')">
895
+ <svg class="icon">
896
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
897
+ <polyline points="14 2 14 8 20 8"></polyline>
898
+ </svg>
899
+ <span>Providers</span>
900
+ </div>
901
+ <div class="tab" onclick="switchTab(event, 'categories')">
902
+ <svg class="icon">
903
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
904
+ <line x1="3" y1="9" x2="21" y2="9"></line>
905
+ <line x1="9" y1="21" x2="9" y2="9"></line>
906
+ </svg>
907
+ <span>Categories</span>
908
+ </div>
909
+ <div class="tab" onclick="switchTab(event, 'ratelimits')">
910
+ <svg class="icon">
911
+ <circle cx="12" cy="12" r="10"></circle>
912
+ <polyline points="12 6 12 12 16 14"></polyline>
913
+ </svg>
914
+ <span>Rate Limits</span>
915
+ </div>
916
+ <div class="tab" onclick="switchTab(event, 'logs')">
917
+ <svg class="icon">
918
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
919
+ <polyline points="14 2 14 8 20 8"></polyline>
920
+ <line x1="16" y1="13" x2="8" y2="13"></line>
921
+ </svg>
922
+ <span>Logs</span>
923
+ </div>
924
+ <div class="tab" onclick="switchTab(event, 'alerts')">
925
+ <svg class="icon">
926
+ <circle cx="12" cy="12" r="10"></circle>
927
+ <line x1="12" y1="8" x2="12" y2="12"></line>
928
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
929
+ </svg>
930
+ <span>Alerts</span>
931
+ </div>
932
+ <div class="tab" onclick="switchTab(event, 'huggingface')">
933
+ <svg class="icon">
934
+ <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
935
+ <circle cx="12" cy="12" r="3"></circle>
936
+ </svg>
937
+ <span>HuggingFace</span>
938
+ </div>
939
+ </div>
940
+
941
+ <!-- Dashboard Tab -->
942
+ <div class="tab-content active" id="tab-dashboard">
943
+ <div id="alertsContainer"></div>
944
+
945
+ <div class="card">
946
+ <div class="card-header">
947
+ <h2 class="card-title">
948
+ <svg class="icon icon-lg">
949
+ <rect x="3" y="3" width="7" height="7"></rect>
950
+ <rect x="14" y="3" width="7" height="7"></rect>
951
+ </svg>
952
+ System Overview
953
+ </h2>
954
+ <button class="btn btn-secondary" onclick="loadProviders()">
955
+ <svg class="icon">
956
+ <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"></path>
957
+ </svg>
958
+ Refresh
959
+ </button>
960
+ </div>
961
+ <div class="table-container">
962
+ <table class="table">
963
+ <thead>
964
+ <tr>
965
+ <th>Provider</th>
966
+ <th>Category</th>
967
+ <th>Status</th>
968
+ <th>Response Time</th>
969
+ <th>Last Check</th>
970
+ </tr>
971
+ </thead>
972
+ <tbody id="providersTableBody">
973
+ <tr>
974
+ <td colspan="5">
975
+ <div class="loading-inline">
976
+ <div class="spinner-inline"></div>
977
+ Loading providers...
978
+ </div>
979
+ </td>
980
+ </tr>
981
+ </tbody>
982
+ </table>
983
+ </div>
984
+ </div>
985
+
986
+ <div class="grid grid-2">
987
+ <div class="card">
988
+ <div class="card-header">
989
+ <h2 class="card-title">
990
+ <svg class="icon icon-lg">
991
+ <polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline>
992
+ </svg>
993
+ Health Status
994
+ </h2>
995
+ </div>
996
+ <div class="chart-container">
997
+ <canvas id="healthChart"></canvas>
998
+ </div>
999
+ </div>
1000
+
1001
+ <div class="card">
1002
+ <div class="card-header">
1003
+ <h2 class="card-title">
1004
+ <svg class="icon icon-lg">
1005
+ <path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path>
1006
+ </svg>
1007
+ Status Distribution
1008
+ </h2>
1009
+ </div>
1010
+ <div class="chart-container">
1011
+ <canvas id="statusChart"></canvas>
1012
+ </div>
1013
+ </div>
1014
+ </div>
1015
+ </div>
1016
+
1017
+ <!-- Providers Tab -->
1018
+ <div class="tab-content" id="tab-providers">
1019
+ <div class="card">
1020
+ <div class="card-header">
1021
+ <h2 class="card-title">
1022
+ <svg class="icon icon-lg">
1023
+ <circle cx="12" cy="12" r="10"></circle>
1024
+ </svg>
1025
+ All Providers
1026
+ </h2>
1027
+ <button class="btn btn-secondary" onclick="loadProviders()">
1028
+ <svg class="icon">
1029
+ <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"></path>
1030
+ </svg>
1031
+ Refresh
1032
+ </button>
1033
+ </div>
1034
+ <div id="providersDetail">
1035
+ <div class="loading-inline">
1036
+ <div class="spinner-inline"></div>
1037
+ Loading providers details...
1038
+ </div>
1039
+ </div>
1040
+ </div>
1041
+ </div>
1042
+
1043
+ <!-- Categories Tab -->
1044
+ <div class="tab-content" id="tab-categories">
1045
+ <div class="card">
1046
+ <div class="card-header">
1047
+ <h2 class="card-title">
1048
+ <svg class="icon icon-lg">
1049
+ <rect x="3" y="3" width="18" height="18" rx="2"></rect>
1050
+ <line x1="3" y1="9" x2="21" y2="9"></line>
1051
+ </svg>
1052
+ Categories Overview
1053
+ </h2>
1054
+ <button class="btn btn-secondary" onclick="loadCategories()">
1055
+ <svg class="icon">
1056
+ <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"></path>
1057
+ </svg>
1058
+ Refresh
1059
+ </button>
1060
+ </div>
1061
+ <div class="table-container">
1062
+ <table class="table">
1063
+ <thead>
1064
+ <tr>
1065
+ <th>Category</th>
1066
+ <th>Total Sources</th>
1067
+ <th>Online</th>
1068
+ <th>Health %</th>
1069
+ <th>Avg Response</th>
1070
+ <th>Last Updated</th>
1071
+ <th>Status</th>
1072
+ </tr>
1073
+ </thead>
1074
+ <tbody id="categoriesTableBody">
1075
+ <tr>
1076
+ <td colspan="7">
1077
+ <div class="loading-inline">
1078
+ <div class="spinner-inline"></div>
1079
+ Loading categories...
1080
+ </div>
1081
+ </td>
1082
+ </tr>
1083
+ </tbody>
1084
+ </table>
1085
+ </div>
1086
+ </div>
1087
+ </div>
1088
+
1089
+ <!-- Rate Limits Tab -->
1090
+ <div class="tab-content" id="tab-ratelimits">
1091
+ <div class="card">
1092
+ <div class="card-header">
1093
+ <h2 class="card-title">
1094
+ <svg class="icon icon-lg">
1095
+ <circle cx="12" cy="12" r="10"></circle>
1096
+ <polyline points="12 6 12 12 16 14"></polyline>
1097
+ </svg>
1098
+ Rate Limit Monitor
1099
+ </h2>
1100
+ <button class="btn btn-secondary" onclick="loadRateLimits()">
1101
+ <svg class="icon">
1102
+ <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"></path>
1103
+ </svg>
1104
+ Refresh
1105
+ </button>
1106
+ </div>
1107
+ <div id="rateLimitCards" class="grid grid-2">
1108
+ <div class="loading-inline">
1109
+ <div class="spinner-inline"></div>
1110
+ Loading rate limits...
1111
+ </div>
1112
+ </div>
1113
+ </div>
1114
+ </div>
1115
+
1116
+ <!-- Logs Tab -->
1117
+ <div class="tab-content" id="tab-logs">
1118
+ <div class="card">
1119
+ <div class="card-header">
1120
+ <h2 class="card-title">
1121
+ <svg class="icon icon-lg">
1122
+ <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
1123
+ <polyline points="14 2 14 8 20 8"></polyline>
1124
+ </svg>
1125
+ Connection Logs
1126
+ </h2>
1127
+ <div class="card-actions">
1128
+ <select id="logType" class="input" style="width: auto; padding: 10px 16px;" onchange="loadLogs()">
1129
+ <option value="connection">Connection</option>
1130
+ <option value="error">Error</option>
1131
+ <option value="rate_limit">Rate Limit</option>
1132
+ <option value="all">All</option>
1133
+ </select>
1134
+ <button class="btn btn-secondary" onclick="loadLogs()">
1135
+ <svg class="icon">
1136
+ <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"></path>
1137
+ </svg>
1138
+ Refresh
1139
+ </button>
1140
+ </div>
1141
+ </div>
1142
+ <div class="table-container">
1143
+ <table class="table">
1144
+ <thead>
1145
+ <tr>
1146
+ <th>Timestamp</th>
1147
+ <th>Provider</th>
1148
+ <th>Type</th>
1149
+ <th>Status</th>
1150
+ <th>Response Time</th>
1151
+ <th>Message</th>
1152
+ </tr>
1153
+ </thead>
1154
+ <tbody id="logsTableBody">
1155
+ <tr>
1156
+ <td colspan="6">
1157
+ <div class="loading-inline">
1158
+ <div class="spinner-inline"></div>
1159
+ Loading logs...
1160
+ </div>
1161
+ </td>
1162
+ </tr>
1163
+ </tbody>
1164
+ </table>
1165
+ </div>
1166
+ </div>
1167
+ </div>
1168
+
1169
+ <!-- Alerts Tab -->
1170
+ <div class="tab-content" id="tab-alerts">
1171
+ <div class="card">
1172
+ <div class="card-header">
1173
+ <h2 class="card-title">
1174
+ <svg class="icon icon-lg">
1175
+ <circle cx="12" cy="12" r="10"></circle>
1176
+ <line x1="12" y1="8" x2="12" y2="12"></line>
1177
+ <line x1="12" y1="16" x2="12.01" y2="16"></line>
1178
+ </svg>
1179
+ System Alerts
1180
+ </h2>
1181
+ <button class="btn btn-secondary" onclick="loadAlerts()">
1182
+ <svg class="icon">
1183
+ <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"></path>
1184
+ </svg>
1185
+ Refresh
1186
+ </button>
1187
+ </div>
1188
+ <div id="alertsList">
1189
+ <div class="loading-inline">
1190
+ <div class="spinner-inline"></div>
1191
+ Loading alerts...
1192
+ </div>
1193
+ </div>
1194
+ </div>
1195
+ </div>
1196
+
1197
+ <!-- HuggingFace Tab -->
1198
+ <div class="tab-content" id="tab-huggingface">
1199
+ <div class="card">
1200
+ <div class="card-header">
1201
+ <h2 class="card-title">
1202
+ <svg class="icon icon-lg">
1203
+ <circle cx="12" cy="12" r="10"></circle>
1204
+ </svg>
1205
+ 🤗 HuggingFace Health Status
1206
+ </h2>
1207
+ <div class="card-actions">
1208
+ <button class="btn" onclick="refreshHFRegistry()">
1209
+ <svg class="icon">
1210
+ <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"></path>
1211
+ </svg>
1212
+ Refresh Registry
1213
+ </button>
1214
+ </div>
1215
+ </div>
1216
+ <div id="hfHealthDisplay" style="padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; max-height: 300px; overflow-y: auto; border: 2px solid var(--border);">
1217
+ Loading HF health status...
1218
+ </div>
1219
+ </div>
1220
+
1221
+ <div class="grid grid-2">
1222
+ <div class="card">
1223
+ <div class="card-header">
1224
+ <h2 class="card-title">
1225
+ Models Registry
1226
+ <span class="badge badge-success" id="hfModelsCount">0</span>
1227
+ </h2>
1228
+ </div>
1229
+ <div id="hfModelsList" style="max-height: 400px; overflow-y: auto;">
1230
+ <div class="loading-inline">
1231
+ <div class="spinner-inline"></div>
1232
+ Loading models...
1233
+ </div>
1234
+ </div>
1235
+ </div>
1236
+
1237
+ <div class="card">
1238
+ <div class="card-header">
1239
+ <h2 class="card-title">
1240
+ Datasets Registry
1241
+ <span class="badge badge-success" id="hfDatasetsCount">0</span>
1242
+ </h2>
1243
+ </div>
1244
+ <div id="hfDatasetsList" style="max-height: 400px; overflow-y: auto;">
1245
+ <div class="loading-inline">
1246
+ <div class="spinner-inline"></div>
1247
+ Loading datasets...
1248
+ </div>
1249
+ </div>
1250
+ </div>
1251
+ </div>
1252
+
1253
+ <div class="card">
1254
+ <div class="card-header">
1255
+ <h2 class="card-title">
1256
+ <svg class="icon icon-lg">
1257
+ <circle cx="11" cy="11" r="8"></circle>
1258
+ <path d="m21 21-4.35-4.35"></path>
1259
+ </svg>
1260
+ Search Registry
1261
+ </h2>
1262
+ </div>
1263
+ <div style="display: flex; gap: 12px; margin-bottom: 20px;">
1264
+ <input type="text" id="hfSearchQuery" placeholder="Search crypto, bitcoin, sentiment..." class="input" style="flex: 1;" value="crypto">
1265
+ <select id="hfSearchKind" class="input" style="width: auto; padding: 12px 16px;">
1266
+ <option value="models">Models</option>
1267
+ <option value="datasets">Datasets</option>
1268
+ </select>
1269
+ <button class="btn" onclick="searchHF()">
1270
+ <svg class="icon">
1271
+ <circle cx="11" cy="11" r="8"></circle>
1272
+ <path d="m21 21-4.35-4.35"></path>
1273
+ </svg>
1274
+ Search
1275
+ </button>
1276
+ </div>
1277
+ <div id="hfSearchResults" style="max-height: 400px; overflow-y: auto; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); border: 2px solid var(--border);">
1278
+ <div style="text-align: center; color: var(--text-muted);">Enter a query and click search</div>
1279
+ </div>
1280
+ </div>
1281
+
1282
+ <div class="card">
1283
+ <div class="card-header">
1284
+ <h2 class="card-title">💭 Sentiment Analysis</h2>
1285
+ </div>
1286
+ <div style="margin-bottom: 16px;">
1287
+ <label style="display: block; font-weight: 700; margin-bottom: 8px; color: var(--text-primary);">Text Samples (one per line)</label>
1288
+ <textarea id="hfSentimentTexts" rows="6" class="input" placeholder="BTC strong breakout&#10;ETH looks weak&#10;Crypto market is bullish today">BTC strong breakout
1289
+ ETH looks weak
1290
+ Crypto market is bullish today
1291
+ Bears are taking control
1292
+ Neutral market conditions</textarea>
1293
+ </div>
1294
+ <button class="btn" onclick="runHFSentiment()">
1295
+ <svg class="icon">
1296
+ <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2zm0 18a8 8 0 1 1 8-8 8 8 0 0 1-8 8z"></path>
1297
+ </svg>
1298
+ Run Sentiment Analysis
1299
+ </button>
1300
+ <div id="hfSentimentVote" style="margin: 20px 0; padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); text-align: center; font-size: 32px; font-weight: 900; border: 2px solid var(--border);">
1301
+ <span style="color: var(--text-muted);">—</span>
1302
+ </div>
1303
+ <div id="hfSentimentResults" style="padding: 20px; background: var(--bg-secondary); border-radius: var(--radius); font-family: monospace; font-size: 13px; white-space: pre-wrap; max-height: 400px; overflow-y: auto; border: 2px solid var(--border);">
1304
+ Results will appear here...
1305
+ </div>
1306
+ </div>
1307
+ </div>
1308
+ </div>
1309
+
1310
+ <script>
1311
+ // Configuration
1312
+ const config = {
1313
+ apiBaseUrl: window.location.origin,
1314
+ wsUrl: (() => {
1315
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1316
+ const host = window.location.host;
1317
+ return `${protocol}//${host}/ws/live`;
1318
+ })(),
1319
+ autoRefreshInterval: 30000,
1320
+ maxRetries: 3
1321
+ };
1322
+
1323
+ // Global state
1324
+ let state = {
1325
+ ws: null,
1326
+ wsConnected: false,
1327
+ autoRefreshEnabled: true,
1328
+ charts: {},
1329
+ currentTab: 'dashboard',
1330
+ providers: [],
1331
+ categories: [],
1332
+ rateLimits: [],
1333
+ logs: [],
1334
+ alerts: [],
1335
+ lastUpdate: null
1336
+ };
1337
+
1338
+ // Initialize on page load
1339
+ document.addEventListener('DOMContentLoaded', function() {
1340
+ console.log('🚀 Initializing Crypto API Monitor...');
1341
+ console.log('📍 API Base URL:', config.apiBaseUrl);
1342
+ console.log('📡 WebSocket URL:', config.wsUrl);
1343
+
1344
+ initializeWebSocket();
1345
+ loadInitialData();
1346
+ startAutoRefresh();
1347
+ });
1348
+
1349
+ // WebSocket Connection
1350
+ function initializeWebSocket() {
1351
+ updateWSStatus('connecting');
1352
+
1353
+ try {
1354
+ state.ws = new WebSocket(config.wsUrl);
1355
+
1356
+ state.ws.onopen = () => {
1357
+ console.log('✅ WebSocket connected');
1358
+ state.wsConnected = true;
1359
+ updateWSStatus('connected');
1360
+ showToast('Connected', 'Real-time data stream active', 'success');
1361
+ };
1362
+
1363
+ state.ws.onmessage = (event) => {
1364
+ try {
1365
+ const data = JSON.parse(event.data);
1366
+ handleWSMessage(data);
1367
+ } catch (error) {
1368
+ console.error('Error parsing WebSocket message:', error);
1369
+ }
1370
+ };
1371
+
1372
+ state.ws.onerror = (error) => {
1373
+ console.error('❌ WebSocket error:', error);
1374
+ updateWSStatus('disconnected');
1375
+ };
1376
+
1377
+ state.ws.onclose = () => {
1378
+ console.log('⚠️ WebSocket disconnected');
1379
+ state.wsConnected = false;
1380
+ updateWSStatus('disconnected');
1381
+ };
1382
+ } catch (error) {
1383
+ console.log('⚠️ WebSocket not available - using polling mode');
1384
+ updateWSStatus('disconnected');
1385
+ }
1386
+ }
1387
+
1388
+ function updateWSStatus(status) {
1389
+ const statusEl = document.getElementById('wsStatus');
1390
+ const textEl = document.getElementById('wsStatusText');
1391
+
1392
+ statusEl.classList.remove('connected', 'disconnected', 'connecting');
1393
+ statusEl.classList.add(status);
1394
+
1395
+ const statusText = {
1396
+ 'connected': '✓ Connected',
1397
+ 'disconnected': '✗ Disconnected',
1398
+ 'connecting': '⟳ Connecting...'
1399
+ };
1400
+
1401
+ textEl.textContent = statusText[status] || 'Unknown';
1402
+ }
1403
+
1404
+ function handleWSMessage(data) {
1405
+ console.log('📨 WebSocket message:', data.type);
1406
+
1407
+ switch(data.type) {
1408
+ case 'status_update':
1409
+ updateKPIs(data.data);
1410
+ break;
1411
+ case 'provider_status_change':
1412
+ loadProviders();
1413
+ break;
1414
+ case 'new_alert':
1415
+ addAlert(data.data);
1416
+ break;
1417
+ default:
1418
+ console.log('Unknown message type:', data.type);
1419
+ }
1420
+ }
1421
+
1422
+ // API Calls
1423
+ async function apiCall(endpoint, options = {}) {
1424
+ try {
1425
+ const url = `${config.apiBaseUrl}${endpoint}`;
1426
+ console.log('🌐 API Call:', url);
1427
+
1428
+ const response = await fetch(url, {
1429
+ ...options,
1430
+ headers: {
1431
+ 'Content-Type': 'application/json',
1432
+ ...options.headers
1433
+ }
1434
+ });
1435
+
1436
+ if (!response.ok) {
1437
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1438
+ }
1439
+
1440
+ const data = await response.json();
1441
+ console.log('✅ API Response:', endpoint, data);
1442
+ return data;
1443
+ } catch (error) {
1444
+ console.error(`❌ API call failed: ${endpoint}`, error);
1445
+ showToast('API Error', `Failed: ${endpoint}`, 'error');
1446
+ throw error;
1447
+ }
1448
+ }
1449
+
1450
+ async function loadInitialData() {
1451
+ showLoading();
1452
+
1453
+ try {
1454
+ console.log('📊 Loading initial data...');
1455
+
1456
+ await loadHealth();
1457
+ await loadProviders();
1458
+ await loadSystemInfo();
1459
+
1460
+ initializeCharts();
1461
+
1462
+ state.lastUpdate = new Date();
1463
+ updateLastUpdateDisplay();
1464
+
1465
+ console.log('✅ Initial data loaded successfully');
1466
+ showToast('Success', 'Dashboard loaded successfully', 'success');
1467
+ } catch (error) {
1468
+ console.error('❌ Error loading initial data:', error);
1469
+ showToast('Error', 'Failed to load initial data', 'error');
1470
+ } finally {
1471
+ hideLoading();
1472
+ }
1473
+ }
1474
+
1475
+ async function loadHealth() {
1476
+ try {
1477
+ const data = await apiCall('/health');
1478
+ updateKPIs(data.components);
1479
+
1480
+ const statusBadge = document.getElementById('systemStatus');
1481
+ const statusText = document.getElementById('systemStatusText');
1482
+
1483
+ if (data.status === 'healthy') {
1484
+ statusBadge.style.background = 'linear-gradient(135deg, rgba(16, 185, 129, 0.15), rgba(16, 185, 129, 0.08))';
1485
+ statusBadge.style.borderColor = 'rgba(16, 185, 129, 0.4)';
1486
+ statusBadge.style.color = 'var(--success)';
1487
+ statusText.textContent = '✓ System Healthy';
1488
+ } else if (data.status === 'degraded') {
1489
+ statusBadge.style.background = 'linear-gradient(135deg, rgba(245, 158, 11, 0.15), rgba(245, 158, 11, 0.08))';
1490
+ statusBadge.style.borderColor = 'rgba(245, 158, 11, 0.4)';
1491
+ statusBadge.style.color = 'var(--warning)';
1492
+ statusText.textContent = '⚠ System Degraded';
1493
+ } else {
1494
+ statusBadge.style.background = 'linear-gradient(135deg, rgba(239, 68, 68, 0.15), rgba(239, 68, 68, 0.08))';
1495
+ statusBadge.style.borderColor = 'rgba(239, 68, 68, 0.4)';
1496
+ statusBadge.style.color = 'var(--danger)';
1497
+ statusText.textContent = '✗ System Critical';
1498
+ }
1499
+ } catch (error) {
1500
+ console.error('Error loading health:', error);
1501
+ }
1502
+ }
1503
+
1504
+ async function loadSystemInfo() {
1505
+ try {
1506
+ const data = await apiCall('/info');
1507
+ console.log('📋 System Info:', data);
1508
+ } catch (error) {
1509
+ console.error('Error loading system info:', error);
1510
+ }
1511
+ }
1512
+
1513
+ async function loadProviders() {
1514
+ try {
1515
+ const data = await apiCall('/api/providers');
1516
+ state.providers = data;
1517
+ renderProvidersTable(data);
1518
+ renderProvidersDetail(data);
1519
+ updateStatusChart(data);
1520
+ } catch (error) {
1521
+ console.error('Error loading providers:', error);
1522
+ document.getElementById('providersTableBody').innerHTML = `
1523
+ <tr>
1524
+ <td colspan="5" style="text-align: center; color: var(--text-muted); padding: 40px;">
1525
+ Failed to load providers
1526
+ </td>
1527
+ </tr>
1528
+ `;
1529
+ }
1530
+ }
1531
+
1532
+ async function loadCategories() {
1533
+ try {
1534
+ showLoading();
1535
+ const data = await apiCall('/api/categories');
1536
+ state.categories = data;
1537
+ renderCategoriesTable(data);
1538
+ showToast('Success', 'Categories loaded successfully', 'success');
1539
+ } catch (error) {
1540
+ console.error('Error loading categories:', error);
1541
+ showToast('Error', 'Failed to load categories', 'error');
1542
+ } finally {
1543
+ hideLoading();
1544
+ }
1545
+ }
1546
+
1547
+ async function loadRateLimits() {
1548
+ try {
1549
+ showLoading();
1550
+ const data = await apiCall('/api/rate-limits');
1551
+ state.rateLimits = data;
1552
+ renderRateLimitCards(data);
1553
+ showToast('Success', 'Rate limits loaded successfully', 'success');
1554
+ } catch (error) {
1555
+ console.error('Error loading rate limits:', error);
1556
+ showToast('Error', 'Failed to load rate limits', 'error');
1557
+ } finally {
1558
+ hideLoading();
1559
+ }
1560
+ }
1561
+
1562
+ async function loadLogs() {
1563
+ try {
1564
+ showLoading();
1565
+ const logType = document.getElementById('logType').value;
1566
+ const data = await apiCall(`/api/logs?type=${logType}`);
1567
+ state.logs = data;
1568
+ renderLogsTable(data);
1569
+ showToast('Success', 'Logs loaded successfully', 'success');
1570
+ } catch (error) {
1571
+ console.error('Error loading logs:', error);
1572
+ showToast('Error', 'Failed to load logs', 'error');
1573
+ } finally {
1574
+ hideLoading();
1575
+ }
1576
+ }
1577
+
1578
+ async function loadAlerts() {
1579
+ try {
1580
+ showLoading();
1581
+ const data = await apiCall('/api/alerts');
1582
+ state.alerts = data;
1583
+ renderAlertsList(data);
1584
+ showToast('Success', 'Alerts loaded successfully', 'success');
1585
+ } catch (error) {
1586
+ console.error('Error loading alerts:', error);
1587
+ showToast('Error', 'Failed to load alerts', 'error');
1588
+ } finally {
1589
+ hideLoading();
1590
+ }
1591
+ }
1592
+
1593
+ // HuggingFace APIs
1594
+ async function loadHFHealth() {
1595
+ try {
1596
+ const data = await apiCall('/api/hf/health');
1597
+ document.getElementById('hfHealthDisplay').textContent = JSON.stringify(data, null, 2);
1598
+ } catch (error) {
1599
+ console.error('Error loading HF health:', error);
1600
+ document.getElementById('hfHealthDisplay').textContent = 'Error loading health status';
1601
+ }
1602
+ }
1603
+
1604
+ async function refreshHFRegistry() {
1605
+ try {
1606
+ showLoading();
1607
+ const data = await apiCall('/api/hf/refresh', { method: 'POST' });
1608
+ showToast('Success', 'HF Registry refreshed', 'success');
1609
+ loadHFModels();
1610
+ loadHFDatasets();
1611
+ } catch (error) {
1612
+ console.error('Error refreshing HF registry:', error);
1613
+ showToast('Error', 'Failed to refresh registry', 'error');
1614
+ } finally {
1615
+ hideLoading();
1616
+ }
1617
+ }
1618
+
1619
+ async function loadHFModels() {
1620
+ try {
1621
+ const data = await apiCall('/api/hf/registry?type=models');
1622
+ document.getElementById('hfModelsCount').textContent = data.length || 0;
1623
+ document.getElementById('hfModelsList').innerHTML = data.map(item => `
1624
+ <div style="padding: 12px; border-bottom: 1px solid var(--border);">
1625
+ <div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
1626
+ <div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
1627
+ </div>
1628
+ `).join('');
1629
+ } catch (error) {
1630
+ console.error('Error loading HF models:', error);
1631
+ document.getElementById('hfModelsList').innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Error loading models</div>';
1632
+ }
1633
+ }
1634
+
1635
+ async function loadHFDatasets() {
1636
+ try {
1637
+ const data = await apiCall('/api/hf/registry?type=datasets');
1638
+ document.getElementById('hfDatasetsCount').textContent = data.length || 0;
1639
+ document.getElementById('hfDatasetsList').innerHTML = data.map(item => `
1640
+ <div style="padding: 12px; border-bottom: 1px solid var(--border);">
1641
+ <div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
1642
+ <div style="font-size: 12px; color: var(--text-muted);">${item.description || 'No description'}</div>
1643
+ </div>
1644
+ `).join('');
1645
+ } catch (error) {
1646
+ console.error('Error loading HF datasets:', error);
1647
+ document.getElementById('hfDatasetsList').innerHTML = '<div style="padding: 20px; text-align: center; color: var(--text-muted);">Error loading datasets</div>';
1648
+ }
1649
+ }
1650
+
1651
+ async function searchHF() {
1652
+ try {
1653
+ showLoading();
1654
+ const query = document.getElementById('hfSearchQuery').value;
1655
+ const kind = document.getElementById('hfSearchKind').value;
1656
+ const data = await apiCall(`/api/hf/search?q=${encodeURIComponent(query)}&kind=${kind}`);
1657
+
1658
+ document.getElementById('hfSearchResults').innerHTML = data.map(item => `
1659
+ <div style="padding: 12px; border-bottom: 1px solid var(--border);">
1660
+ <div style="font-weight: 700; margin-bottom: 4px;">${item.id || 'Unknown'}</div>
1661
+ <div style="font-size: 12px; color: var(--text-muted); margin-bottom: 4px;">${item.description || 'No description'}</div>
1662
+ <div style="font-size: 11px; color: var(--accent-primary);">Downloads: ${item.downloads || 0} • Likes: ${item.likes || 0}</div>
1663
+ </div>
1664
+ `).join('');
1665
+
1666
+ showToast('Success', `Found ${data.length} results`, 'success');
1667
+ } catch (error) {
1668
+ console.error('Error searching HF:', error);
1669
+ showToast('Error', 'Search failed', 'error');
1670
+ } finally {
1671
+ hideLoading();
1672
+ }
1673
+ }
1674
+
1675
+ async function runHFSentiment() {
1676
+ try {
1677
+ showLoading();
1678
+ const texts = document.getElementById('hfSentimentTexts').value.split('\n').filter(t => t.trim());
1679
+
1680
+ const data = await apiCall('/api/hf/run-sentiment', {
1681
+ method: 'POST',
1682
+ body: JSON.stringify({ texts: texts })
1683
+ });
1684
+
1685
+ document.getElementById('hfSentimentResults').textContent = JSON.stringify(data, null, 2);
1686
+
1687
+ // Calculate overall sentiment vote
1688
+ const sentiments = data.results || [];
1689
+ const positive = sentiments.filter(s => s.sentiment === 'positive').length;
1690
+ const negative = sentiments.filter(s => s.sentiment === 'negative').length;
1691
+ const neutral = sentiments.filter(s => s.sentiment === 'neutral').length;
1692
+
1693
+ let overall = 'NEUTRAL';
1694
+ let color = 'var(--info)';
1695
+
1696
+ if (positive > negative && positive > neutral) {
1697
+ overall = 'BULLISH 📈';
1698
+ color = 'var(--success)';
1699
+ } else if (negative > positive && negative > neutral) {
1700
+ overall = 'BEARISH 📉';
1701
+ color = 'var(--danger)';
1702
+ }
1703
+
1704
+ document.getElementById('hfSentimentVote').innerHTML = `
1705
+ <span style="color: ${color};">${overall}</span>
1706
+ <div style="font-size: 14px; margin-top: 8px; color: var(--text-muted);">
1707
+ Positive: ${positive} • Negative: ${negative} • Neutral: ${neutral}
1708
+ </div>
1709
+ `;
1710
+
1711
+ showToast('Success', 'Sentiment analysis completed', 'success');
1712
+ } catch (error) {
1713
+ console.error('Error running sentiment analysis:', error);
1714
+ showToast('Error', 'Sentiment analysis failed', 'error');
1715
+ } finally {
1716
+ hideLoading();
1717
+ }
1718
+ }
1719
+
1720
+ // Rendering Functions
1721
+ function renderProvidersTable(providers) {
1722
+ const tbody = document.getElementById('providersTableBody');
1723
+
1724
+ if (!providers || providers.length === 0) {
1725
+ tbody.innerHTML = `
1726
+ <tr>
1727
+ <td colspan="5" style="text-align: center; color: var(--text-muted); padding: 40px;">
1728
+ No providers found
1729
+ </td>
1730
+ </tr>
1731
+ `;
1732
+ return;
1733
+ }
1734
+
1735
+ tbody.innerHTML = providers.map(provider => `
1736
+ <tr>
1737
+ <td>
1738
+ <strong>${provider.name || 'Unknown'}</strong>
1739
+ <div style="font-size: 12px; color: var(--text-muted);">${provider.base_url || ''}</div>
1740
+ </td>
1741
+ <td>${provider.category || 'General'}</td>
1742
+ <td>
1743
+ <span class="badge ${getStatusBadgeClass(provider.status)}">
1744
+ ${provider.status || 'unknown'}
1745
+ </span>
1746
+ </td>
1747
+ <td>${provider.response_time ? provider.response_time + 'ms' : '--'}</td>
1748
+ <td>
1749
+ <div style="font-size: 12px; font-weight: 700;">${formatTimestamp(provider.last_checked)}</div>
1750
+ <div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(provider.last_checked)}</div>
1751
+ </td>
1752
+ </tr>
1753
+ `).join('');
1754
+ }
1755
+
1756
+ function renderProvidersDetail(providers) {
1757
+ const container = document.getElementById('providersDetail');
1758
+
1759
+ if (!providers || providers.length === 0) {
1760
+ container.innerHTML = `
1761
+ <div style="text-align: center; color: var(--text-muted); padding: 40px;">
1762
+ No providers data available
1763
+ </div>
1764
+ `;
1765
+ return;
1766
+ }
1767
+
1768
+ container.innerHTML = `
1769
+ <div class="table-container">
1770
+ <table class="table">
1771
+ <thead>
1772
+ <tr>
1773
+ <th>Provider</th>
1774
+ <th>Status</th>
1775
+ <th>Response Time</th>
1776
+ <th>Success Rate</th>
1777
+ <th>Last Success</th>
1778
+ <th>Errors (24h)</th>
1779
+ </tr>
1780
+ </thead>
1781
+ <tbody>
1782
+ ${providers.map(provider => `
1783
+ <tr>
1784
+ <td>
1785
+ <strong>${provider.name || 'Unknown'}</strong>
1786
+ <div style="font-size: 12px; color: var(--text-muted);">${provider.base_url || ''}</div>
1787
+ </td>
1788
+ <td>
1789
+ <span class="badge ${getStatusBadgeClass(provider.status)}">
1790
+ ${provider.status || 'unknown'}
1791
+ </span>
1792
+ </td>
1793
+ <td>${provider.response_time ? provider.response_time + 'ms' : '--'}</td>
1794
+ <td>
1795
+ <div class="progress">
1796
+ <div class="progress-bar ${getHealthClass(provider.success_rate || 0)}"
1797
+ style="width: ${provider.success_rate || 0}%"></div>
1798
+ </div>
1799
+ <small>${Math.round(provider.success_rate || 0)}%</small>
1800
+ </td>
1801
+ <td>${formatTimestamp(provider.last_success)}</td>
1802
+ <td>${provider.error_count_24h || 0}</td>
1803
+ </tr>
1804
+ `).join('')}
1805
+ </tbody>
1806
+ </table>
1807
+ </div>
1808
+ `;
1809
+ }
1810
+
1811
+ function renderCategoriesTable(categories) {
1812
+ const tbody = document.getElementById('categoriesTableBody');
1813
+
1814
+ if (!categories || categories.length === 0) {
1815
+ tbody.innerHTML = `
1816
+ <tr>
1817
+ <td colspan="7" style="text-align: center; color: var(--text-muted); padding: 40px;">
1818
+ No categories found
1819
+ </td>
1820
+ </tr>
1821
+ `;
1822
+ return;
1823
+ }
1824
+
1825
+ tbody.innerHTML = categories.map(category => `
1826
+ <tr>
1827
+ <td>
1828
+ <strong>${category.name || 'Unnamed'}</strong>
1829
+ </td>
1830
+ <td>${category.total_sources || 0}</td>
1831
+ <td>${category.online || 0}</td>
1832
+ <td>
1833
+ <div class="progress">
1834
+ <div class="progress-bar ${getHealthClass(category.health_percentage || 0)}"
1835
+ style="width: ${category.health_percentage || 0}%"></div>
1836
+ </div>
1837
+ <small>${Math.round(category.health_percentage || 0)}%</small>
1838
+ </td>
1839
+ <td>${category.avg_response || '--'}ms</td>
1840
+ <td>${formatTimestamp(category.last_updated)}</td>
1841
+ <td>
1842
+ <span class="badge ${getStatusBadgeClass(category.status)}">
1843
+ ${category.status || 'unknown'}
1844
+ </span>
1845
+ </td>
1846
+ </tr>
1847
+ `).join('');
1848
+ }
1849
+
1850
+ function renderRateLimitCards(rateLimits) {
1851
+ const container = document.getElementById('rateLimitCards');
1852
+
1853
+ if (!rateLimits || rateLimits.length === 0) {
1854
+ container.innerHTML = `
1855
+ <div class="card" style="grid-column: 1 / -1; text-align: center; padding: 40px;">
1856
+ <div style="color: var(--text-muted); font-size: 16px;">
1857
+ No rate limit data available
1858
+ </div>
1859
+ </div>
1860
+ `;
1861
+ return;
1862
+ }
1863
+
1864
+ container.innerHTML = rateLimits.map(limit => `
1865
+ <div class="card">
1866
+ <div class="card-header">
1867
+ <h3 class="card-title" style="font-size: 16px;">
1868
+ ${limit.provider || 'Unknown Provider'}
1869
+ </h3>
1870
+ <span class="badge ${getRateLimitStatusClass(limit)}">
1871
+ ${getRateLimitStatus(limit)}
1872
+ </span>
1873
+ </div>
1874
+
1875
+ <div style="margin-bottom: 16px;">
1876
+ <div style="display: flex; justify-content: space-between; margin-bottom: 8px;">
1877
+ <span style="font-size: 12px; color: var(--text-muted);">Usage</span>
1878
+ <span style="font-size: 12px; font-weight: 700;">
1879
+ ${limit.used || 0}/${limit.limit || 0}
1880
+ </span>
1881
+ </div>
1882
+ <div class="progress">
1883
+ <div class="progress-bar ${getRateLimitProgressClass(limit)}"
1884
+ style="width: ${calculateUsagePercentage(limit)}%"></div>
1885
+ </div>
1886
+ </div>
1887
+
1888
+ <div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; font-size: 12px;">
1889
+ <div>
1890
+ <div style="color: var(--text-muted);">Reset In</div>
1891
+ <div style="font-weight: 700;">${formatResetTime(limit.reset_time)}</div>
1892
+ </div>
1893
+ <div>
1894
+ <div style="color: var(--text-muted);">Window</div>
1895
+ <div style="font-weight: 700;">${limit.window || 'N/A'}</div>
1896
+ </div>
1897
+ </div>
1898
+
1899
+ ${limit.endpoint ? `
1900
+ <div style="margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border);">
1901
+ <div style="font-size: 11px; color: var(--text-muted);">Endpoint</div>
1902
+ <div style="font-size: 12px; font-family: monospace;">${limit.endpoint}</div>
1903
+ </div>
1904
+ ` : ''}
1905
+ </div>
1906
+ `).join('');
1907
+ }
1908
+
1909
+ function renderLogsTable(logs) {
1910
+ const tbody = document.getElementById('logsTableBody');
1911
+
1912
+ if (!logs || logs.length === 0) {
1913
+ tbody.innerHTML = `
1914
+ <tr>
1915
+ <td colspan="6" style="text-align: center; color: var(--text-muted); padding: 40px;">
1916
+ No logs found
1917
+ </td>
1918
+ </tr>
1919
+ `;
1920
+ return;
1921
+ }
1922
+
1923
+ tbody.innerHTML = logs.map(log => `
1924
+ <tr>
1925
+ <td>
1926
+ <div style="font-size: 12px; font-weight: 700;">${formatTimestamp(log.timestamp)}</div>
1927
+ <div style="font-size: 11px; color: var(--text-muted);">${formatTimeAgo(log.timestamp)}</div>
1928
+ </td>
1929
+ <td>
1930
+ <strong>${log.provider || 'System'}</strong>
1931
+ </td>
1932
+ <td>
1933
+ <span class="badge ${getLogTypeClass(log.type)}">
1934
+ ${log.type || 'unknown'}
1935
+ </span>
1936
+ </td>
1937
+ <td>
1938
+ <span class="badge ${getStatusBadgeClass(log.status)}">
1939
+ ${log.status || 'unknown'}
1940
+ </span>
1941
+ </td>
1942
+ <td>${log.response_time ? log.response_time + 'ms' : '--'}</td>
1943
+ <td>
1944
+ <div style="max-width: 300px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
1945
+ ${log.message || 'No message'}
1946
+ </div>
1947
+ </td>
1948
+ </tr>
1949
+ `).join('');
1950
+ }
1951
+
1952
+ function renderAlertsList(alerts) {
1953
+ const container = document.getElementById('alertsList');
1954
+
1955
+ if (!alerts || alerts.length === 0) {
1956
+ container.innerHTML = `
1957
+ <div class="alert alert-success">
1958
+ <div class="alert-content">
1959
+ <div class="alert-title">No Active Alerts</div>
1960
+ <div class="alert-message">All systems are operating normally</div>
1961
+ </div>
1962
+ </div>
1963
+ `;
1964
+ return;
1965
+ }
1966
+
1967
+ container.innerHTML = alerts.map(alert => `
1968
+ <div class="alert ${getAlertClass(alert.severity)}">
1969
+ <div class="alert-content">
1970
+ <div class="alert-title">
1971
+ ${alert.title || 'Alert'}
1972
+ <span style="font-size: 11px; margin-left: 8px; opacity: 0.8;">
1973
+ ${formatTimeAgo(alert.timestamp)}
1974
+ </span>
1975
+ </div>
1976
+ <div class="alert-message">
1977
+ ${alert.message || 'No message provided'}
1978
+ </div>
1979
+ ${alert.provider ? `
1980
+ <div style="margin-top: 8px; font-size: 12px;">
1981
+ <strong>Provider:</strong> ${alert.provider}
1982
+ </div>
1983
+ ` : ''}
1984
+ </div>
1985
+ </div>
1986
+ `).join('');
1987
+ }
1988
+
1989
+ // Helper functions
1990
+ function getHealthClass(percentage) {
1991
+ if (percentage >= 80) return 'success';
1992
+ if (percentage >= 60) return 'warning';
1993
+ return 'danger';
1994
+ }
1995
+
1996
+ function getStatusBadgeClass(status) {
1997
+ switch (status?.toLowerCase()) {
1998
+ case 'healthy': case 'online': case 'success': return 'badge-success';
1999
+ case 'degraded': case 'warning': return 'badge-warning';
2000
+ case 'offline': case 'error': case 'critical': return 'badge-danger';
2001
+ default: return 'badge-info';
2002
+ }
2003
+ }
2004
+
2005
+ function getLogTypeClass(type) {
2006
+ switch (type?.toLowerCase()) {
2007
+ case 'error': return 'badge-danger';
2008
+ case 'warning': return 'badge-warning';
2009
+ case 'info': case 'connection': return 'badge-info';
2010
+ case 'success': return 'badge-success';
2011
+ default: return 'badge-info';
2012
+ }
2013
+ }
2014
+
2015
+ function getAlertClass(severity) {
2016
+ switch (severity?.toLowerCase()) {
2017
+ case 'critical': case 'error': return 'alert-danger';
2018
+ case 'warning': return 'alert-warning';
2019
+ case 'info': return 'alert-info';
2020
+ case 'success': return 'alert-success';
2021
+ default: return 'alert-info';
2022
+ }
2023
+ }
2024
+
2025
+ function getRateLimitStatusClass(limit) {
2026
+ const usage = calculateUsagePercentage(limit);
2027
+ if (usage >= 90) return 'badge-danger';
2028
+ if (usage >= 75) return 'badge-warning';
2029
+ return 'badge-success';
2030
+ }
2031
+
2032
+ function getRateLimitStatus(limit) {
2033
+ const usage = calculateUsagePercentage(limit);
2034
+ if (usage >= 90) return 'Critical';
2035
+ if (usage >= 75) return 'Warning';
2036
+ return 'Normal';
2037
+ }
2038
+
2039
+ function getRateLimitProgressClass(limit) {
2040
+ const usage = calculateUsagePercentage(limit);
2041
+ if (usage >= 90) return 'danger';
2042
+ if (usage >= 75) return 'warning';
2043
+ return 'success';
2044
+ }
2045
+
2046
+ function calculateUsagePercentage(limit) {
2047
+ if (!limit.limit || limit.limit === 0) return 0;
2048
+ return Math.min(100, ((limit.used || 0) / limit.limit) * 100);
2049
+ }
2050
+
2051
+ function formatResetTime(resetTime) {
2052
+ if (!resetTime) return 'N/A';
2053
+ // Simple formatting - you can enhance this with proper time parsing
2054
+ return typeof resetTime === 'string' ? resetTime : 'Soon';
2055
+ }
2056
+
2057
+ function formatTimestamp(timestamp) {
2058
+ if (!timestamp) return '--';
2059
+ try {
2060
+ return new Date(timestamp).toLocaleString();
2061
+ } catch {
2062
+ return 'Invalid Date';
2063
+ }
2064
+ }
2065
+
2066
+ function formatTimeAgo(timestamp) {
2067
+ if (!timestamp) return '';
2068
+ try {
2069
+ const now = new Date();
2070
+ const time = new Date(timestamp);
2071
+ const diff = now - time;
2072
+
2073
+ const minutes = Math.floor(diff / 60000);
2074
+ const hours = Math.floor(diff / 3600000);
2075
+ const days = Math.floor(diff / 86400000);
2076
+
2077
+ if (days > 0) return `${days}d ago`;
2078
+ if (hours > 0) return `${hours}h ago`;
2079
+ if (minutes > 0) return `${minutes}m ago`;
2080
+ return 'Just now';
2081
+ } catch {
2082
+ return 'Unknown';
2083
+ }
2084
+ }
2085
+
2086
+ // KPI Updates
2087
+ function updateKPIs(data) {
2088
+ if (!data) return;
2089
+
2090
+ // Update Total APIs
2091
+ const totalAPIs = data.length || 0;
2092
+ document.getElementById('kpiTotalAPIs').textContent = totalAPIs;
2093
+ document.getElementById('kpiTotalTrend').textContent = `${totalAPIs} active`;
2094
+
2095
+ // Update Online count
2096
+ const onlineCount = data.filter(p => p.status === 'online' || p.status === 'healthy').length;
2097
+ document.getElementById('kpiOnline').textContent = onlineCount;
2098
+ document.getElementById('kpiOnlineTrend').textContent = `${Math.round((onlineCount / totalAPIs) * 100)}% uptime`;
2099
+
2100
+ // Update Average Response
2101
+ const validResponses = data.filter(p => p.response_time).map(p => p.response_time);
2102
+ const avgResponse = validResponses.length > 0 ?
2103
+ Math.round(validResponses.reduce((a, b) => a + b, 0) / validResponses.length) : 0;
2104
+
2105
+ document.getElementById('kpiAvgResponse').textContent = avgResponse + 'ms';
2106
+
2107
+ const responseTrend = avgResponse < 500 ? 'Optimal' : avgResponse < 1000 ? 'Acceptable' : 'Slow';
2108
+ document.getElementById('kpiResponseTrend').textContent = responseTrend;
2109
+
2110
+ const trendElement = document.querySelector('#kpiAvgResponse').nextElementSibling;
2111
+ trendElement.className = `kpi-trend ${
2112
+ avgResponse < 500 ? 'trend-up' : avgResponse < 1000 ? 'trend-neutral' : 'trend-down'
2113
+ }`;
2114
+ }
2115
+
2116
+ function updateLastUpdateDisplay() {
2117
+ if (state.lastUpdate) {
2118
+ document.getElementById('kpiLastUpdate').textContent =
2119
+ state.lastUpdate.toLocaleTimeString() + '\n' + state.lastUpdate.toLocaleDateString();
2120
+ }
2121
+ }
2122
+
2123
+ // Chart Functions
2124
+ function initializeCharts() {
2125
+ // Health Chart
2126
+ const healthCtx = document.getElementById('healthChart').getContext('2d');
2127
+ state.charts.health = new Chart(healthCtx, {
2128
+ type: 'line',
2129
+ data: {
2130
+ labels: [],
2131
+ datasets: [{
2132
+ label: 'System Health %',
2133
+ data: [],
2134
+ borderColor: '#8b5cf6',
2135
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
2136
+ borderWidth: 3,
2137
+ fill: true,
2138
+ tension: 0.4
2139
+ }]
2140
+ },
2141
+ options: {
2142
+ responsive: true,
2143
+ maintainAspectRatio: false,
2144
+ plugins: {
2145
+ legend: {
2146
+ display: false
2147
+ }
2148
+ },
2149
+ scales: {
2150
+ y: {
2151
+ beginAtZero: true,
2152
+ max: 100,
2153
+ grid: {
2154
+ color: 'rgba(139, 92, 246, 0.1)'
2155
+ }
2156
+ },
2157
+ x: {
2158
+ grid: {
2159
+ color: 'rgba(139, 92, 246, 0.1)'
2160
+ }
2161
+ }
2162
+ }
2163
+ }
2164
+ });
2165
+
2166
+ // Status Chart
2167
+ const statusCtx = document.getElementById('statusChart').getContext('2d');
2168
+ state.charts.status = new Chart(statusCtx, {
2169
+ type: 'doughnut',
2170
+ data: {
2171
+ labels: ['Online', 'Degraded', 'Offline'],
2172
+ datasets: [{
2173
+ data: [0, 0, 0],
2174
+ backgroundColor: [
2175
+ '#10b981',
2176
+ '#f59e0b',
2177
+ '#ef4444'
2178
+ ],
2179
+ borderWidth: 0
2180
+ }]
2181
+ },
2182
+ options: {
2183
+ responsive: true,
2184
+ maintainAspectRatio: false,
2185
+ cutout: '70%',
2186
+ plugins: {
2187
+ legend: {
2188
+ position: 'bottom'
2189
+ }
2190
+ }
2191
+ }
2192
+ });
2193
+ }
2194
+
2195
+ function updateStatusChart(providers) {
2196
+ if (!state.charts.status || !providers) return;
2197
+
2198
+ const online = providers.filter(p => p.status === 'online' || p.status === 'healthy').length;
2199
+ const degraded = providers.filter(p => p.status === 'degraded' || p.status === 'warning').length;
2200
+ const offline = providers.filter(p => p.status === 'offline' || p.status === 'error').length;
2201
+
2202
+ state.charts.status.data.datasets[0].data = [online, degraded, offline];
2203
+ state.charts.status.update();
2204
+ }
2205
+
2206
+ // Tab Management
2207
+ function switchTab(event, tabName) {
2208
+ // Remove active class from all tabs
2209
+ document.querySelectorAll('.tab').forEach(tab => {
2210
+ tab.classList.remove('active');
2211
+ });
2212
+
2213
+ // Remove active class from all contents
2214
+ document.querySelectorAll('.tab-content').forEach(content => {
2215
+ content.classList.remove('active');
2216
+ });
2217
+
2218
+ // Add active class to clicked tab
2219
+ event.currentTarget.classList.add('active');
2220
+
2221
+ // Show corresponding content
2222
+ document.getElementById(`tab-${tabName}`).classList.add('active');
2223
+
2224
+ // Load data for the tab
2225
+ switch(tabName) {
2226
+ case 'dashboard':
2227
+ loadProviders();
2228
+ break;
2229
+ case 'providers':
2230
+ loadProviders();
2231
+ break;
2232
+ case 'categories':
2233
+ loadCategories();
2234
+ break;
2235
+ case 'ratelimits':
2236
+ loadRateLimits();
2237
+ break;
2238
+ case 'logs':
2239
+ loadLogs();
2240
+ break;
2241
+ case 'alerts':
2242
+ loadAlerts();
2243
+ break;
2244
+ case 'huggingface':
2245
+ loadHFHealth();
2246
+ loadHFModels();
2247
+ loadHFDatasets();
2248
+ break;
2249
+ }
2250
+
2251
+ state.currentTab = tabName;
2252
+ }
2253
+
2254
+ // Utility Functions
2255
+ function showLoading() {
2256
+ document.getElementById('loadingOverlay').classList.add('active');
2257
+ }
2258
+
2259
+ function hideLoading() {
2260
+ document.getElementById('loadingOverlay').classList.remove('active');
2261
+ }
2262
+
2263
+ function showToast(title, message, type = 'info') {
2264
+ const container = document.getElementById('toastContainer');
2265
+ const toast = document.createElement('div');
2266
+ toast.className = `toast ${type}`;
2267
+ toast.innerHTML = `
2268
+ <div class="toast-content">
2269
+ <div class="toast-title">${title}</div>
2270
+ <div class="toast-message">${message}</div>
2271
+ </div>
2272
+ <button onclick="this.parentElement.remove()" style="background: none; border: none; cursor: pointer; color: inherit;">
2273
+ <svg class="icon" style="width: 16px; height: 16px;">
2274
+ <line x1="18" y1="6" x2="6" y2="18"></line>
2275
+ <line x1="6" y1="6" x2="18" y2="18"></line>
2276
+ </svg>
2277
+ </button>
2278
+ `;
2279
+
2280
+ container.appendChild(toast);
2281
+
2282
+ // Auto remove after 5 seconds
2283
+ setTimeout(() => {
2284
+ if (toast.parentElement) {
2285
+ toast.remove();
2286
+ }
2287
+ }, 5000);
2288
+ }
2289
+
2290
+ function addAlert(alert) {
2291
+ const container = document.getElementById('alertsContainer');
2292
+ const alertEl = document.createElement('div');
2293
+ alertEl.className = `alert ${getAlertClass(alert.severity)}`;
2294
+ alertEl.innerHTML = `
2295
+ <div class="alert-content">
2296
+ <div class="alert-title">
2297
+ ${alert.title}
2298
+ <span style="font-size: 11px; margin-left: 8px; opacity: 0.8;">
2299
+ ${formatTimeAgo(alert.timestamp)}
2300
+ </span>
2301
+ </div>
2302
+ <div class="alert-message">${alert.message}</div>
2303
+ </div>
2304
+ `;
2305
+
2306
+ container.appendChild(alertEl);
2307
+
2308
+ // Auto remove after 10 seconds
2309
+ setTimeout(() => {
2310
+ if (alertEl.parentElement) {
2311
+ alertEl.remove();
2312
+ }
2313
+ }, 10000);
2314
+ }
2315
+
2316
+ function refreshAll() {
2317
+ console.log('🔄 Refreshing all data...');
2318
+ loadInitialData();
2319
+
2320
+ // Refresh current tab data
2321
+ switch(state.currentTab) {
2322
+ case 'categories':
2323
+ loadCategories();
2324
+ break;
2325
+ case 'ratelimits':
2326
+ loadRateLimits();
2327
+ break;
2328
+ case 'logs':
2329
+ loadLogs();
2330
+ break;
2331
+ case 'alerts':
2332
+ loadAlerts();
2333
+ break;
2334
+ case 'huggingface':
2335
+ loadHFHealth();
2336
+ loadHFModels();
2337
+ loadHFDatasets();
2338
+ break;
2339
+ }
2340
+ }
2341
+
2342
+ function startAutoRefresh() {
2343
+ setInterval(() => {
2344
+ if (state.autoRefreshEnabled && state.wsConnected) {
2345
+ console.log('🔄 Auto-refreshing data...');
2346
+ refreshAll();
2347
+ }
2348
+ }, config.autoRefreshInterval);
2349
+ }
2350
+ </script>
2351
+ </body>
2352
+ </html>