|
|
<!DOCTYPE html> |
|
|
<html lang="en" data-theme="light"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<meta name="description" content="Crypto Intelligence Hub - Modern Dashboard with 40+ Data Sources"> |
|
|
<title>Dashboard | Crypto Intelligence Hub</title> |
|
|
|
|
|
|
|
|
<link rel="icon" href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Cdefs%3E%3ClinearGradient id='g' x1='0%25' y1='0%25' x2='100%25' y2='100%25'%3E%3Cstop offset='0%25' stop-color='%2322d3ee'/%3E%3Cstop offset='100%25' stop-color='%236366f1'/%3E%3C/linearGradient%3E%3C/defs%3E%3Ccircle cx='50' cy='50' r='45' fill='url(%23g)'/%3E%3C/svg%3E"> |
|
|
|
|
|
|
|
|
<link rel="stylesheet" href="/static/shared/css/theme-modern.css"> |
|
|
<link rel="stylesheet" href="/static/shared/css/sidebar-modern.css"> |
|
|
|
|
|
<style> |
|
|
|
|
|
body { |
|
|
margin: 0; |
|
|
padding: 0; |
|
|
background: var(--bg-secondary); |
|
|
} |
|
|
|
|
|
.app-layout { |
|
|
display: flex; |
|
|
min-height: 100vh; |
|
|
} |
|
|
|
|
|
.main-content { |
|
|
flex: 1; |
|
|
margin-left: var(--sidebar-width); |
|
|
transition: margin-left var(--transition-base); |
|
|
padding: var(--space-6); |
|
|
} |
|
|
|
|
|
.sidebar-modern.collapsed ~ .main-content { |
|
|
margin-left: var(--sidebar-collapsed-width); |
|
|
} |
|
|
|
|
|
|
|
|
.page-header { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
margin-bottom: var(--space-8); |
|
|
padding-bottom: var(--space-6); |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
} |
|
|
|
|
|
.page-title h1 { |
|
|
font-size: var(--text-4xl); |
|
|
font-weight: var(--font-bold); |
|
|
background: var(--accent-gradient); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
margin-bottom: var(--space-2); |
|
|
} |
|
|
|
|
|
.page-subtitle { |
|
|
color: var(--text-tertiary); |
|
|
font-size: var(--text-lg); |
|
|
} |
|
|
|
|
|
.page-actions { |
|
|
display: flex; |
|
|
gap: var(--space-3); |
|
|
} |
|
|
|
|
|
.btn { |
|
|
padding: var(--space-3) var(--space-6); |
|
|
border-radius: var(--radius-lg); |
|
|
font-weight: var(--font-semibold); |
|
|
font-size: var(--text-sm); |
|
|
cursor: pointer; |
|
|
transition: all var(--transition-base); |
|
|
border: none; |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: var(--space-2); |
|
|
} |
|
|
|
|
|
.btn-primary { |
|
|
background: var(--accent-gradient); |
|
|
color: white; |
|
|
box-shadow: var(--shadow-md); |
|
|
} |
|
|
|
|
|
.btn-primary:hover { |
|
|
transform: translateY(-2px); |
|
|
box-shadow: var(--shadow-lg); |
|
|
} |
|
|
|
|
|
.btn-secondary { |
|
|
background: var(--surface-secondary); |
|
|
color: var(--text-primary); |
|
|
border: 1px solid var(--border-primary); |
|
|
} |
|
|
|
|
|
.btn-secondary:hover { |
|
|
background: var(--surface-hover); |
|
|
} |
|
|
|
|
|
|
|
|
.stats-grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); |
|
|
gap: var(--space-6); |
|
|
margin-bottom: var(--space-8); |
|
|
} |
|
|
|
|
|
.stat-card { |
|
|
background: var(--surface-primary); |
|
|
border-radius: var(--radius-xl); |
|
|
padding: var(--space-6); |
|
|
border: 1px solid var(--border-primary); |
|
|
box-shadow: var(--shadow-sm); |
|
|
transition: all var(--transition-base); |
|
|
position: relative; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.stat-card::before { |
|
|
content: ''; |
|
|
position: absolute; |
|
|
top: 0; |
|
|
left: 0; |
|
|
right: 0; |
|
|
height: 4px; |
|
|
background: var(--accent-gradient); |
|
|
opacity: 0; |
|
|
transition: opacity var(--transition-fast); |
|
|
} |
|
|
|
|
|
.stat-card:hover { |
|
|
transform: translateY(-4px); |
|
|
box-shadow: var(--shadow-md); |
|
|
} |
|
|
|
|
|
.stat-card:hover::before { |
|
|
opacity: 1; |
|
|
} |
|
|
|
|
|
.stat-header { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
margin-bottom: var(--space-4); |
|
|
} |
|
|
|
|
|
.stat-icon { |
|
|
width: 48px; |
|
|
height: 48px; |
|
|
border-radius: var(--radius-lg); |
|
|
background: var(--accent-gradient); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
box-shadow: 0 4px 12px rgba(34, 211, 238, 0.3); |
|
|
} |
|
|
|
|
|
.stat-icon svg { |
|
|
width: 24px; |
|
|
height: 24px; |
|
|
color: white; |
|
|
} |
|
|
|
|
|
.stat-badge { |
|
|
padding: var(--space-1) var(--space-3); |
|
|
border-radius: var(--radius-full); |
|
|
font-size: var(--text-xs); |
|
|
font-weight: var(--font-bold); |
|
|
text-transform: uppercase; |
|
|
} |
|
|
|
|
|
.stat-badge.success { |
|
|
background: rgba(16, 185, 129, 0.1); |
|
|
color: var(--color-success); |
|
|
} |
|
|
|
|
|
.stat-badge.warning { |
|
|
background: rgba(245, 158, 11, 0.1); |
|
|
color: var(--color-warning); |
|
|
} |
|
|
|
|
|
.stat-value { |
|
|
font-size: var(--text-4xl); |
|
|
font-weight: var(--font-extrabold); |
|
|
color: var(--text-primary); |
|
|
margin-bottom: var(--space-2); |
|
|
} |
|
|
|
|
|
.stat-label { |
|
|
font-size: var(--text-sm); |
|
|
color: var(--text-tertiary); |
|
|
font-weight: var(--font-medium); |
|
|
} |
|
|
|
|
|
.stat-change { |
|
|
margin-top: var(--space-3); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: var(--space-2); |
|
|
font-size: var(--text-sm); |
|
|
font-weight: var(--font-semibold); |
|
|
} |
|
|
|
|
|
.stat-change.positive { |
|
|
color: var(--color-success); |
|
|
} |
|
|
|
|
|
.stat-change.negative { |
|
|
color: var(--color-danger); |
|
|
} |
|
|
|
|
|
|
|
|
.card { |
|
|
background: var(--surface-primary); |
|
|
border-radius: var(--radius-xl); |
|
|
border: 1px solid var(--border-primary); |
|
|
box-shadow: var(--shadow-sm); |
|
|
overflow: hidden; |
|
|
margin-bottom: var(--space-6); |
|
|
} |
|
|
|
|
|
.card-header { |
|
|
padding: var(--space-6); |
|
|
border-bottom: 1px solid var(--border-primary); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
} |
|
|
|
|
|
.card-title { |
|
|
font-size: var(--text-xl); |
|
|
font-weight: var(--font-bold); |
|
|
color: var(--text-primary); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: var(--space-3); |
|
|
} |
|
|
|
|
|
.card-body { |
|
|
padding: var(--space-6); |
|
|
} |
|
|
|
|
|
|
|
|
.loading { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
padding: var(--space-8); |
|
|
color: var(--text-tertiary); |
|
|
} |
|
|
|
|
|
.spinner { |
|
|
width: 40px; |
|
|
height: 40px; |
|
|
border: 3px solid var(--border-primary); |
|
|
border-top-color: var(--accent-primary); |
|
|
border-radius: 50%; |
|
|
animation: spin 1s linear infinite; |
|
|
} |
|
|
|
|
|
@keyframes spin { |
|
|
to { transform: rotate(360deg); } |
|
|
} |
|
|
|
|
|
|
|
|
.news-list { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: var(--space-4); |
|
|
} |
|
|
|
|
|
.news-item { |
|
|
display: flex; |
|
|
gap: var(--space-4); |
|
|
padding: var(--space-4); |
|
|
border-radius: var(--radius-lg); |
|
|
transition: all var(--transition-fast); |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.news-item:hover { |
|
|
background: var(--surface-hover); |
|
|
} |
|
|
|
|
|
.news-source { |
|
|
font-size: var(--text-xs); |
|
|
color: var(--text-tertiary); |
|
|
font-weight: var(--font-semibold); |
|
|
text-transform: uppercase; |
|
|
} |
|
|
|
|
|
.news-title { |
|
|
font-size: var(--text-base); |
|
|
font-weight: var(--font-semibold); |
|
|
color: var(--text-primary); |
|
|
margin: var(--space-2) 0; |
|
|
} |
|
|
|
|
|
.news-time { |
|
|
font-size: var(--text-sm); |
|
|
color: var(--text-tertiary); |
|
|
} |
|
|
|
|
|
|
|
|
.fng-gauge { |
|
|
width: 200px; |
|
|
height: 200px; |
|
|
margin: 0 auto; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.fng-circle { |
|
|
width: 100%; |
|
|
height: 100%; |
|
|
border-radius: 50%; |
|
|
background: conic-gradient( |
|
|
from 180deg, |
|
|
var(--color-danger) 0deg 45deg, |
|
|
var(--color-warning) 45deg 90deg, |
|
|
var(--color-info) 90deg 135deg, |
|
|
var(--color-success) 135deg 180deg |
|
|
); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.fng-inner { |
|
|
width: 80%; |
|
|
height: 80%; |
|
|
background: var(--bg-primary); |
|
|
border-radius: 50%; |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: center; |
|
|
justify-content: center; |
|
|
} |
|
|
|
|
|
.fng-value { |
|
|
font-size: var(--text-5xl); |
|
|
font-weight: var(--font-extrabold); |
|
|
background: var(--accent-gradient); |
|
|
-webkit-background-clip: text; |
|
|
-webkit-text-fill-color: transparent; |
|
|
} |
|
|
|
|
|
.fng-label { |
|
|
font-size: var(--text-sm); |
|
|
color: var(--text-tertiary); |
|
|
font-weight: var(--font-semibold); |
|
|
margin-top: var(--space-2); |
|
|
} |
|
|
|
|
|
|
|
|
@media (max-width: 1024px) { |
|
|
.main-content { |
|
|
margin-left: 0; |
|
|
} |
|
|
|
|
|
.sidebar-modern.collapsed ~ .main-content { |
|
|
margin-left: 0; |
|
|
} |
|
|
} |
|
|
|
|
|
@media (max-width: 768px) { |
|
|
.main-content { |
|
|
padding: var(--space-4); |
|
|
} |
|
|
|
|
|
.page-header { |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
gap: var(--space-4); |
|
|
} |
|
|
|
|
|
.stats-grid { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
|
|
|
<script src="/static/js/api-config.js"></script> |
|
|
<script> |
|
|
|
|
|
window.apiReady = new Promise((resolve) => { |
|
|
if (window.apiClient) { |
|
|
console.log('✅ API Client ready'); |
|
|
resolve(window.apiClient); |
|
|
} else { |
|
|
console.error('❌ API Client not loaded'); |
|
|
} |
|
|
}); |
|
|
</script> |
|
|
|
|
|
</head> |
|
|
<body> |
|
|
<div class="app-layout"> |
|
|
|
|
|
<div id="sidebar-container"></div> |
|
|
|
|
|
|
|
|
<main class="main-content"> |
|
|
|
|
|
<header class="page-header"> |
|
|
<div class="page-title"> |
|
|
<h1>Dashboard</h1> |
|
|
<p class="page-subtitle">Real-time crypto market intelligence</p> |
|
|
</div> |
|
|
<div class="page-actions"> |
|
|
<button class="btn btn-secondary" id="refresh-btn"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<path d="M21 12a9 9 0 1 1-9-9c2.52 0 4.93 1 6.74 2.74L21 8"/> |
|
|
<path d="M21 3v5h-5"/> |
|
|
</svg> |
|
|
Refresh |
|
|
</button> |
|
|
<button class="btn btn-primary" id="theme-toggle"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<circle cx="12" cy="12" r="4"/> |
|
|
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41"/> |
|
|
</svg> |
|
|
Toggle Theme |
|
|
</button> |
|
|
</div> |
|
|
</header> |
|
|
|
|
|
|
|
|
<div class="stats-grid"> |
|
|
|
|
|
<div class="stat-card" id="btc-card"> |
|
|
<div class="stat-header"> |
|
|
<div class="stat-icon"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<path d="M12 2v20M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="stat-badge success">Live</span> |
|
|
</div> |
|
|
<div class="stat-value" id="btc-price">Loading...</div> |
|
|
<div class="stat-label">Bitcoin (BTC)</div> |
|
|
<div class="stat-change positive" id="btc-change"> |
|
|
<span>↑</span> |
|
|
<span>--</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="stat-card" id="eth-card"> |
|
|
<div class="stat-header"> |
|
|
<div class="stat-icon"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<polygon points="12 2 3 12 12 16 21 12"/> |
|
|
<polygon points="12 22 3 14 12 18 21 14"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="stat-badge success">Live</span> |
|
|
</div> |
|
|
<div class="stat-value" id="eth-price">Loading...</div> |
|
|
<div class="stat-label">Ethereum (ETH)</div> |
|
|
<div class="stat-change positive" id="eth-change"> |
|
|
<span>↑</span> |
|
|
<span>--</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<div class="stat-icon"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<path d="M3 3v18h18"/> |
|
|
<path d="M18 7l-5 5-4-4-5 5"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="stat-badge warning">24h</span> |
|
|
</div> |
|
|
<div class="stat-value" id="total-cap">$2.1T</div> |
|
|
<div class="stat-label">Total Market Cap</div> |
|
|
<div class="stat-change positive"> |
|
|
<span>↑</span> |
|
|
<span>2.3%</span> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="stat-card"> |
|
|
<div class="stat-header"> |
|
|
<div class="stat-icon"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<path d="M4 11a9 9 0 0 1 9 9"/> |
|
|
<path d="M4 4a16 16 0 0 1 16 16"/> |
|
|
<circle cx="5" cy="19" r="2"/> |
|
|
</svg> |
|
|
</div> |
|
|
<span class="stat-badge success" id="api-status-badge">Online</span> |
|
|
</div> |
|
|
<div class="stat-value" id="api-success-rate">98%</div> |
|
|
<div class="stat-label">API Success Rate</div> |
|
|
<div class="stat-change positive" id="api-stats"> |
|
|
<span>40+</span> |
|
|
<span>sources active</span> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div style="display: grid; grid-template-columns: 2fr 1fr; gap: var(--space-6);"> |
|
|
|
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2"/> |
|
|
</svg> |
|
|
Latest News |
|
|
</div> |
|
|
<span id="news-count" style="color: var(--text-tertiary); font-size: var(--text-sm);">Loading...</span> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
<div class="news-list" id="news-list"> |
|
|
<div class="loading"> |
|
|
<div class="spinner"></div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card"> |
|
|
<div class="card-header"> |
|
|
<div class="card-title"> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> |
|
|
<circle cx="12" cy="12" r="10"/> |
|
|
<path d="M8 14s1.5 2 4 2 4-2 4-2"/> |
|
|
</svg> |
|
|
Fear & Greed |
|
|
</div> |
|
|
</div> |
|
|
<div class="card-body"> |
|
|
<div class="fng-gauge" id="fng-gauge"> |
|
|
<div class="fng-circle"> |
|
|
<div class="fng-inner"> |
|
|
<div class="fng-value" id="fng-value">--</div> |
|
|
<div class="fng-label" id="fng-label">Loading...</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
<div style="text-align: center; margin-top: var(--space-6); color: var(--text-tertiary); font-size: var(--text-sm);" id="fng-source"> |
|
|
Source: -- |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
</div> |
|
|
|
|
|
|
|
|
<script type="module"> |
|
|
import apiClient from '/static/shared/js/api-client-comprehensive.js'; |
|
|
import sidebarManager from '/static/shared/js/sidebar-manager.js'; |
|
|
|
|
|
|
|
|
fetch('/static/shared/layouts/sidebar-modern.html') |
|
|
.then(r => r.text()) |
|
|
.then(html => { |
|
|
document.getElementById('sidebar-container').innerHTML = html; |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('theme-toggle').addEventListener('click', () => { |
|
|
const html = document.documentElement; |
|
|
const current = html.getAttribute('data-theme') || 'light'; |
|
|
const next = current === 'light' ? 'dark' : 'light'; |
|
|
html.setAttribute('data-theme', next); |
|
|
localStorage.setItem('theme', next); |
|
|
}); |
|
|
|
|
|
|
|
|
const savedTheme = localStorage.getItem('theme') || 'light'; |
|
|
document.documentElement.setAttribute('data-theme', savedTheme); |
|
|
|
|
|
|
|
|
async function loadDashboard() { |
|
|
try { |
|
|
|
|
|
const btc = await apiClient.getMarketPrice('bitcoin'); |
|
|
document.getElementById('btc-price').textContent = `$${btc.price.toLocaleString()}`; |
|
|
if (btc.change24h) { |
|
|
const changeEl = document.getElementById('btc-change'); |
|
|
changeEl.innerHTML = `<span>${btc.change24h > 0 ? '↑' : '↓'}</span><span>${Math.abs(btc.change24h).toFixed(2)}%</span>`; |
|
|
changeEl.className = `stat-change ${btc.change24h > 0 ? 'positive' : 'negative'}`; |
|
|
} |
|
|
|
|
|
|
|
|
const eth = await apiClient.getMarketPrice('ethereum'); |
|
|
document.getElementById('eth-price').textContent = `$${eth.price.toLocaleString()}`; |
|
|
if (eth.change24h) { |
|
|
const changeEl = document.getElementById('eth-change'); |
|
|
changeEl.innerHTML = `<span>${eth.change24h > 0 ? '↑' : '↓'}</span><span>${Math.abs(eth.change24h).toFixed(2)}%</span>`; |
|
|
changeEl.className = `stat-change ${eth.change24h > 0 ? 'positive' : 'negative'}`; |
|
|
} |
|
|
|
|
|
|
|
|
const fng = await apiClient.getSentiment(); |
|
|
document.getElementById('fng-value').textContent = fng.value; |
|
|
document.getElementById('fng-label').textContent = fng.classification; |
|
|
document.getElementById('fng-source').textContent = `Source: ${fng.source}`; |
|
|
|
|
|
|
|
|
const news = await apiClient.getNews(10); |
|
|
document.getElementById('news-count').textContent = `${news.length} articles`; |
|
|
const newsList = document.getElementById('news-list'); |
|
|
newsList.innerHTML = news.map(item => ` |
|
|
<div class="news-item" onclick="window.open('${item.link}', '_blank')"> |
|
|
<div style="flex: 1;"> |
|
|
<div class="news-source">${item.source}</div> |
|
|
<div class="news-title">${item.title}</div> |
|
|
<div class="news-time">${new Date(item.publishedAt || Date.now()).toLocaleString()}</div> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
|
|
|
|
|
|
const stats = apiClient.getStats(); |
|
|
document.getElementById('api-success-rate').textContent = stats.successRate; |
|
|
document.getElementById('api-stats').innerHTML = `<span>${stats.successful}/${stats.total}</span><span>requests</span>`; |
|
|
|
|
|
console.log('✅ Dashboard loaded successfully'); |
|
|
console.log('API Stats:', stats); |
|
|
} catch (error) { |
|
|
console.error('❌ Failed to load dashboard:', error); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
document.getElementById('refresh-btn').addEventListener('click', () => { |
|
|
apiClient.clearCache(); |
|
|
loadDashboard(); |
|
|
}); |
|
|
|
|
|
|
|
|
loadDashboard(); |
|
|
|
|
|
|
|
|
setInterval(() => { |
|
|
loadDashboard(); |
|
|
}, 120000); |
|
|
</script> |
|
|
</body> |
|
|
</html> |
|
|
|
|
|
|