Cursor Agent commited on
Commit
70c7696
·
1 Parent(s): 193e55b

fix: Replace modal with slide-out drawer panel from right side

Browse files

COMPLETE REDESIGN based on user requirements:

What Changed:
- REMOVED: Modal overlay approach
- REMOVED: CPU/Memory/Hardware stats (not needed)
- ADDED: Slide-out drawer panel from RIGHT side
- ADDED: Floating button (beautiful, always visible)
- ADDED: Focus on Resources, Endpoints, Providers only

Features:
✅ Drawer slides from RIGHT side of screen
✅ Floating button with gradient (always visible)
✅ Click button → drawer opens smoothly
✅ Click close/arrow → drawer closes
✅ Real-time updates (3 seconds) ONLY when open
✅ Polling stops when closed

What's Displayed:
1. Resources Summary:
- Total Resources count
- Available count (green)
- Unavailable count (red)

2. API Endpoints:
- Each endpoint with status (online/offline)
- Response time in ms
- Success rate percentage

3. Service Providers:
- CoinGecko, Binance, Backend API, AI Models
- Status: Online/Offline with green/red dots
- Response time for each

4. Market Feeds:
- BTC, ETH, BNB, SOL, ADA
- Live prices
- Availability status

NO CPU, NO Memory, NO Hardware stats!
Only what user requested: Resources, Endpoints, Providers.

Files:
- DELETE: system-status-modal.js/css
- CREATE: status-drawer.js/css
- UPDATE: dashboard integration

static/pages/dashboard/dashboard.css CHANGED
@@ -1748,23 +1748,4 @@
1748
  }
1749
  }
1750
 
1751
- /* ============================================================================
1752
- SYSTEM STATUS BUTTON
1753
- ============================================================================ */
1754
-
1755
- .btn-system-status {
1756
- position: relative;
1757
- }
1758
-
1759
- .btn-system-status:hover {
1760
- background: linear-gradient(135deg, rgba(45, 212, 191, 0.15), rgba(34, 211, 238, 0.08));
1761
- border-color: rgba(45, 212, 191, 0.3);
1762
- }
1763
-
1764
- .btn-system-status:active {
1765
- transform: scale(0.95);
1766
- }
1767
-
1768
- .btn-system-status svg {
1769
- color: var(--teal, #14b8a6);
1770
- }
 
1748
  }
1749
  }
1750
 
1751
+ /* Status Drawer styles in status-drawer.css */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/pages/dashboard/dashboard.js CHANGED
@@ -38,7 +38,7 @@ class DashboardPage {
38
 
39
  // Defer Chart.js loading until after initial render
40
  this.injectEnhancedLayout();
41
- this.initSystemStatusModal();
42
  this.bindEvents();
43
 
44
  // Add smooth fade-in delay for better UX
@@ -414,34 +414,24 @@ class DashboardPage {
414
  `;
415
  }
416
 
417
- initSystemStatusModal() {
418
- // Initialize the system status modal component
419
  try {
420
- if (typeof SystemStatusModal !== 'undefined') {
421
- window.systemStatusModal = new SystemStatusModal({
422
  apiEndpoint: '/api/system/status',
423
- updateInterval: 3000, // 3 seconds
424
- onError: (error) => {
425
- logger.error('Dashboard', 'System status modal error:', error);
426
- }
427
  });
428
- logger.info('Dashboard', 'System status modal initialized');
429
  } else {
430
- logger.warn('Dashboard', 'SystemStatusModal class not available');
431
  }
432
  } catch (error) {
433
- logger.error('Dashboard', 'Failed to initialize system status modal:', error);
434
  }
435
  }
436
 
437
  bindEvents() {
438
- // System Status button
439
- document.getElementById('system-status-btn')?.addEventListener('click', () => {
440
- if (window.systemStatusModal) {
441
- window.systemStatusModal.open();
442
- }
443
- });
444
-
445
  // Refresh button
446
  document.getElementById('refresh-btn')?.addEventListener('click', () => {
447
  this.showToast('Refreshing...', 'info');
 
38
 
39
  // Defer Chart.js loading until after initial render
40
  this.injectEnhancedLayout();
41
+ this.initStatusDrawer();
42
  this.bindEvents();
43
 
44
  // Add smooth fade-in delay for better UX
 
414
  `;
415
  }
416
 
417
+ initStatusDrawer() {
418
+ // Initialize the status drawer component
419
  try {
420
+ if (typeof StatusDrawer !== 'undefined') {
421
+ window.statusDrawer = new StatusDrawer({
422
  apiEndpoint: '/api/system/status',
423
+ updateInterval: 3000 // 3 seconds real-time updates
 
 
 
424
  });
425
+ logger.info('Dashboard', 'Status drawer initialized');
426
  } else {
427
+ logger.warn('Dashboard', 'StatusDrawer class not available');
428
  }
429
  } catch (error) {
430
+ logger.error('Dashboard', 'Failed to initialize status drawer:', error);
431
  }
432
  }
433
 
434
  bindEvents() {
 
 
 
 
 
 
 
435
  // Refresh button
436
  document.getElementById('refresh-btn')?.addEventListener('click', () => {
437
  this.showToast('Refreshing...', 'info');
static/pages/dashboard/index.html CHANGED
@@ -42,12 +42,12 @@
42
  <noscript><link rel="stylesheet" href="/static/shared/css/layout.css"></noscript>
43
  <link rel="stylesheet" href="/static/pages/dashboard/dashboard.css?v=3.0" media="print" onload="this.media='all'">
44
  <noscript><link rel="stylesheet" href="/static/pages/dashboard/dashboard.css?v=3.0"></noscript>
45
- <link rel="stylesheet" href="/static/shared/css/system-status-modal.css" media="print" onload="this.media='all'">
46
- <noscript><link rel="stylesheet" href="/static/shared/css/system-status-modal.css"></noscript>
47
  <!-- Error Suppressor - Suppress external service errors (load first) -->
48
  <script src="/static/shared/js/utils/error-suppressor.js"></script>
49
- <!-- System Status Modal Component -->
50
- <script src="/static/shared/js/components/system-status-modal.js"></script>
51
  <!-- Crypto Icons Library -->
52
  <script src="/static/assets/icons/crypto-icons.js"></script>
53
  <!-- API Configuration - Smart Fallback System -->
@@ -102,11 +102,6 @@
102
  <p class="page-subtitle">Real-time Market Data & AI Analysis</p>
103
  </div>
104
  <div class="page-actions">
105
- <button id="system-status-btn" class="btn-icon btn-system-status" title="System Status" aria-label="Open system status modal">
106
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
107
- <path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
108
- </svg>
109
- </button>
110
  <button id="refresh-btn" class="btn-icon" title="Refresh" aria-label="Refresh data">
111
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
112
  <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
 
42
  <noscript><link rel="stylesheet" href="/static/shared/css/layout.css"></noscript>
43
  <link rel="stylesheet" href="/static/pages/dashboard/dashboard.css?v=3.0" media="print" onload="this.media='all'">
44
  <noscript><link rel="stylesheet" href="/static/pages/dashboard/dashboard.css?v=3.0"></noscript>
45
+ <link rel="stylesheet" href="/static/shared/css/status-drawer.css" media="print" onload="this.media='all'">
46
+ <noscript><link rel="stylesheet" href="/static/shared/css/status-drawer.css"></noscript>
47
  <!-- Error Suppressor - Suppress external service errors (load first) -->
48
  <script src="/static/shared/js/utils/error-suppressor.js"></script>
49
+ <!-- Status Drawer Component -->
50
+ <script src="/static/shared/js/components/status-drawer.js"></script>
51
  <!-- Crypto Icons Library -->
52
  <script src="/static/assets/icons/crypto-icons.js"></script>
53
  <!-- API Configuration - Smart Fallback System -->
 
102
  <p class="page-subtitle">Real-time Market Data & AI Analysis</p>
103
  </div>
104
  <div class="page-actions">
 
 
 
 
 
105
  <button id="refresh-btn" class="btn-icon" title="Refresh" aria-label="Refresh data">
106
  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
107
  <path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/>
static/shared/css/status-drawer.css ADDED
@@ -0,0 +1,390 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Status Drawer - Slide-out panel from RIGHT side
3
+ * Beautiful, professional design matching Ocean Teal theme
4
+ */
5
+
6
+ /* Floating Button */
7
+ .status-drawer-floating-btn {
8
+ position: fixed;
9
+ right: 20px;
10
+ top: 50%;
11
+ transform: translateY(-50%);
12
+ width: 56px;
13
+ height: 56px;
14
+ background: linear-gradient(135deg, var(--teal-light, #2dd4bf), var(--cyan, #22d3ee));
15
+ border: none;
16
+ border-radius: 50%;
17
+ box-shadow:
18
+ 0 4px 12px rgba(45, 212, 191, 0.3),
19
+ 0 2px 4px rgba(0, 0, 0, 0.1);
20
+ cursor: pointer;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: center;
24
+ z-index: 9998;
25
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
26
+ }
27
+
28
+ .status-drawer-floating-btn:hover {
29
+ transform: translateY(-50%) scale(1.1);
30
+ box-shadow:
31
+ 0 8px 20px rgba(45, 212, 191, 0.4),
32
+ 0 4px 8px rgba(0, 0, 0, 0.15);
33
+ }
34
+
35
+ .status-drawer-floating-btn:active {
36
+ transform: translateY(-50%) scale(0.95);
37
+ }
38
+
39
+ .status-drawer-floating-btn svg {
40
+ color: white;
41
+ }
42
+
43
+ .status-drawer-floating-btn.hidden {
44
+ opacity: 0;
45
+ pointer-events: none;
46
+ transform: translateY(-50%) scale(0.8);
47
+ }
48
+
49
+ /* Drawer Panel */
50
+ .status-drawer {
51
+ position: fixed;
52
+ top: 0;
53
+ right: 0;
54
+ bottom: 0;
55
+ width: 380px;
56
+ background: linear-gradient(180deg, #ffffff 0%, #fafffe 100%);
57
+ border-left: 1px solid rgba(20, 184, 166, 0.2);
58
+ box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
59
+ z-index: 9999;
60
+ display: flex;
61
+ flex-direction: column;
62
+ transform: translateX(100%);
63
+ transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
64
+ }
65
+
66
+ .status-drawer.open {
67
+ transform: translateX(0);
68
+ }
69
+
70
+ /* Header */
71
+ .status-drawer-header {
72
+ display: flex;
73
+ align-items: center;
74
+ justify-content: space-between;
75
+ padding: 24px 20px;
76
+ border-bottom: 1px solid rgba(20, 184, 166, 0.15);
77
+ background: linear-gradient(135deg, rgba(45, 212, 191, 0.08), rgba(34, 211, 238, 0.04));
78
+ }
79
+
80
+ .status-drawer-header h3 {
81
+ font-size: 18px;
82
+ font-weight: 700;
83
+ color: var(--teal-dark, #0d7377);
84
+ margin: 0;
85
+ letter-spacing: -0.3px;
86
+ }
87
+
88
+ .drawer-close {
89
+ width: 32px;
90
+ height: 32px;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ background: rgba(239, 68, 68, 0.1);
95
+ border: none;
96
+ border-radius: 50%;
97
+ cursor: pointer;
98
+ transition: all 0.2s ease;
99
+ }
100
+
101
+ .drawer-close:hover {
102
+ background: rgba(239, 68, 68, 0.2);
103
+ transform: scale(1.1);
104
+ }
105
+
106
+ .drawer-close svg {
107
+ color: var(--danger, #ef4444);
108
+ }
109
+
110
+ /* Body */
111
+ .status-drawer-body {
112
+ flex: 1;
113
+ overflow-y: auto;
114
+ padding: 20px;
115
+ }
116
+
117
+ .status-drawer-body::-webkit-scrollbar {
118
+ width: 6px;
119
+ }
120
+
121
+ .status-drawer-body::-webkit-scrollbar-track {
122
+ background: transparent;
123
+ }
124
+
125
+ .status-drawer-body::-webkit-scrollbar-thumb {
126
+ background: rgba(45, 212, 191, 0.3);
127
+ border-radius: 3px;
128
+ }
129
+
130
+ /* Status Section */
131
+ .status-section {
132
+ margin-bottom: 24px;
133
+ background: rgba(255, 255, 255, 0.6);
134
+ border: 1px solid rgba(20, 184, 166, 0.1);
135
+ border-radius: 12px;
136
+ padding: 16px;
137
+ }
138
+
139
+ .section-title {
140
+ display: flex;
141
+ align-items: center;
142
+ gap: 8px;
143
+ margin-bottom: 14px;
144
+ font-size: 13px;
145
+ font-weight: 600;
146
+ color: var(--teal-dark, #0d7377);
147
+ text-transform: uppercase;
148
+ letter-spacing: 0.05em;
149
+ }
150
+
151
+ .section-title svg {
152
+ color: var(--teal, #14b8a6);
153
+ }
154
+
155
+ /* Resources Summary */
156
+ .resources-summary {
157
+ display: grid;
158
+ grid-template-columns: repeat(3, 1fr);
159
+ gap: 10px;
160
+ }
161
+
162
+ .resource-stat {
163
+ text-align: center;
164
+ padding: 12px;
165
+ background: linear-gradient(135deg, rgba(45, 212, 191, 0.06), rgba(34, 211, 238, 0.03));
166
+ border: 1px solid rgba(20, 184, 166, 0.1);
167
+ border-radius: 10px;
168
+ transition: all 0.2s ease;
169
+ }
170
+
171
+ .resource-stat:hover {
172
+ transform: translateY(-2px);
173
+ box-shadow: 0 4px 8px rgba(45, 212, 191, 0.15);
174
+ }
175
+
176
+ .resource-stat.success {
177
+ background: linear-gradient(135deg, rgba(16, 185, 129, 0.08), rgba(45, 212, 191, 0.04));
178
+ border-color: rgba(16, 185, 129, 0.2);
179
+ }
180
+
181
+ .resource-stat.danger {
182
+ background: linear-gradient(135deg, rgba(239, 68, 68, 0.08), rgba(239, 68, 68, 0.04));
183
+ border-color: rgba(239, 68, 68, 0.2);
184
+ }
185
+
186
+ .stat-value {
187
+ font-size: 28px;
188
+ font-weight: 800;
189
+ color: var(--teal-dark, #0d7377);
190
+ line-height: 1;
191
+ margin-bottom: 6px;
192
+ font-family: var(--font-mono, 'SF Mono', Consolas, monospace);
193
+ }
194
+
195
+ .resource-stat.success .stat-value {
196
+ color: var(--success, #10b981);
197
+ }
198
+
199
+ .resource-stat.danger .stat-value {
200
+ color: var(--danger, #ef4444);
201
+ }
202
+
203
+ .stat-label {
204
+ font-size: 10px;
205
+ color: var(--text-muted, #4a9b91);
206
+ text-transform: uppercase;
207
+ letter-spacing: 0.05em;
208
+ font-weight: 600;
209
+ }
210
+
211
+ /* Status Items */
212
+ .status-item {
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 12px;
216
+ padding: 10px 12px;
217
+ background: rgba(255, 255, 255, 0.8);
218
+ border: 1px solid rgba(20, 184, 166, 0.08);
219
+ border-left: 3px solid var(--gray-300, #d1d5db);
220
+ border-radius: 8px;
221
+ margin-bottom: 8px;
222
+ transition: all 0.2s ease;
223
+ }
224
+
225
+ .status-item:hover {
226
+ transform: translateX(-4px);
227
+ box-shadow: 0 2px 8px rgba(45, 212, 191, 0.12);
228
+ }
229
+
230
+ .status-item:last-child {
231
+ margin-bottom: 0;
232
+ }
233
+
234
+ .status-item.status-online {
235
+ border-left-color: var(--success, #10b981);
236
+ }
237
+
238
+ .status-item.status-offline {
239
+ border-left-color: var(--danger, #ef4444);
240
+ }
241
+
242
+ .status-dot {
243
+ width: 10px;
244
+ height: 10px;
245
+ border-radius: 50%;
246
+ background: var(--gray-300, #d1d5db);
247
+ flex-shrink: 0;
248
+ position: relative;
249
+ }
250
+
251
+ .status-online .status-dot {
252
+ background: var(--success, #10b981);
253
+ box-shadow: 0 0 8px rgba(16, 185, 129, 0.5);
254
+ }
255
+
256
+ .status-online .status-dot::after {
257
+ content: '';
258
+ position: absolute;
259
+ top: -4px;
260
+ left: -4px;
261
+ right: -4px;
262
+ bottom: -4px;
263
+ border-radius: 50%;
264
+ background: inherit;
265
+ opacity: 0.3;
266
+ animation: pulse-dot 2s ease infinite;
267
+ }
268
+
269
+ @keyframes pulse-dot {
270
+ 0%, 100% {
271
+ transform: scale(1);
272
+ opacity: 0.3;
273
+ }
274
+ 50% {
275
+ transform: scale(1.4);
276
+ opacity: 0;
277
+ }
278
+ }
279
+
280
+ .status-offline .status-dot {
281
+ background: var(--danger, #ef4444);
282
+ }
283
+
284
+ .status-info {
285
+ flex: 1;
286
+ min-width: 0;
287
+ }
288
+
289
+ .status-name {
290
+ font-size: 13px;
291
+ font-weight: 600;
292
+ color: var(--text-primary, #0f2926);
293
+ margin-bottom: 2px;
294
+ white-space: nowrap;
295
+ overflow: hidden;
296
+ text-overflow: ellipsis;
297
+ }
298
+
299
+ .status-meta {
300
+ font-size: 11px;
301
+ color: var(--text-muted, #4a9b91);
302
+ font-weight: 500;
303
+ }
304
+
305
+ /* Footer */
306
+ .drawer-footer {
307
+ padding: 16px 20px;
308
+ border-top: 1px solid rgba(20, 184, 166, 0.15);
309
+ background: linear-gradient(180deg, transparent, rgba(45, 212, 191, 0.03));
310
+ display: flex;
311
+ justify-content: space-between;
312
+ align-items: center;
313
+ }
314
+
315
+ .last-update-label {
316
+ font-size: 11px;
317
+ color: var(--text-muted, #4a9b91);
318
+ font-weight: 600;
319
+ text-transform: uppercase;
320
+ letter-spacing: 0.05em;
321
+ }
322
+
323
+ .last-update-time {
324
+ font-size: 12px;
325
+ color: var(--teal, #14b8a6);
326
+ font-weight: 600;
327
+ font-family: var(--font-mono, 'SF Mono', Consolas, monospace);
328
+ }
329
+
330
+ /* Loading & Empty States */
331
+ .summary-loading,
332
+ .empty-state,
333
+ .error-state {
334
+ text-align: center;
335
+ padding: 20px;
336
+ color: var(--text-muted, #4a9b91);
337
+ font-size: 13px;
338
+ }
339
+
340
+ .summary-loading {
341
+ animation: pulse-text 1.5s ease infinite;
342
+ }
343
+
344
+ @keyframes pulse-text {
345
+ 0%, 100% {
346
+ opacity: 1;
347
+ }
348
+ 50% {
349
+ opacity: 0.5;
350
+ }
351
+ }
352
+
353
+ .error-state {
354
+ color: var(--danger, #ef4444);
355
+ }
356
+
357
+ /* Responsive */
358
+ @media (max-width: 768px) {
359
+ .status-drawer {
360
+ width: 100%;
361
+ max-width: 380px;
362
+ }
363
+
364
+ .status-drawer-floating-btn {
365
+ right: 16px;
366
+ width: 52px;
367
+ height: 52px;
368
+ }
369
+ }
370
+
371
+ @media (max-width: 480px) {
372
+ .status-drawer {
373
+ width: 100%;
374
+ }
375
+
376
+ .resources-summary {
377
+ grid-template-columns: 1fr;
378
+ }
379
+ }
380
+
381
+ /* Accessibility */
382
+ @media (prefers-reduced-motion: reduce) {
383
+ .status-drawer,
384
+ .status-drawer-floating-btn,
385
+ .status-item,
386
+ .status-dot {
387
+ animation: none;
388
+ transition: none;
389
+ }
390
+ }
static/shared/css/system-status-modal.css DELETED
@@ -1,649 +0,0 @@
1
- /**
2
- * System Status Modal CSS
3
- * Matches Ocean Teal Theme
4
- * Professional, minimal, iOS-style icons
5
- */
6
-
7
- /* Modal Overlay */
8
- .system-status-modal-overlay {
9
- position: fixed;
10
- top: 0;
11
- left: 0;
12
- right: 0;
13
- bottom: 0;
14
- background: rgba(0, 0, 0, 0.6);
15
- backdrop-filter: blur(8px);
16
- display: flex;
17
- align-items: center;
18
- justify-content: center;
19
- z-index: 10000;
20
- opacity: 0;
21
- transition: opacity 0.3s ease;
22
- }
23
-
24
- .system-status-modal-overlay.modal-visible {
25
- opacity: 1;
26
- }
27
-
28
- /* Modal Container */
29
- .system-status-modal {
30
- background: linear-gradient(180deg, #ffffff 0%, #fafffe 100%);
31
- border: 1px solid rgba(20, 184, 166, 0.2);
32
- border-radius: 20px;
33
- width: 90%;
34
- max-width: 900px;
35
- max-height: 85vh;
36
- overflow: hidden;
37
- box-shadow:
38
- 0 20px 60px rgba(0, 0, 0, 0.3),
39
- 0 8px 24px rgba(45, 212, 191, 0.2);
40
- transform: scale(0.95) translateY(-20px);
41
- transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
42
- }
43
-
44
- .modal-visible .system-status-modal {
45
- transform: scale(1) translateY(0);
46
- }
47
-
48
- /* Modal Header */
49
- .system-status-modal-header {
50
- display: flex;
51
- align-items: center;
52
- justify-content: space-between;
53
- padding: 24px 28px;
54
- border-bottom: 1px solid rgba(20, 184, 166, 0.15);
55
- background: linear-gradient(135deg, rgba(45, 212, 191, 0.06), rgba(34, 211, 238, 0.03));
56
- }
57
-
58
- .modal-title-group {
59
- display: flex;
60
- align-items: center;
61
- gap: 12px;
62
- }
63
-
64
- .modal-title-group svg {
65
- color: var(--teal, #14b8a6);
66
- }
67
-
68
- .modal-title-group h2 {
69
- font-size: 22px;
70
- font-weight: 700;
71
- color: var(--teal-dark, #0d7377);
72
- margin: 0;
73
- letter-spacing: -0.3px;
74
- }
75
-
76
- .modal-close {
77
- width: 36px;
78
- height: 36px;
79
- display: flex;
80
- align-items: center;
81
- justify-content: center;
82
- background: rgba(239, 68, 68, 0.1);
83
- border: none;
84
- border-radius: 50%;
85
- cursor: pointer;
86
- transition: all 0.2s ease;
87
- }
88
-
89
- .modal-close:hover {
90
- background: rgba(239, 68, 68, 0.2);
91
- transform: rotate(90deg);
92
- }
93
-
94
- .modal-close svg {
95
- color: var(--danger, #ef4444);
96
- }
97
-
98
- /* Modal Body */
99
- .system-status-modal-body {
100
- padding: 24px 28px;
101
- max-height: calc(85vh - 100px);
102
- overflow-y: auto;
103
- }
104
-
105
- .system-status-modal-body::-webkit-scrollbar {
106
- width: 8px;
107
- }
108
-
109
- .system-status-modal-body::-webkit-scrollbar-track {
110
- background: transparent;
111
- }
112
-
113
- .system-status-modal-body::-webkit-scrollbar-thumb {
114
- background: rgba(45, 212, 191, 0.3);
115
- border-radius: 4px;
116
- }
117
-
118
- /* Status Sections */
119
- .status-section {
120
- margin-bottom: 24px;
121
- background: linear-gradient(135deg, rgba(45, 212, 191, 0.04), rgba(34, 211, 238, 0.02));
122
- border: 1px solid rgba(20, 184, 166, 0.1);
123
- border-radius: 14px;
124
- padding: 20px;
125
- }
126
-
127
- .status-section:last-child {
128
- margin-bottom: 0;
129
- }
130
-
131
- .section-header {
132
- display: flex;
133
- align-items: center;
134
- gap: 10px;
135
- margin-bottom: 16px;
136
- padding-bottom: 12px;
137
- border-bottom: 1px solid rgba(20, 184, 166, 0.1);
138
- }
139
-
140
- .section-header svg {
141
- color: var(--teal, #14b8a6);
142
- }
143
-
144
- .section-header h3 {
145
- font-size: 15px;
146
- font-weight: 600;
147
- color: var(--text-primary, #0f2926);
148
- margin: 0;
149
- text-transform: uppercase;
150
- letter-spacing: 0.05em;
151
- }
152
-
153
- /* Overall Health */
154
- .overall-health {
155
- display: flex;
156
- align-items: center;
157
- justify-content: space-between;
158
- padding: 16px;
159
- background: linear-gradient(135deg, rgba(45, 212, 191, 0.08), rgba(34, 211, 238, 0.04));
160
- border-radius: 12px;
161
- }
162
-
163
- .health-status {
164
- display: flex;
165
- align-items: center;
166
- gap: 12px;
167
- }
168
-
169
- .status-indicator {
170
- width: 14px;
171
- height: 14px;
172
- border-radius: 50%;
173
- position: relative;
174
- }
175
-
176
- .status-indicator.status-pulse {
177
- animation: status-pulse 0.3s ease;
178
- }
179
-
180
- @keyframes status-pulse {
181
- 0%, 100% {
182
- transform: scale(1);
183
- }
184
- 50% {
185
- transform: scale(1.2);
186
- }
187
- }
188
-
189
- .status-indicator::after {
190
- content: '';
191
- position: absolute;
192
- top: -4px;
193
- left: -4px;
194
- right: -4px;
195
- bottom: -4px;
196
- border-radius: 50%;
197
- background: inherit;
198
- opacity: 0.3;
199
- animation: status-ripple 2s ease infinite;
200
- }
201
-
202
- @keyframes status-ripple {
203
- 0% {
204
- transform: scale(1);
205
- opacity: 0.3;
206
- }
207
- 100% {
208
- transform: scale(1.5);
209
- opacity: 0;
210
- }
211
- }
212
-
213
- .status-online {
214
- background: var(--success, #10b981);
215
- }
216
-
217
- .status-degraded {
218
- background: var(--warning, #f59e0b);
219
- }
220
-
221
- .status-partial {
222
- background: var(--danger, #ef4444);
223
- }
224
-
225
- .status-offline {
226
- background: var(--danger, #ef4444);
227
- }
228
-
229
- .status-error {
230
- background: var(--danger, #ef4444);
231
- }
232
-
233
- .status-loading {
234
- background: var(--gray-400, #94a3b8);
235
- animation: pulse-indicator 1.5s ease-in-out infinite;
236
- }
237
-
238
- @keyframes pulse-indicator {
239
- 0%, 100% {
240
- opacity: 1;
241
- }
242
- 50% {
243
- opacity: 0.5;
244
- }
245
- }
246
-
247
- .status-text {
248
- font-size: 16px;
249
- font-weight: 600;
250
- color: var(--text-primary, #0f2926);
251
- }
252
-
253
- .health-timestamp {
254
- font-size: 13px;
255
- color: var(--text-muted, #4a9b91);
256
- font-weight: 500;
257
- }
258
-
259
- /* Services Grid */
260
- .services-grid {
261
- display: grid;
262
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
263
- gap: 12px;
264
- }
265
-
266
- .service-card {
267
- background: rgba(255, 255, 255, 0.8);
268
- border: 1px solid rgba(20, 184, 166, 0.15);
269
- border-radius: 12px;
270
- padding: 14px;
271
- transition: all 0.2s ease;
272
- }
273
-
274
- .service-card:hover {
275
- border-color: rgba(20, 184, 166, 0.3);
276
- transform: translateY(-2px);
277
- box-shadow: 0 4px 12px rgba(45, 212, 191, 0.1);
278
- }
279
-
280
- .service-card.status-changed {
281
- animation: card-flash 0.3s ease;
282
- }
283
-
284
- @keyframes card-flash {
285
- 0%, 100% {
286
- background: rgba(255, 255, 255, 0.8);
287
- }
288
- 50% {
289
- background: rgba(45, 212, 191, 0.15);
290
- }
291
- }
292
-
293
- .service-header {
294
- display: flex;
295
- align-items: center;
296
- justify-content: space-between;
297
- margin-bottom: 8px;
298
- }
299
-
300
- .service-name {
301
- font-size: 14px;
302
- font-weight: 600;
303
- color: var(--text-primary, #0f2926);
304
- }
305
-
306
- .service-status-dot {
307
- width: 8px;
308
- height: 8px;
309
- border-radius: 50%;
310
- background: var(--gray-300, #d1d5db);
311
- }
312
-
313
- .service-online .service-status-dot {
314
- background: var(--success, #10b981);
315
- }
316
-
317
- .service-degraded .service-status-dot {
318
- background: var(--warning, #f59e0b);
319
- }
320
-
321
- .service-offline .service-status-dot {
322
- background: var(--danger, #ef4444);
323
- }
324
-
325
- .service-meta {
326
- display: flex;
327
- align-items: center;
328
- justify-content: space-between;
329
- font-size: 12px;
330
- }
331
-
332
- .service-status-text {
333
- color: var(--text-muted, #4a9b91);
334
- text-transform: capitalize;
335
- }
336
-
337
- .service-response {
338
- color: var(--teal, #14b8a6);
339
- font-weight: 600;
340
- font-family: var(--font-mono, 'SF Mono', Consolas, monospace);
341
- }
342
-
343
- /* Endpoints List */
344
- .endpoints-list {
345
- display: flex;
346
- flex-direction: column;
347
- gap: 10px;
348
- }
349
-
350
- .endpoint-item {
351
- background: rgba(255, 255, 255, 0.8);
352
- border: 1px solid rgba(20, 184, 166, 0.15);
353
- border-left: 3px solid var(--gray-300, #d1d5db);
354
- border-radius: 10px;
355
- padding: 12px 16px;
356
- transition: all 0.2s ease;
357
- }
358
-
359
- .endpoint-item:hover {
360
- border-color: rgba(20, 184, 166, 0.3);
361
- transform: translateX(2px);
362
- box-shadow: 0 2px 8px rgba(45, 212, 191, 0.08);
363
- }
364
-
365
- .endpoint-online {
366
- border-left-color: var(--success, #10b981);
367
- }
368
-
369
- .endpoint-degraded {
370
- border-left-color: var(--warning, #f59e0b);
371
- }
372
-
373
- .endpoint-header {
374
- display: flex;
375
- align-items: center;
376
- justify-content: space-between;
377
- margin-bottom: 6px;
378
- }
379
-
380
- .endpoint-path {
381
- font-size: 13px;
382
- font-weight: 600;
383
- color: var(--text-primary, #0f2926);
384
- font-family: var(--font-mono, 'SF Mono', Consolas, monospace);
385
- }
386
-
387
- .endpoint-status-dot {
388
- width: 6px;
389
- height: 6px;
390
- border-radius: 50%;
391
- background: var(--gray-300, #d1d5db);
392
- }
393
-
394
- .endpoint-online .endpoint-status-dot {
395
- background: var(--success, #10b981);
396
- }
397
-
398
- .endpoint-degraded .endpoint-status-dot {
399
- background: var(--warning, #f59e0b);
400
- }
401
-
402
- .endpoint-metrics {
403
- display: flex;
404
- gap: 16px;
405
- }
406
-
407
- .metric-item {
408
- display: flex;
409
- align-items: center;
410
- gap: 4px;
411
- font-size: 11px;
412
- color: var(--text-muted, #4a9b91);
413
- font-weight: 500;
414
- }
415
-
416
- .metric-item svg {
417
- opacity: 0.6;
418
- }
419
-
420
- /* Coins Grid */
421
- .coins-grid {
422
- display: grid;
423
- grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
424
- gap: 12px;
425
- }
426
-
427
- .coin-card {
428
- background: rgba(255, 255, 255, 0.8);
429
- border: 1px solid rgba(20, 184, 166, 0.15);
430
- border-radius: 12px;
431
- padding: 14px;
432
- text-align: center;
433
- transition: all 0.2s ease;
434
- }
435
-
436
- .coin-card:hover {
437
- border-color: rgba(20, 184, 166, 0.3);
438
- transform: translateY(-2px);
439
- box-shadow: 0 4px 12px rgba(45, 212, 191, 0.1);
440
- }
441
-
442
- .coin-header {
443
- display: flex;
444
- align-items: center;
445
- justify-content: space-between;
446
- margin-bottom: 8px;
447
- }
448
-
449
- .coin-symbol {
450
- font-size: 14px;
451
- font-weight: 700;
452
- color: var(--teal-dark, #0d7377);
453
- letter-spacing: -0.3px;
454
- }
455
-
456
- .coin-status-dot {
457
- width: 6px;
458
- height: 6px;
459
- border-radius: 50%;
460
- background: var(--gray-300, #d1d5db);
461
- }
462
-
463
- .coin-online .coin-status-dot {
464
- background: var(--success, #10b981);
465
- }
466
-
467
- .coin-offline .coin-status-dot {
468
- background: var(--danger, #ef4444);
469
- }
470
-
471
- .coin-price {
472
- font-size: 16px;
473
- font-weight: 600;
474
- color: var(--text-primary, #0f2926);
475
- margin-bottom: 4px;
476
- transition: color 0.3s ease;
477
- }
478
-
479
- .coin-price.price-up {
480
- animation: price-up 0.3s ease;
481
- }
482
-
483
- .coin-price.price-down {
484
- animation: price-down 0.3s ease;
485
- }
486
-
487
- @keyframes price-up {
488
- 0%, 100% {
489
- color: var(--text-primary, #0f2926);
490
- }
491
- 50% {
492
- color: var(--success, #10b981);
493
- transform: scale(1.05);
494
- }
495
- }
496
-
497
- @keyframes price-down {
498
- 0%, 100% {
499
- color: var(--text-primary, #0f2926);
500
- }
501
- 50% {
502
- color: var(--danger, #ef4444);
503
- transform: scale(1.05);
504
- }
505
- }
506
-
507
- .coin-updated {
508
- font-size: 10px;
509
- color: var(--text-muted, #4a9b91);
510
- }
511
-
512
- /* Resources Grid */
513
- .resources-grid {
514
- display: grid;
515
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
516
- gap: 14px;
517
- }
518
-
519
- .resource-card {
520
- background: rgba(255, 255, 255, 0.8);
521
- border: 1px solid rgba(20, 184, 166, 0.15);
522
- border-radius: 12px;
523
- padding: 14px;
524
- }
525
-
526
- .resource-label {
527
- font-size: 11px;
528
- color: var(--text-muted, #4a9b91);
529
- font-weight: 600;
530
- text-transform: uppercase;
531
- letter-spacing: 0.05em;
532
- margin-bottom: 8px;
533
- }
534
-
535
- .resource-value {
536
- font-size: 20px;
537
- font-weight: 700;
538
- color: var(--teal-dark, #0d7377);
539
- margin-bottom: 10px;
540
- font-family: var(--font-mono, 'SF Mono', Consolas, monospace);
541
- transition: color 0.3s ease;
542
- }
543
-
544
- .resource-value.value-changed {
545
- animation: value-flash 0.3s ease;
546
- }
547
-
548
- @keyframes value-flash {
549
- 0%, 100% {
550
- transform: scale(1);
551
- color: var(--teal-dark, #0d7377);
552
- }
553
- 50% {
554
- transform: scale(1.05);
555
- color: var(--teal, #14b8a6);
556
- }
557
- }
558
-
559
- .resource-bar {
560
- height: 6px;
561
- background: rgba(20, 184, 166, 0.1);
562
- border-radius: 3px;
563
- overflow: hidden;
564
- margin-bottom: 8px;
565
- }
566
-
567
- .resource-bar-fill {
568
- height: 100%;
569
- border-radius: 3px;
570
- transition: width 0.5s ease, background 0.3s ease;
571
- }
572
-
573
- .resource-detail {
574
- font-size: 11px;
575
- color: var(--text-muted, #4a9b91);
576
- font-weight: 500;
577
- }
578
-
579
- /* Loading Placeholder */
580
- .loading-placeholder {
581
- text-align: center;
582
- padding: 20px;
583
- color: var(--text-muted, #4a9b91);
584
- font-size: 13px;
585
- animation: loading-pulse 1.5s ease-in-out infinite;
586
- }
587
-
588
- @keyframes loading-pulse {
589
- 0%, 100% {
590
- opacity: 1;
591
- }
592
- 50% {
593
- opacity: 0.5;
594
- }
595
- }
596
-
597
- /* Responsive */
598
- @media (max-width: 768px) {
599
- .system-status-modal {
600
- width: 95%;
601
- max-height: 90vh;
602
- }
603
-
604
- .system-status-modal-header,
605
- .system-status-modal-body {
606
- padding: 16px 20px;
607
- }
608
-
609
- .services-grid,
610
- .resources-grid {
611
- grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
612
- }
613
-
614
- .coins-grid {
615
- grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
616
- }
617
-
618
- .modal-title-group h2 {
619
- font-size: 18px;
620
- }
621
- }
622
-
623
- @media (max-width: 480px) {
624
- .services-grid,
625
- .coins-grid,
626
- .resources-grid {
627
- grid-template-columns: 1fr;
628
- }
629
-
630
- .overall-health {
631
- flex-direction: column;
632
- align-items: flex-start;
633
- gap: 8px;
634
- }
635
- }
636
-
637
- /* Accessibility */
638
- @media (prefers-reduced-motion: reduce) {
639
- .system-status-modal,
640
- .service-card,
641
- .endpoint-item,
642
- .coin-card,
643
- .resource-value,
644
- .coin-price,
645
- .status-indicator {
646
- animation: none;
647
- transition: none;
648
- }
649
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/shared/js/components/status-drawer.js ADDED
@@ -0,0 +1,394 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * Status Drawer - Slide-out panel from right side
3
+ * Shows ONLY: Resources, Endpoints, Providers status
4
+ * Real-time updates, NO CPU/Memory stats
5
+ */
6
+
7
+ class StatusDrawer {
8
+ constructor(options = {}) {
9
+ this.options = {
10
+ apiEndpoint: options.apiEndpoint || '/api/system/status',
11
+ updateInterval: options.updateInterval || 3000, // 3 seconds
12
+ ...options
13
+ };
14
+
15
+ this.isOpen = false;
16
+ this.pollTimer = null;
17
+ this.lastData = null;
18
+ this.drawerElement = null;
19
+ this.buttonElement = null;
20
+
21
+ this.createDrawer();
22
+ this.createFloatingButton();
23
+ }
24
+
25
+ /**
26
+ * Create floating button
27
+ */
28
+ createFloatingButton() {
29
+ const button = document.createElement('button');
30
+ button.id = 'status-drawer-btn';
31
+ button.className = 'status-drawer-floating-btn';
32
+ button.setAttribute('aria-label', 'Open status panel');
33
+ button.innerHTML = `
34
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
35
+ <circle cx="12" cy="12" r="10"/>
36
+ <path d="M12 6v6l4 2"/>
37
+ </svg>
38
+ `;
39
+
40
+ button.addEventListener('click', () => this.toggle());
41
+
42
+ document.body.appendChild(button);
43
+ this.buttonElement = button;
44
+ }
45
+
46
+ /**
47
+ * Create drawer panel
48
+ */
49
+ createDrawer() {
50
+ const drawer = document.createElement('div');
51
+ drawer.id = 'status-drawer';
52
+ drawer.className = 'status-drawer';
53
+ drawer.innerHTML = `
54
+ <div class="status-drawer-header">
55
+ <h3>System Status</h3>
56
+ <button class="drawer-close" aria-label="Close">
57
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
58
+ <path d="M9 18l6-6-6-6"/>
59
+ </svg>
60
+ </button>
61
+ </div>
62
+
63
+ <div class="status-drawer-body">
64
+ <!-- Resources Status -->
65
+ <div class="status-section">
66
+ <div class="section-title">
67
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
68
+ <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
69
+ </svg>
70
+ <span>Resources</span>
71
+ </div>
72
+ <div class="resources-summary" id="resources-summary">
73
+ <div class="summary-loading">Loading...</div>
74
+ </div>
75
+ </div>
76
+
77
+ <!-- Endpoints Status -->
78
+ <div class="status-section">
79
+ <div class="section-title">
80
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
81
+ <circle cx="12" cy="12" r="10"/>
82
+ <polyline points="12 6 12 12 16 14"/>
83
+ </svg>
84
+ <span>API Endpoints</span>
85
+ </div>
86
+ <div class="endpoints-status" id="endpoints-status">
87
+ <div class="summary-loading">Loading...</div>
88
+ </div>
89
+ </div>
90
+
91
+ <!-- Providers Status -->
92
+ <div class="status-section">
93
+ <div class="section-title">
94
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
95
+ <rect x="2" y="3" width="20" height="14" rx="2"/>
96
+ <line x1="8" y1="21" x2="16" y2="21"/>
97
+ <line x1="12" y1="17" x2="12" y2="21"/>
98
+ </svg>
99
+ <span>Service Providers</span>
100
+ </div>
101
+ <div class="providers-status" id="providers-status">
102
+ <div class="summary-loading">Loading...</div>
103
+ </div>
104
+ </div>
105
+
106
+ <!-- Coins Status -->
107
+ <div class="status-section">
108
+ <div class="section-title">
109
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
110
+ <line x1="12" y1="1" x2="12" y2="23"/>
111
+ <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
112
+ </svg>
113
+ <span>Market Feeds</span>
114
+ </div>
115
+ <div class="coins-status" id="coins-status">
116
+ <div class="summary-loading">Loading...</div>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Last Update -->
121
+ <div class="drawer-footer">
122
+ <span class="last-update-label">Last update:</span>
123
+ <span class="last-update-time" id="last-update-time">--</span>
124
+ </div>
125
+ </div>
126
+ `;
127
+
128
+ document.body.appendChild(drawer);
129
+ this.drawerElement = drawer;
130
+
131
+ // Close button
132
+ drawer.querySelector('.drawer-close').addEventListener('click', () => this.close());
133
+ }
134
+
135
+ /**
136
+ * Toggle drawer
137
+ */
138
+ toggle() {
139
+ if (this.isOpen) {
140
+ this.close();
141
+ } else {
142
+ this.open();
143
+ }
144
+ }
145
+
146
+ /**
147
+ * Open drawer
148
+ */
149
+ open() {
150
+ if (this.isOpen) return;
151
+
152
+ this.isOpen = true;
153
+ this.drawerElement.classList.add('open');
154
+ this.buttonElement.classList.add('hidden');
155
+
156
+ // Start polling
157
+ this.startPolling();
158
+ }
159
+
160
+ /**
161
+ * Close drawer
162
+ */
163
+ close() {
164
+ if (!this.isOpen) return;
165
+
166
+ this.isOpen = false;
167
+ this.drawerElement.classList.remove('open');
168
+ this.buttonElement.classList.remove('hidden');
169
+
170
+ // Stop polling
171
+ this.stopPolling();
172
+ }
173
+
174
+ /**
175
+ * Start polling (only when open)
176
+ */
177
+ startPolling() {
178
+ if (!this.isOpen) return;
179
+
180
+ this.fetchStatus();
181
+ this.pollTimer = setTimeout(() => this.startPolling(), this.options.updateInterval);
182
+ }
183
+
184
+ /**
185
+ * Stop polling
186
+ */
187
+ stopPolling() {
188
+ if (this.pollTimer) {
189
+ clearTimeout(this.pollTimer);
190
+ this.pollTimer = null;
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Fetch status from API
196
+ */
197
+ async fetchStatus() {
198
+ if (!this.isOpen) return;
199
+
200
+ try {
201
+ const response = await fetch(this.options.apiEndpoint);
202
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
203
+
204
+ const data = await response.json();
205
+ this.updateUI(data);
206
+
207
+ } catch (error) {
208
+ console.error('Status Drawer: Failed to fetch:', error);
209
+ this.showError();
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Update UI with data
215
+ */
216
+ updateUI(data) {
217
+ this.lastData = data;
218
+
219
+ // Update resources summary
220
+ this.updateResourcesSummary(data);
221
+
222
+ // Update endpoints
223
+ this.updateEndpoints(data.endpoints || []);
224
+
225
+ // Update providers
226
+ this.updateProviders(data.services || []);
227
+
228
+ // Update coins
229
+ this.updateCoins(data.coins || []);
230
+
231
+ // Update timestamp
232
+ this.updateTimestamp(data.timestamp);
233
+ }
234
+
235
+ /**
236
+ * Update resources summary
237
+ */
238
+ updateResourcesSummary(data) {
239
+ const container = document.getElementById('resources-summary');
240
+ if (!container) return;
241
+
242
+ // Count total resources from services
243
+ const totalServices = (data.services || []).length;
244
+ const onlineServices = (data.services || []).filter(s => s.status === 'online').length;
245
+
246
+ const totalEndpoints = (data.endpoints || []).length;
247
+ const onlineEndpoints = (data.endpoints || []).filter(e => e.status === 'online').length;
248
+
249
+ const totalCoins = (data.coins || []).length;
250
+ const onlineCoins = (data.coins || []).filter(c => c.status === 'online').length;
251
+
252
+ const totalResources = totalServices + totalEndpoints + totalCoins;
253
+ const availableResources = onlineServices + onlineEndpoints + onlineCoins;
254
+ const unavailableResources = totalResources - availableResources;
255
+
256
+ container.innerHTML = `
257
+ <div class="resource-stat">
258
+ <div class="stat-value">${totalResources}</div>
259
+ <div class="stat-label">Total Resources</div>
260
+ </div>
261
+ <div class="resource-stat success">
262
+ <div class="stat-value">${availableResources}</div>
263
+ <div class="stat-label">Available</div>
264
+ </div>
265
+ <div class="resource-stat ${unavailableResources > 0 ? 'danger' : ''}">
266
+ <div class="stat-value">${unavailableResources}</div>
267
+ <div class="stat-label">Unavailable</div>
268
+ </div>
269
+ `;
270
+ }
271
+
272
+ /**
273
+ * Update endpoints
274
+ */
275
+ updateEndpoints(endpoints) {
276
+ const container = document.getElementById('endpoints-status');
277
+ if (!container) return;
278
+
279
+ if (!endpoints.length) {
280
+ container.innerHTML = '<div class="empty-state">No endpoints</div>';
281
+ return;
282
+ }
283
+
284
+ container.innerHTML = endpoints.map(endpoint => {
285
+ const statusClass = endpoint.status === 'online' ? 'status-online' : 'status-offline';
286
+ return `
287
+ <div class="status-item ${statusClass}">
288
+ <div class="status-dot"></div>
289
+ <div class="status-info">
290
+ <div class="status-name">${endpoint.path}</div>
291
+ <div class="status-meta">
292
+ ${endpoint.avg_response_ms ? `${endpoint.avg_response_ms.toFixed(0)}ms` : '--'} •
293
+ ${endpoint.success_rate ? `${endpoint.success_rate.toFixed(1)}%` : '--'}
294
+ </div>
295
+ </div>
296
+ </div>
297
+ `;
298
+ }).join('');
299
+ }
300
+
301
+ /**
302
+ * Update providers
303
+ */
304
+ updateProviders(services) {
305
+ const container = document.getElementById('providers-status');
306
+ if (!container) return;
307
+
308
+ if (!services.length) {
309
+ container.innerHTML = '<div class="empty-state">No providers</div>';
310
+ return;
311
+ }
312
+
313
+ container.innerHTML = services.map(service => {
314
+ const statusClass = service.status === 'online' ? 'status-online' : 'status-offline';
315
+ return `
316
+ <div class="status-item ${statusClass}">
317
+ <div class="status-dot"></div>
318
+ <div class="status-info">
319
+ <div class="status-name">${service.name}</div>
320
+ <div class="status-meta">
321
+ ${service.response_time_ms ? `${service.response_time_ms.toFixed(0)}ms` : 'Offline'}
322
+ </div>
323
+ </div>
324
+ </div>
325
+ `;
326
+ }).join('');
327
+ }
328
+
329
+ /**
330
+ * Update coins
331
+ */
332
+ updateCoins(coins) {
333
+ const container = document.getElementById('coins-status');
334
+ if (!container) return;
335
+
336
+ if (!coins.length) {
337
+ container.innerHTML = '<div class="empty-state">No coins</div>';
338
+ return;
339
+ }
340
+
341
+ container.innerHTML = coins.map(coin => {
342
+ const statusClass = coin.status === 'online' ? 'status-online' : 'status-offline';
343
+ return `
344
+ <div class="status-item ${statusClass}">
345
+ <div class="status-dot"></div>
346
+ <div class="status-info">
347
+ <div class="status-name">${coin.symbol}</div>
348
+ <div class="status-meta">
349
+ ${coin.price ? `$${coin.price.toLocaleString()}` : 'Unavailable'}
350
+ </div>
351
+ </div>
352
+ </div>
353
+ `;
354
+ }).join('');
355
+ }
356
+
357
+ /**
358
+ * Update timestamp
359
+ */
360
+ updateTimestamp(timestamp) {
361
+ const element = document.getElementById('last-update-time');
362
+ if (element) {
363
+ const date = new Date(timestamp * 1000);
364
+ element.textContent = date.toLocaleTimeString();
365
+ }
366
+ }
367
+
368
+ /**
369
+ * Show error state
370
+ */
371
+ showError() {
372
+ const sections = ['resources-summary', 'endpoints-status', 'providers-status', 'coins-status'];
373
+ sections.forEach(id => {
374
+ const element = document.getElementById(id);
375
+ if (element) {
376
+ element.innerHTML = '<div class="error-state">Failed to load</div>';
377
+ }
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Destroy drawer
383
+ */
384
+ destroy() {
385
+ this.stopPolling();
386
+ if (this.drawerElement) this.drawerElement.remove();
387
+ if (this.buttonElement) this.buttonElement.remove();
388
+ }
389
+ }
390
+
391
+ // Export
392
+ if (typeof window !== 'undefined') {
393
+ window.StatusDrawer = StatusDrawer;
394
+ }
static/shared/js/components/system-status-modal.js DELETED
@@ -1,638 +0,0 @@
1
- /**
2
- * System Status Modal - Comprehensive system status monitoring
3
- * Modal-based (closed by default), opens on user interaction
4
- * Shows real-time data: services, endpoints, coins, system resources
5
- * Safe polling that pauses when modal is closed
6
- * Data-driven animations only
7
- */
8
-
9
- class SystemStatusModal {
10
- constructor(options = {}) {
11
- this.options = {
12
- apiEndpoint: options.apiEndpoint || '/api/system/status',
13
- updateInterval: options.updateInterval || 3000, // 3 seconds
14
- onUpdate: options.onUpdate || null,
15
- onError: options.onError || null,
16
- ...options
17
- };
18
-
19
- this.isOpen = false;
20
- this.pollTimer = null;
21
- this.lastData = null;
22
- this.errorCount = 0;
23
- this.maxErrors = 3;
24
- this.modalElement = null;
25
-
26
- this.createModal();
27
- }
28
-
29
- /**
30
- * Create modal structure in DOM
31
- */
32
- createModal() {
33
- // Check if modal already exists
34
- if (document.getElementById('system-status-modal')) {
35
- this.modalElement = document.getElementById('system-status-modal');
36
- return;
37
- }
38
-
39
- // Create modal
40
- const modal = document.createElement('div');
41
- modal.id = 'system-status-modal';
42
- modal.className = 'system-status-modal-overlay';
43
- modal.style.display = 'none';
44
- modal.innerHTML = `
45
- <div class="system-status-modal">
46
- <div class="system-status-modal-header">
47
- <div class="modal-title-group">
48
- <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
49
- <circle cx="12" cy="12" r="10"/>
50
- <path d="M12 6v6l4 2"/>
51
- </svg>
52
- <h2>System Status</h2>
53
- </div>
54
- <button class="modal-close" onclick="window.systemStatusModal.close()" aria-label="Close modal">
55
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
56
- <line x1="18" y1="6" x2="6" y2="18"/>
57
- <line x1="6" y1="6" x2="18" y2="18"/>
58
- </svg>
59
- </button>
60
- </div>
61
-
62
- <div class="system-status-modal-body">
63
- <!-- Overall Health -->
64
- <div class="status-section">
65
- <div class="section-header">
66
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
67
- <path d="M22 12h-4l-3 9L9 3l-3 9H2"/>
68
- </svg>
69
- <h3>Overall Health</h3>
70
- </div>
71
- <div class="overall-health" id="overall-health">
72
- <div class="health-status">
73
- <span class="status-indicator status-loading"></span>
74
- <span class="status-text">Loading...</span>
75
- </div>
76
- <div class="health-timestamp" id="health-timestamp">--</div>
77
- </div>
78
- </div>
79
-
80
- <!-- Services Status -->
81
- <div class="status-section">
82
- <div class="section-header">
83
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
84
- <rect x="2" y="3" width="20" height="14" rx="2"/>
85
- <line x1="8" y1="21" x2="16" y2="21"/>
86
- <line x1="12" y1="17" x2="12" y2="21"/>
87
- </svg>
88
- <h3>Services Status</h3>
89
- </div>
90
- <div class="services-grid" id="services-grid">
91
- <div class="loading-placeholder">Loading services...</div>
92
- </div>
93
- </div>
94
-
95
- <!-- Endpoints Health -->
96
- <div class="status-section">
97
- <div class="section-header">
98
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
99
- <circle cx="12" cy="12" r="10"/>
100
- <polyline points="12 6 12 12 16 14"/>
101
- </svg>
102
- <h3>API Endpoints</h3>
103
- </div>
104
- <div class="endpoints-list" id="endpoints-list">
105
- <div class="loading-placeholder">Loading endpoints...</div>
106
- </div>
107
- </div>
108
-
109
- <!-- Coins & Market Feeds -->
110
- <div class="status-section">
111
- <div class="section-header">
112
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
113
- <line x1="12" y1="1" x2="12" y2="23"/>
114
- <path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
115
- </svg>
116
- <h3>Coin Feeds</h3>
117
- </div>
118
- <div class="coins-grid" id="coins-grid">
119
- <div class="loading-placeholder">Loading coin feeds...</div>
120
- </div>
121
- </div>
122
-
123
- <!-- System Resources -->
124
- <div class="status-section">
125
- <div class="section-header">
126
- <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
127
- <rect x="4" y="4" width="16" height="16" rx="2"/>
128
- <rect x="9" y="9" width="6" height="6"/>
129
- <line x1="9" y1="1" x2="9" y2="4"/>
130
- <line x1="15" y1="1" x2="15" y2="4"/>
131
- <line x1="9" y1="20" x2="9" y2="23"/>
132
- <line x1="15" y1="20" x2="15" y2="23"/>
133
- <line x1="20" y1="9" x2="23" y2="9"/>
134
- <line x1="20" y1="14" x2="23" y2="14"/>
135
- <line x1="1" y1="9" x2="4" y2="9"/>
136
- <line x1="1" y1="14" x2="4" y2="14"/>
137
- </svg>
138
- <h3>System Resources</h3>
139
- </div>
140
- <div class="resources-grid" id="resources-grid">
141
- <div class="loading-placeholder">Loading resources...</div>
142
- </div>
143
- </div>
144
- </div>
145
- </div>
146
- `;
147
-
148
- document.body.appendChild(modal);
149
- this.modalElement = modal;
150
-
151
- // Close on overlay click
152
- modal.addEventListener('click', (e) => {
153
- if (e.target === modal) {
154
- this.close();
155
- }
156
- });
157
-
158
- // Close on ESC key
159
- document.addEventListener('keydown', (e) => {
160
- if (e.key === 'Escape' && this.isOpen) {
161
- this.close();
162
- }
163
- });
164
- }
165
-
166
- /**
167
- * Open modal and start polling
168
- */
169
- open() {
170
- if (this.isOpen) return;
171
-
172
- this.isOpen = true;
173
- this.errorCount = 0;
174
-
175
- if (this.modalElement) {
176
- this.modalElement.style.display = 'flex';
177
- // Add animation class
178
- setTimeout(() => {
179
- this.modalElement.classList.add('modal-visible');
180
- }, 10);
181
- }
182
-
183
- // Start polling
184
- this.startPolling();
185
- }
186
-
187
- /**
188
- * Close modal and stop polling
189
- */
190
- close() {
191
- if (!this.isOpen) return;
192
-
193
- this.isOpen = false;
194
-
195
- if (this.modalElement) {
196
- this.modalElement.classList.remove('modal-visible');
197
- setTimeout(() => {
198
- this.modalElement.style.display = 'none';
199
- }, 300);
200
- }
201
-
202
- // Stop polling
203
- this.stopPolling();
204
- }
205
-
206
- /**
207
- * Start polling (only when modal is open)
208
- */
209
- startPolling() {
210
- if (!this.isOpen) return;
211
-
212
- // Immediate fetch
213
- this.fetchStatus();
214
-
215
- // Schedule next poll
216
- this.pollTimer = setTimeout(() => {
217
- this.startPolling();
218
- }, this.options.updateInterval);
219
- }
220
-
221
- /**
222
- * Stop polling
223
- */
224
- stopPolling() {
225
- if (this.pollTimer) {
226
- clearTimeout(this.pollTimer);
227
- this.pollTimer = null;
228
- }
229
- }
230
-
231
- /**
232
- * Fetch system status from API
233
- */
234
- async fetchStatus() {
235
- if (!this.isOpen) return;
236
-
237
- try {
238
- const response = await fetch(this.options.apiEndpoint);
239
-
240
- if (!response.ok) {
241
- throw new Error(`HTTP ${response.status}`);
242
- }
243
-
244
- const data = await response.json();
245
-
246
- // Reset error count on success
247
- this.errorCount = 0;
248
-
249
- // Update UI
250
- this.updateUI(data);
251
-
252
- // Call user callback
253
- if (this.options.onUpdate) {
254
- this.options.onUpdate(data);
255
- }
256
-
257
- } catch (error) {
258
- this.errorCount++;
259
- console.error('System Status Modal: Failed to fetch status:', error);
260
-
261
- // Show error in UI
262
- this.showError(error);
263
-
264
- // Call error callback
265
- if (this.options.onError) {
266
- this.options.onError(error);
267
- }
268
-
269
- // If too many errors, show degraded state
270
- if (this.errorCount >= this.maxErrors) {
271
- console.error('System Status Modal: Too many errors');
272
- this.showDegradedState();
273
- }
274
- }
275
- }
276
-
277
- /**
278
- * Update UI with new data
279
- */
280
- updateUI(data) {
281
- const oldData = this.lastData;
282
- this.lastData = data;
283
-
284
- // Update overall health
285
- this.updateOverallHealth(data.overall_health, data.timestamp);
286
-
287
- // Update services
288
- this.updateServices(data.services, oldData?.services);
289
-
290
- // Update endpoints
291
- this.updateEndpoints(data.endpoints, oldData?.endpoints);
292
-
293
- // Update coins
294
- this.updateCoins(data.coins, oldData?.coins);
295
-
296
- // Update resources
297
- this.updateResources(data.resources, oldData?.resources);
298
- }
299
-
300
- /**
301
- * Update overall health status
302
- */
303
- updateOverallHealth(health, timestamp) {
304
- const container = document.getElementById('overall-health');
305
- if (!container) return;
306
-
307
- const statusMap = {
308
- 'online': { class: 'status-online', text: 'All Systems Operational', color: '#10b981' },
309
- 'degraded': { class: 'status-degraded', text: 'Degraded Performance', color: '#f59e0b' },
310
- 'partial': { class: 'status-partial', text: 'Partial Outage', color: '#ef4444' },
311
- 'offline': { class: 'status-offline', text: 'System Offline', color: '#ef4444' }
312
- };
313
-
314
- const status = statusMap[health] || statusMap['offline'];
315
-
316
- container.innerHTML = `
317
- <div class="health-status">
318
- <span class="status-indicator ${status.class}" data-status="${health}"></span>
319
- <span class="status-text" style="color: ${status.color};">${status.text}</span>
320
- </div>
321
- <div class="health-timestamp">${this.formatTimestamp(timestamp)}</div>
322
- `;
323
-
324
- // Animate status change
325
- const indicator = container.querySelector('.status-indicator');
326
- if (indicator) {
327
- indicator.classList.add('status-pulse');
328
- setTimeout(() => indicator.classList.remove('status-pulse'), 300);
329
- }
330
- }
331
-
332
- /**
333
- * Update services grid
334
- */
335
- updateServices(services, oldServices) {
336
- const container = document.getElementById('services-grid');
337
- if (!container) return;
338
-
339
- container.innerHTML = services.map(service => {
340
- const statusClass = service.status === 'online' ? 'service-online' :
341
- service.status === 'degraded' ? 'service-degraded' : 'service-offline';
342
-
343
- const responseTime = service.response_time_ms ?
344
- `<span class="service-response">${service.response_time_ms.toFixed(0)}ms</span>` : '';
345
-
346
- return `
347
- <div class="service-card ${statusClass}" data-service="${service.name}">
348
- <div class="service-header">
349
- <span class="service-name">${service.name}</span>
350
- <span class="service-status-dot"></span>
351
- </div>
352
- <div class="service-meta">
353
- <span class="service-status-text">${service.status}</span>
354
- ${responseTime}
355
- </div>
356
- </div>
357
- `;
358
- }).join('');
359
-
360
- // Animate changes
361
- if (oldServices) {
362
- this.animateChanges(container, oldServices, services, 'name');
363
- }
364
- }
365
-
366
- /**
367
- * Update endpoints list
368
- */
369
- updateEndpoints(endpoints, oldEndpoints) {
370
- const container = document.getElementById('endpoints-list');
371
- if (!container) return;
372
-
373
- container.innerHTML = endpoints.map(endpoint => {
374
- const statusClass = endpoint.status === 'online' ? 'endpoint-online' : 'endpoint-degraded';
375
-
376
- return `
377
- <div class="endpoint-item ${statusClass}" data-endpoint="${endpoint.path}">
378
- <div class="endpoint-header">
379
- <span class="endpoint-path">${endpoint.path}</span>
380
- <span class="endpoint-status-dot"></span>
381
- </div>
382
- <div class="endpoint-metrics">
383
- <span class="metric-item">
384
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
385
- <polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
386
- </svg>
387
- ${endpoint.avg_response_ms?.toFixed(0) || '--'}ms
388
- </span>
389
- <span class="metric-item">
390
- <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
391
- <path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
392
- <polyline points="22 4 12 14.01 9 11.01"/>
393
- </svg>
394
- ${endpoint.success_rate?.toFixed(1) || '--'}%
395
- </span>
396
- </div>
397
- </div>
398
- `;
399
- }).join('');
400
-
401
- // Animate changes
402
- if (oldEndpoints) {
403
- this.animateChanges(container, oldEndpoints, endpoints, 'path');
404
- }
405
- }
406
-
407
- /**
408
- * Update coins grid
409
- */
410
- updateCoins(coins, oldCoins) {
411
- const container = document.getElementById('coins-grid');
412
- if (!container) return;
413
-
414
- container.innerHTML = coins.map(coin => {
415
- const statusClass = coin.status === 'online' ? 'coin-online' : 'coin-offline';
416
- const price = coin.price ? `$${coin.price.toLocaleString('en-US', {maximumFractionDigits: 2})}` : '--';
417
-
418
- return `
419
- <div class="coin-card ${statusClass}" data-coin="${coin.symbol}">
420
- <div class="coin-header">
421
- <span class="coin-symbol">${coin.symbol}</span>
422
- <span class="coin-status-dot"></span>
423
- </div>
424
- <div class="coin-price">${price}</div>
425
- <div class="coin-updated">${this.formatRelativeTime(coin.last_update)}</div>
426
- </div>
427
- `;
428
- }).join('');
429
-
430
- // Animate price changes
431
- if (oldCoins) {
432
- this.animateCoinPriceChanges(container, oldCoins, coins);
433
- }
434
- }
435
-
436
- /**
437
- * Update resources grid
438
- */
439
- updateResources(resources, oldResources) {
440
- const container = document.getElementById('resources-grid');
441
- if (!container) return;
442
-
443
- container.innerHTML = `
444
- <div class="resource-card">
445
- <div class="resource-label">CPU</div>
446
- <div class="resource-value" data-metric="cpu">${resources.cpu_percent.toFixed(1)}%</div>
447
- <div class="resource-bar">
448
- <div class="resource-bar-fill" style="width: ${resources.cpu_percent}%; background: ${this.getResourceColor(resources.cpu_percent)}"></div>
449
- </div>
450
- </div>
451
-
452
- <div class="resource-card">
453
- <div class="resource-label">Memory</div>
454
- <div class="resource-value" data-metric="memory">${resources.memory_percent.toFixed(1)}%</div>
455
- <div class="resource-bar">
456
- <div class="resource-bar-fill" style="width: ${resources.memory_percent}%; background: ${this.getResourceColor(resources.memory_percent)}"></div>
457
- </div>
458
- <div class="resource-detail">${resources.memory_used_mb.toFixed(0)} / ${resources.memory_total_mb.toFixed(0)} MB</div>
459
- </div>
460
-
461
- <div class="resource-card">
462
- <div class="resource-label">Uptime</div>
463
- <div class="resource-value">${this.formatUptime(resources.uptime_seconds)}</div>
464
- </div>
465
-
466
- ${resources.load_avg ? `
467
- <div class="resource-card">
468
- <div class="resource-label">Load Average</div>
469
- <div class="resource-value">${resources.load_avg.map(v => v.toFixed(2)).join(', ')}</div>
470
- </div>
471
- ` : ''}
472
- `;
473
-
474
- // Animate value changes (data-driven)
475
- if (oldResources) {
476
- this.animateResourceChanges(container, oldResources, resources);
477
- }
478
- }
479
-
480
- /**
481
- * Animate changes (data-driven only)
482
- */
483
- animateChanges(container, oldData, newData, keyField) {
484
- // Find changed items
485
- const oldMap = new Map(oldData.map(item => [item[keyField], item]));
486
-
487
- newData.forEach(newItem => {
488
- const oldItem = oldMap.get(newItem[keyField]);
489
- if (oldItem && oldItem.status !== newItem.status) {
490
- // Status changed - animate
491
- const element = container.querySelector(`[data-${keyField.toLowerCase()}="${newItem[keyField]}"]`);
492
- if (element) {
493
- element.classList.add('status-changed');
494
- setTimeout(() => element.classList.remove('status-changed'), 300);
495
- }
496
- }
497
- });
498
- }
499
-
500
- /**
501
- * Animate coin price changes (data-driven)
502
- */
503
- animateCoinPriceChanges(container, oldCoins, newCoins) {
504
- const oldMap = new Map(oldCoins.map(c => [c.symbol, c]));
505
-
506
- newCoins.forEach(newCoin => {
507
- const oldCoin = oldMap.get(newCoin.symbol);
508
- if (oldCoin && oldCoin.price && newCoin.price && oldCoin.price !== newCoin.price) {
509
- const element = container.querySelector(`[data-coin="${newCoin.symbol}"] .coin-price`);
510
- if (element) {
511
- element.classList.add(newCoin.price > oldCoin.price ? 'price-up' : 'price-down');
512
- setTimeout(() => {
513
- element.classList.remove('price-up', 'price-down');
514
- }, 300);
515
- }
516
- }
517
- });
518
- }
519
-
520
- /**
521
- * Animate resource changes (data-driven)
522
- */
523
- animateResourceChanges(container, oldResources, newResources) {
524
- // CPU
525
- if (oldResources.cpu_percent !== newResources.cpu_percent) {
526
- const cpuValue = container.querySelector('[data-metric="cpu"]');
527
- if (cpuValue) {
528
- cpuValue.classList.add('value-changed');
529
- setTimeout(() => cpuValue.classList.remove('value-changed'), 300);
530
- }
531
- }
532
-
533
- // Memory
534
- if (oldResources.memory_percent !== newResources.memory_percent) {
535
- const memValue = container.querySelector('[data-metric="memory"]');
536
- if (memValue) {
537
- memValue.classList.add('value-changed');
538
- setTimeout(() => memValue.classList.remove('value-changed'), 300);
539
- }
540
- }
541
- }
542
-
543
- /**
544
- * Show error state
545
- */
546
- showError(error) {
547
- const overallHealth = document.getElementById('overall-health');
548
- if (overallHealth) {
549
- overallHealth.innerHTML = `
550
- <div class="health-status">
551
- <span class="status-indicator status-error"></span>
552
- <span class="status-text" style="color: #ef4444;">Failed to fetch status</span>
553
- </div>
554
- <div class="health-timestamp">${error.message}</div>
555
- `;
556
- }
557
- }
558
-
559
- /**
560
- * Show degraded state after multiple errors
561
- */
562
- showDegradedState() {
563
- // Show last known data with warning
564
- const overallHealth = document.getElementById('overall-health');
565
- if (overallHealth && this.lastData) {
566
- overallHealth.innerHTML = `
567
- <div class="health-status">
568
- <span class="status-indicator status-degraded"></span>
569
- <span class="status-text" style="color: #f59e0b;">Showing last known data</span>
570
- </div>
571
- <div class="health-timestamp">Data may be stale</div>
572
- `;
573
- }
574
- }
575
-
576
- /**
577
- * Get color for resource usage
578
- */
579
- getResourceColor(percent) {
580
- if (percent < 50) return '#10b981';
581
- if (percent < 75) return '#22d3ee';
582
- if (percent < 90) return '#f59e0b';
583
- return '#ef4444';
584
- }
585
-
586
- /**
587
- * Format timestamp
588
- */
589
- formatTimestamp(timestamp) {
590
- const date = new Date(timestamp * 1000);
591
- return date.toLocaleTimeString();
592
- }
593
-
594
- /**
595
- * Format relative time
596
- */
597
- formatRelativeTime(isoString) {
598
- if (!isoString) return 'Never';
599
- const diff = Date.now() - new Date(isoString).getTime();
600
- const seconds = Math.floor(diff / 1000);
601
-
602
- if (seconds < 60) return 'Just now';
603
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
604
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
605
- return `${Math.floor(seconds / 86400)}d ago`;
606
- }
607
-
608
- /**
609
- * Format uptime
610
- */
611
- formatUptime(seconds) {
612
- if (seconds < 60) return `${seconds}s`;
613
- if (seconds < 3600) return `${Math.floor(seconds / 60)}m`;
614
- if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ${Math.floor((seconds % 3600) / 60)}m`;
615
- return `${Math.floor(seconds / 86400)}d ${Math.floor((seconds % 86400) / 3600)}h`;
616
- }
617
-
618
- /**
619
- * Destroy modal
620
- */
621
- destroy() {
622
- this.stopPolling();
623
- if (this.modalElement) {
624
- this.modalElement.remove();
625
- this.modalElement = null;
626
- }
627
- }
628
- }
629
-
630
- // Export for use in other modules
631
- if (typeof module !== 'undefined' && module.exports) {
632
- module.exports = SystemStatusModal;
633
- }
634
-
635
- // Make available globally
636
- if (typeof window !== 'undefined') {
637
- window.SystemStatusModal = SystemStatusModal;
638
- }