Spaces:
Sleeping
Sleeping
| <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> | |
| <!-- Schedule Modal --> | |
| <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> | |
| <!-- Toast notification --> | |
| <div class="toast" id="toast"></div> | |
| <script> | |
| let ws = null; | |
| let reconnectAttempts = 0; | |
| const maxReconnectAttempts = 5; | |
| const reconnectDelay = 3000; | |
| // Initialize WebSocket connection | |
| 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; | |
| // Subscribe to all updates | |
| ws.send(JSON.stringify({ type: 'subscribe_all' })); | |
| // Start heartbeat | |
| 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); | |
| // Keep only last 50 entries | |
| 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); | |
| } | |
| // Load system status | |
| 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); | |
| } | |
| } | |
| // Load APIs | |
| 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); | |
| } | |
| } | |
| // Export functions | |
| 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}`); | |
| // Trigger download | |
| 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}`); | |
| // Trigger download | |
| 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)); // Small delay | |
| } | |
| 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); | |
| } | |
| } | |
| // Schedule modal functions | |
| 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; | |
| // Load current schedule | |
| 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); | |
| } | |
| } | |
| // Initialize on load | |
| window.addEventListener('load', () => { | |
| initWebSocket(); | |
| loadSystemStatus(); | |
| loadAPIs(); | |
| // Refresh status every 30 seconds | |
| setInterval(loadSystemStatus, 30000); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |