fix: Replace modal with slide-out drawer panel from right side
Browse filesCOMPLETE 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 +1 -20
- static/pages/dashboard/dashboard.js +9 -19
- static/pages/dashboard/index.html +4 -9
- static/shared/css/status-drawer.css +390 -0
- static/shared/css/system-status-modal.css +0 -649
- static/shared/js/components/status-drawer.js +394 -0
- static/shared/js/components/system-status-modal.js +0 -638
|
@@ -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 */
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -38,7 +38,7 @@ class DashboardPage {
|
|
| 38 |
|
| 39 |
// Defer Chart.js loading until after initial render
|
| 40 |
this.injectEnhancedLayout();
|
| 41 |
-
this.
|
| 42 |
this.bindEvents();
|
| 43 |
|
| 44 |
// Add smooth fade-in delay for better UX
|
|
@@ -414,34 +414,24 @@ class DashboardPage {
|
|
| 414 |
`;
|
| 415 |
}
|
| 416 |
|
| 417 |
-
|
| 418 |
-
// Initialize the
|
| 419 |
try {
|
| 420 |
-
if (typeof
|
| 421 |
-
window.
|
| 422 |
apiEndpoint: '/api/system/status',
|
| 423 |
-
updateInterval: 3000
|
| 424 |
-
onError: (error) => {
|
| 425 |
-
logger.error('Dashboard', 'System status modal error:', error);
|
| 426 |
-
}
|
| 427 |
});
|
| 428 |
-
logger.info('Dashboard', '
|
| 429 |
} else {
|
| 430 |
-
logger.warn('Dashboard', '
|
| 431 |
}
|
| 432 |
} catch (error) {
|
| 433 |
-
logger.error('Dashboard', 'Failed to initialize
|
| 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');
|
|
@@ -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/
|
| 46 |
-
<noscript><link rel="stylesheet" href="/static/shared/css/
|
| 47 |
<!-- Error Suppressor - Suppress external service errors (load first) -->
|
| 48 |
<script src="/static/shared/js/utils/error-suppressor.js"></script>
|
| 49 |
-
<!--
|
| 50 |
-
<script src="/static/shared/js/components/
|
| 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"/>
|
|
@@ -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 |
+
}
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
+
}
|
|
@@ -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 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|