| <!DOCTYPE html> |
| <html lang="en"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Enhanced Crypto Data Tracker</title> |
| <style> |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); |
| min-height: 100vh; |
| padding: 20px; |
| } |
| |
| .container { |
| max-width: 1600px; |
| margin: 0 auto; |
| } |
| |
| header { |
| background: rgba(255, 255, 255, 0.1); |
| backdrop-filter: blur(10px); |
| border-radius: 15px; |
| padding: 20px 30px; |
| margin-bottom: 20px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| h1 { |
| color: white; |
| font-size: 28px; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .connection-status { |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| color: white; |
| font-size: 14px; |
| } |
| |
| .status-indicator { |
| width: 10px; |
| height: 10px; |
| border-radius: 50%; |
| background: #10b981; |
| animation: pulse 2s infinite; |
| } |
| |
| .status-indicator.disconnected { |
| background: #ef4444; |
| animation: none; |
| } |
| |
| @keyframes pulse { |
| 0%, 100% { opacity: 1; } |
| 50% { opacity: 0.5; } |
| } |
| |
| .controls { |
| background: rgba(255, 255, 255, 0.1); |
| backdrop-filter: blur(10px); |
| border-radius: 15px; |
| padding: 20px; |
| margin-bottom: 20px; |
| } |
| |
| .controls-row { |
| display: flex; |
| gap: 15px; |
| flex-wrap: wrap; |
| align-items: center; |
| } |
| |
| .btn { |
| padding: 10px 20px; |
| border: none; |
| border-radius: 8px; |
| cursor: pointer; |
| font-size: 14px; |
| font-weight: 600; |
| transition: all 0.3s ease; |
| display: flex; |
| align-items: center; |
| gap: 8px; |
| } |
| |
| .btn-primary { |
| background: white; |
| color: #667eea; |
| } |
| |
| .btn-primary:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
| } |
| |
| .btn-success { |
| background: #10b981; |
| color: white; |
| } |
| |
| .btn-danger { |
| background: #ef4444; |
| color: white; |
| } |
| |
| .btn-info { |
| background: #3b82f6; |
| color: white; |
| } |
| |
| .btn:disabled { |
| opacity: 0.5; |
| cursor: not-allowed; |
| } |
| |
| .grid { |
| display: grid; |
| grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); |
| gap: 20px; |
| margin-bottom: 20px; |
| } |
| |
| .card { |
| background: rgba(255, 255, 255, 0.1); |
| backdrop-filter: blur(10px); |
| border-radius: 15px; |
| padding: 20px; |
| color: white; |
| } |
| |
| .card h2 { |
| font-size: 18px; |
| margin-bottom: 15px; |
| display: flex; |
| align-items: center; |
| gap: 10px; |
| } |
| |
| .stat-grid { |
| display: grid; |
| grid-template-columns: repeat(2, 1fr); |
| gap: 15px; |
| } |
| |
| .stat-item { |
| background: rgba(255, 255, 255, 0.1); |
| padding: 15px; |
| border-radius: 10px; |
| } |
| |
| .stat-label { |
| font-size: 12px; |
| opacity: 0.8; |
| margin-bottom: 5px; |
| } |
| |
| .stat-value { |
| font-size: 24px; |
| font-weight: 700; |
| } |
| |
| .api-list { |
| max-height: 400px; |
| overflow-y: auto; |
| } |
| |
| .api-item { |
| background: rgba(255, 255, 255, 0.05); |
| padding: 15px; |
| border-radius: 10px; |
| margin-bottom: 10px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| } |
| |
| .api-info { |
| flex: 1; |
| } |
| |
| .api-name { |
| font-weight: 600; |
| margin-bottom: 5px; |
| } |
| |
| .api-meta { |
| font-size: 12px; |
| opacity: 0.7; |
| display: flex; |
| gap: 15px; |
| } |
| |
| .api-controls { |
| display: flex; |
| gap: 10px; |
| } |
| |
| .small-btn { |
| padding: 5px 12px; |
| font-size: 12px; |
| border-radius: 5px; |
| border: none; |
| cursor: pointer; |
| background: rgba(255, 255, 255, 0.2); |
| color: white; |
| transition: all 0.2s; |
| } |
| |
| .small-btn:hover { |
| background: rgba(255, 255, 255, 0.3); |
| } |
| |
| .status-badge { |
| display: inline-block; |
| padding: 4px 10px; |
| border-radius: 12px; |
| font-size: 11px; |
| font-weight: 600; |
| } |
| |
| .status-success { |
| background: #10b981; |
| color: white; |
| } |
| |
| .status-pending { |
| background: #f59e0b; |
| color: white; |
| } |
| |
| .status-failed { |
| background: #ef4444; |
| color: white; |
| } |
| |
| .log-container { |
| background: rgba(0, 0, 0, 0.3); |
| border-radius: 10px; |
| padding: 15px; |
| max-height: 300px; |
| overflow-y: auto; |
| font-family: 'Courier New', monospace; |
| font-size: 12px; |
| } |
| |
| .log-entry { |
| margin-bottom: 8px; |
| padding: 5px; |
| border-left: 3px solid #667eea; |
| padding-left: 10px; |
| } |
| |
| .log-time { |
| opacity: 0.6; |
| margin-right: 10px; |
| } |
| |
| .modal { |
| display: none; |
| position: fixed; |
| top: 0; |
| left: 0; |
| width: 100%; |
| height: 100%; |
| background: rgba(0, 0, 0, 0.7); |
| z-index: 1000; |
| justify-content: center; |
| align-items: center; |
| } |
| |
| .modal.active { |
| display: flex; |
| } |
| |
| .modal-content { |
| background: white; |
| border-radius: 15px; |
| padding: 30px; |
| max-width: 500px; |
| width: 90%; |
| color: #333; |
| } |
| |
| .modal-header { |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| margin-bottom: 20px; |
| } |
| |
| .modal-close { |
| background: none; |
| border: none; |
| font-size: 24px; |
| cursor: pointer; |
| color: #666; |
| } |
| |
| .form-group { |
| margin-bottom: 15px; |
| } |
| |
| .form-label { |
| display: block; |
| margin-bottom: 5px; |
| font-weight: 600; |
| color: #333; |
| } |
| |
| .form-input { |
| width: 100%; |
| padding: 10px; |
| border: 1px solid #ddd; |
| border-radius: 8px; |
| font-size: 14px; |
| } |
| |
| .form-select { |
| width: 100%; |
| padding: 10px; |
| border: 1px solid #ddd; |
| border-radius: 8px; |
| font-size: 14px; |
| } |
| |
| ::-webkit-scrollbar { |
| width: 8px; |
| } |
| |
| ::-webkit-scrollbar-track { |
| background: rgba(255, 255, 255, 0.1); |
| border-radius: 4px; |
| } |
| |
| ::-webkit-scrollbar-thumb { |
| background: rgba(255, 255, 255, 0.3); |
| border-radius: 4px; |
| } |
| |
| ::-webkit-scrollbar-thumb:hover { |
| background: rgba(255, 255, 255, 0.5); |
| } |
| |
| .toast { |
| position: fixed; |
| bottom: 20px; |
| right: 20px; |
| background: white; |
| color: #333; |
| padding: 15px 20px; |
| border-radius: 10px; |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); |
| opacity: 0; |
| transform: translateY(20px); |
| transition: all 0.3s ease; |
| z-index: 2000; |
| } |
| |
| .toast.show { |
| opacity: 1; |
| transform: translateY(0); |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <header> |
| <h1> |
| <span>🚀</span> |
| Enhanced Crypto Data Tracker |
| </h1> |
| <div class="connection-status"> |
| <div class="status-indicator" id="wsStatus"></div> |
| <span id="wsStatusText">Connecting...</span> |
| </div> |
| </header> |
|
|
| <div class="controls"> |
| <div class="controls-row"> |
| <button class="btn btn-primary" onclick="exportJSON()"> |
| 💾 Export JSON |
| </button> |
| <button class="btn btn-primary" onclick="exportCSV()"> |
| 📊 Export CSV |
| </button> |
| <button class="btn btn-success" onclick="createBackup()"> |
| 🔄 Create Backup |
| </button> |
| <button class="btn btn-info" onclick="showScheduleModal()"> |
| ⏰ Configure Schedule |
| </button> |
| <button class="btn btn-info" onclick="forceUpdateAll()"> |
| 🔃 Force Update All |
| </button> |
| <button class="btn btn-danger" onclick="clearCache()"> |
| 🗑️ Clear Cache |
| </button> |
| </div> |
| </div> |
|
|
| <div class="grid"> |
| <div class="card"> |
| <h2>📊 System Statistics</h2> |
| <div class="stat-grid"> |
| <div class="stat-item"> |
| <div class="stat-label">Total APIs</div> |
| <div class="stat-value" id="totalApis">0</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-label">Active Tasks</div> |
| <div class="stat-value" id="activeTasks">0</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-label">Cached Data</div> |
| <div class="stat-value" id="cachedData">0</div> |
| </div> |
| <div class="stat-item"> |
| <div class="stat-label">WS Connections</div> |
| <div class="stat-value" id="wsConnections">0</div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h2>📈 Recent Activity</h2> |
| <div class="log-container" id="activityLog"> |
| <div class="log-entry"> |
| <span class="log-time">--:--:--</span> |
| Waiting for updates... |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <div class="card"> |
| <h2>🔌 API Sources</h2> |
| <div class="api-list" id="apiList"> |
| Loading... |
| </div> |
| </div> |
| </div> |
|
|
| |
| <div class="modal" id="scheduleModal"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h2>⏰ Configure Schedule</h2> |
| <button class="modal-close" onclick="closeScheduleModal()">×</button> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">API Source</label> |
| <select class="form-select" id="scheduleApiSelect"></select> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Interval (seconds)</label> |
| <input type="number" class="form-input" id="scheduleInterval" value="60" min="10"> |
| </div> |
| <div class="form-group"> |
| <label class="form-label">Enabled</label> |
| <input type="checkbox" id="scheduleEnabled" checked> |
| </div> |
| <button class="btn btn-primary" onclick="updateSchedule()">Save Schedule</button> |
| </div> |
| </div> |
|
|
| |
| <div class="toast" id="toast"></div> |
|
|
| <script> |
| let ws = null; |
| let reconnectAttempts = 0; |
| const maxReconnectAttempts = 5; |
| const reconnectDelay = 3000; |
| |
| |
| function initWebSocket() { |
| const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; |
| const wsUrl = `${protocol}//${window.location.host}/api/v2/ws`; |
| |
| ws = new WebSocket(wsUrl); |
| |
| ws.onopen = () => { |
| console.log('WebSocket connected'); |
| updateWSStatus(true); |
| reconnectAttempts = 0; |
| |
| |
| ws.send(JSON.stringify({ type: 'subscribe_all' })); |
| |
| |
| startHeartbeat(); |
| }; |
| |
| ws.onmessage = (event) => { |
| const message = JSON.parse(event.data); |
| handleWSMessage(message); |
| }; |
| |
| ws.onerror = (error) => { |
| console.error('WebSocket error:', error); |
| }; |
| |
| ws.onclose = () => { |
| console.log('WebSocket disconnected'); |
| updateWSStatus(false); |
| attemptReconnect(); |
| }; |
| } |
| |
| function attemptReconnect() { |
| if (reconnectAttempts < maxReconnectAttempts) { |
| reconnectAttempts++; |
| console.log(`Reconnecting... Attempt ${reconnectAttempts}`); |
| setTimeout(initWebSocket, reconnectDelay); |
| } |
| } |
| |
| let heartbeatInterval; |
| function startHeartbeat() { |
| clearInterval(heartbeatInterval); |
| heartbeatInterval = setInterval(() => { |
| if (ws && ws.readyState === WebSocket.OPEN) { |
| ws.send(JSON.stringify({ type: 'ping' })); |
| } |
| }, 30000); |
| } |
| |
| function updateWSStatus(connected) { |
| const indicator = document.getElementById('wsStatus'); |
| const text = document.getElementById('wsStatusText'); |
| |
| if (connected) { |
| indicator.classList.remove('disconnected'); |
| text.textContent = 'Connected'; |
| } else { |
| indicator.classList.add('disconnected'); |
| text.textContent = 'Disconnected'; |
| } |
| } |
| |
| function handleWSMessage(message) { |
| console.log('Received:', message); |
| |
| switch (message.type) { |
| case 'api_update': |
| handleApiUpdate(message); |
| break; |
| case 'status_update': |
| handleStatusUpdate(message); |
| break; |
| case 'schedule_update': |
| handleScheduleUpdate(message); |
| break; |
| case 'subscribed': |
| addLog(`Subscribed to ${message.api_id || 'all updates'}`); |
| break; |
| } |
| } |
| |
| function handleApiUpdate(message) { |
| addLog(`Updated: ${message.api_id}`, 'success'); |
| loadSystemStatus(); |
| } |
| |
| function handleStatusUpdate(message) { |
| addLog('System status updated'); |
| loadSystemStatus(); |
| } |
| |
| function handleScheduleUpdate(message) { |
| addLog(`Schedule updated for ${message.schedule.api_id}`); |
| loadAPIs(); |
| } |
| |
| function addLog(text, type = 'info') { |
| const logContainer = document.getElementById('activityLog'); |
| const time = new Date().toLocaleTimeString(); |
| |
| const entry = document.createElement('div'); |
| entry.className = 'log-entry'; |
| entry.innerHTML = `<span class="log-time">${time}</span>${text}`; |
| |
| logContainer.insertBefore(entry, logContainer.firstChild); |
| |
| |
| while (logContainer.children.length > 50) { |
| logContainer.removeChild(logContainer.lastChild); |
| } |
| } |
| |
| function showToast(message, duration = 3000) { |
| const toast = document.getElementById('toast'); |
| toast.textContent = message; |
| toast.classList.add('show'); |
| |
| setTimeout(() => { |
| toast.classList.remove('show'); |
| }, duration); |
| } |
| |
| |
| async function loadSystemStatus() { |
| try { |
| const response = await fetch('/api/v2/status'); |
| const data = await response.json(); |
| |
| document.getElementById('totalApis').textContent = |
| data.services.config_loader.apis_loaded; |
| document.getElementById('activeTasks').textContent = |
| data.services.scheduler.total_tasks; |
| document.getElementById('cachedData').textContent = |
| data.services.persistence.cached_apis; |
| document.getElementById('wsConnections').textContent = |
| data.services.websocket.total_connections; |
| |
| } catch (error) { |
| console.error('Error loading status:', error); |
| } |
| } |
| |
| |
| async function loadAPIs() { |
| try { |
| const response = await fetch('/api/v2/config/apis'); |
| const data = await response.json(); |
| |
| const scheduleResponse = await fetch('/api/v2/schedule/tasks'); |
| const schedules = await scheduleResponse.json(); |
| |
| displayAPIs(data.apis, schedules); |
| |
| } catch (error) { |
| console.error('Error loading APIs:', error); |
| } |
| } |
| |
| function displayAPIs(apis, schedules) { |
| const listElement = document.getElementById('apiList'); |
| listElement.innerHTML = ''; |
| |
| for (const [apiId, api] of Object.entries(apis)) { |
| const schedule = schedules[apiId] || {}; |
| |
| const item = document.createElement('div'); |
| item.className = 'api-item'; |
| item.innerHTML = ` |
| <div class="api-info"> |
| <div class="api-name">${api.name}</div> |
| <div class="api-meta"> |
| <span>📂 ${api.category}</span> |
| <span>⏱️ ${schedule.interval || 300}s</span> |
| <span class="status-badge ${schedule.last_status === 'success' ? 'status-success' : 'status-pending'}"> |
| ${schedule.last_status || 'pending'} |
| </span> |
| </div> |
| </div> |
| <div class="api-controls"> |
| <button class="small-btn" onclick="forceUpdate('${apiId}')">🔄 Update</button> |
| <button class="small-btn" onclick="showScheduleModalFor('${apiId}')">⚙️ Schedule</button> |
| </div> |
| `; |
| |
| listElement.appendChild(item); |
| } |
| } |
| |
| |
| async function exportJSON() { |
| try { |
| const response = await fetch('/api/v2/export/json', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ include_history: true }) |
| }); |
| |
| const data = await response.json(); |
| showToast('✅ JSON export created!'); |
| addLog(`Exported to JSON: ${data.filepath}`); |
| |
| |
| window.open(data.download_url, '_blank'); |
| |
| } catch (error) { |
| showToast('❌ Export failed'); |
| console.error(error); |
| } |
| } |
| |
| async function exportCSV() { |
| try { |
| const response = await fetch('/api/v2/export/csv', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ flatten: true }) |
| }); |
| |
| const data = await response.json(); |
| showToast('✅ CSV export created!'); |
| addLog(`Exported to CSV: ${data.filepath}`); |
| |
| |
| window.open(data.download_url, '_blank'); |
| |
| } catch (error) { |
| showToast('❌ Export failed'); |
| console.error(error); |
| } |
| } |
| |
| async function createBackup() { |
| try { |
| const response = await fetch('/api/v2/backup', { |
| method: 'POST' |
| }); |
| |
| const data = await response.json(); |
| showToast('✅ Backup created!'); |
| addLog(`Backup created: ${data.backup_file}`); |
| |
| } catch (error) { |
| showToast('❌ Backup failed'); |
| console.error(error); |
| } |
| } |
| |
| async function forceUpdate(apiId) { |
| try { |
| const response = await fetch(`/api/v2/schedule/tasks/${apiId}/force-update`, { |
| method: 'POST' |
| }); |
| |
| const data = await response.json(); |
| showToast(`✅ ${apiId} updated!`); |
| addLog(`Forced update: ${apiId}`); |
| loadAPIs(); |
| |
| } catch (error) { |
| showToast('❌ Update failed'); |
| console.error(error); |
| } |
| } |
| |
| async function forceUpdateAll() { |
| showToast('🔄 Updating all APIs...'); |
| addLog('Forcing update for all APIs'); |
| |
| try { |
| const response = await fetch('/api/v2/config/apis'); |
| const data = await response.json(); |
| |
| for (const apiId of Object.keys(data.apis)) { |
| await forceUpdate(apiId); |
| await new Promise(resolve => setTimeout(resolve, 100)); |
| } |
| |
| showToast('✅ All APIs updated!'); |
| } catch (error) { |
| showToast('❌ Update failed'); |
| console.error(error); |
| } |
| } |
| |
| async function clearCache() { |
| if (!confirm('Clear all cached data?')) return; |
| |
| try { |
| const response = await fetch('/api/v2/cleanup/cache', { |
| method: 'POST' |
| }); |
| |
| showToast('✅ Cache cleared!'); |
| addLog('Cache cleared'); |
| loadSystemStatus(); |
| |
| } catch (error) { |
| showToast('❌ Failed to clear cache'); |
| console.error(error); |
| } |
| } |
| |
| |
| function showScheduleModal() { |
| loadAPISelectOptions(); |
| document.getElementById('scheduleModal').classList.add('active'); |
| } |
| |
| function closeScheduleModal() { |
| document.getElementById('scheduleModal').classList.remove('active'); |
| } |
| |
| async function showScheduleModalFor(apiId) { |
| await loadAPISelectOptions(); |
| document.getElementById('scheduleApiSelect').value = apiId; |
| |
| |
| try { |
| const response = await fetch(`/api/v2/schedule/tasks/${apiId}`); |
| const schedule = await response.json(); |
| |
| document.getElementById('scheduleInterval').value = schedule.interval; |
| document.getElementById('scheduleEnabled').checked = schedule.enabled; |
| |
| } catch (error) { |
| console.error(error); |
| } |
| |
| showScheduleModal(); |
| } |
| |
| async function loadAPISelectOptions() { |
| try { |
| const response = await fetch('/api/v2/config/apis'); |
| const data = await response.json(); |
| |
| const select = document.getElementById('scheduleApiSelect'); |
| select.innerHTML = ''; |
| |
| for (const [apiId, api] of Object.entries(data.apis)) { |
| const option = document.createElement('option'); |
| option.value = apiId; |
| option.textContent = api.name; |
| select.appendChild(option); |
| } |
| |
| } catch (error) { |
| console.error(error); |
| } |
| } |
| |
| async function updateSchedule() { |
| const apiId = document.getElementById('scheduleApiSelect').value; |
| const interval = parseInt(document.getElementById('scheduleInterval').value); |
| const enabled = document.getElementById('scheduleEnabled').checked; |
| |
| try { |
| const response = await fetch(`/api/v2/schedule/tasks/${apiId}?interval=${interval}&enabled=${enabled}`, { |
| method: 'PUT' |
| }); |
| |
| const data = await response.json(); |
| showToast('✅ Schedule updated!'); |
| addLog(`Updated schedule for ${apiId}`); |
| closeScheduleModal(); |
| loadAPIs(); |
| |
| } catch (error) { |
| showToast('❌ Schedule update failed'); |
| console.error(error); |
| } |
| } |
| |
| |
| window.addEventListener('load', () => { |
| initWebSocket(); |
| loadSystemStatus(); |
| loadAPIs(); |
| |
| |
| setInterval(loadSystemStatus, 30000); |
| }); |
| </script> |
| </body> |
| </html> |
|
|