Your Name
feat: UI improvements and error suppression - Enhanced dashboard and market pages with improved header buttons, logo, and currency symbol display - Stopped animated ticker - Removed pie chart legends - Added error suppressor for external service errors (SSE, Permissions-Policy warnings) - Improved header button prominence and icon appearance - Enhanced logo with glow effects and better design - Fixed currency symbol visibility in market tables
8b7b267
| <html lang="en" data-theme="light"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>OHLCV Data Sources - Crypto Hub</title> | |
| <link rel="stylesheet" href="/static/shared/css/theme-modern.css"> | |
| <link rel="stylesheet" href="/static/shared/css/sidebar-modern.css"> | |
| <style> | |
| body { | |
| margin: 0; | |
| background: var(--bg-secondary); | |
| } | |
| .app-layout { | |
| display: flex; | |
| min-height: 100vh; | |
| } | |
| .main-content { | |
| flex: 1; | |
| margin-left: var(--sidebar-width); | |
| padding: var(--space-6); | |
| transition: margin-left var(--transition-base); | |
| } | |
| .sidebar-modern.collapsed ~ .main-content { | |
| margin-left: var(--sidebar-collapsed-width); | |
| } | |
| .page-header { | |
| margin-bottom: var(--space-8); | |
| padding-bottom: var(--space-6); | |
| border-bottom: 2px solid var(--border-primary); | |
| } | |
| h1 { | |
| font-size: var(--text-4xl); | |
| background: var(--accent-gradient); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| margin-bottom: var(--space-3); | |
| } | |
| .subtitle { | |
| color: var(--text-tertiary); | |
| font-size: var(--text-lg); | |
| } | |
| .controls { | |
| display: flex; | |
| gap: var(--space-4); | |
| margin-bottom: var(--space-6); | |
| flex-wrap: wrap; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space-2); | |
| } | |
| label { | |
| font-size: var(--text-sm); | |
| font-weight: var(--font-semibold); | |
| color: var(--text-secondary); | |
| } | |
| select, input { | |
| padding: var(--space-3) var(--space-4); | |
| border: 1px solid var(--border-primary); | |
| border-radius: var(--radius-lg); | |
| font-size: var(--text-base); | |
| background: var(--surface-primary); | |
| color: var(--text-primary); | |
| transition: all var(--transition-fast); | |
| } | |
| select:focus, input:focus { | |
| outline: none; | |
| border-color: var(--accent-primary); | |
| box-shadow: 0 0 0 3px rgba(34, 211, 238, 0.1); | |
| } | |
| .btn { | |
| padding: var(--space-3) var(--space-6); | |
| border-radius: var(--radius-lg); | |
| font-weight: var(--font-semibold); | |
| font-size: var(--text-base); | |
| 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(200px, 1fr)); | |
| gap: var(--space-4); | |
| margin-bottom: var(--space-8); | |
| } | |
| .stat-card { | |
| background: var(--surface-primary); | |
| padding: var(--space-5); | |
| border-radius: var(--radius-xl); | |
| border: 1px solid var(--border-primary); | |
| box-shadow: var(--shadow-sm); | |
| } | |
| .stat-value { | |
| font-size: var(--text-3xl); | |
| font-weight: var(--font-extrabold); | |
| color: var(--accent-primary); | |
| margin-bottom: var(--space-2); | |
| } | |
| .stat-label { | |
| font-size: var(--text-sm); | |
| color: var(--text-tertiary); | |
| font-weight: var(--font-medium); | |
| } | |
| .results { | |
| background: var(--surface-primary); | |
| border-radius: var(--radius-xl); | |
| padding: var(--space-6); | |
| border: 1px solid var(--border-primary); | |
| box-shadow: var(--shadow-md); | |
| margin-bottom: var(--space-6); | |
| } | |
| .results h2 { | |
| margin-bottom: var(--space-4); | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space-3); | |
| } | |
| .source-list { | |
| display: flex; | |
| flex-direction: column; | |
| gap: var(--space-3); | |
| } | |
| .source-item { | |
| display: flex; | |
| align-items: center; | |
| gap: var(--space-4); | |
| padding: var(--space-4); | |
| background: var(--bg-secondary); | |
| border-radius: var(--radius-lg); | |
| border: 1px solid var(--border-primary); | |
| } | |
| .source-priority { | |
| width: 40px; | |
| height: 40px; | |
| border-radius: var(--radius-md); | |
| background: var(--accent-gradient); | |
| color: white; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| font-weight: var(--font-bold); | |
| font-size: var(--text-lg); | |
| } | |
| .source-info { | |
| flex: 1; | |
| } | |
| .source-name { | |
| font-weight: var(--font-semibold); | |
| color: var(--text-primary); | |
| margin-bottom: 2px; | |
| } | |
| .source-details { | |
| font-size: var(--text-sm); | |
| color: var(--text-tertiary); | |
| } | |
| .source-status { | |
| padding: var(--space-2) var(--space-4); | |
| border-radius: var(--radius-full); | |
| font-size: var(--text-sm); | |
| font-weight: var(--font-bold); | |
| } | |
| .status-success { | |
| background: rgba(16, 185, 129, 0.1); | |
| color: var(--color-success); | |
| } | |
| .status-failed { | |
| background: rgba(239, 68, 68, 0.1); | |
| color: var(--color-danger); | |
| } | |
| .status-pending { | |
| background: rgba(245, 158, 11, 0.1); | |
| color: var(--color-warning); | |
| } | |
| .chart-container { | |
| height: 400px; | |
| margin-top: var(--space-6); | |
| background: var(--surface-primary); | |
| border-radius: var(--radius-lg); | |
| padding: var(--space-4); | |
| } | |
| .data-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-top: var(--space-4); | |
| } | |
| .data-table th, | |
| .data-table td { | |
| padding: var(--space-3); | |
| text-align: left; | |
| border-bottom: 1px solid var(--border-primary); | |
| } | |
| .data-table th { | |
| background: var(--bg-secondary); | |
| font-weight: var(--font-semibold); | |
| color: var(--text-secondary); | |
| font-size: var(--text-sm); | |
| text-transform: uppercase; | |
| letter-spacing: 0.5px; | |
| } | |
| .data-table td { | |
| font-size: var(--text-sm); | |
| color: var(--text-primary); | |
| } | |
| .data-table tr:hover { | |
| background: var(--surface-hover); | |
| } | |
| .loading { | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| padding: var(--space-8); | |
| gap: var(--space-3); | |
| color: var(--text-tertiary); | |
| } | |
| .spinner { | |
| width: 24px; | |
| height: 24px; | |
| 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); } | |
| } | |
| @media (max-width: 1024px) { | |
| .main-content { | |
| margin-left: 0; | |
| } | |
| } | |
| @media (max-width: 768px) { | |
| .controls { | |
| flex-direction: column; | |
| } | |
| .stats-grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| </style> | |
| <!-- API Configuration - Smart Fallback System --> | |
| <script src="/static/js/api-config.js"></script> | |
| <script> | |
| // Initialize API client | |
| 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"> | |
| <!-- Sidebar --> | |
| <div id="sidebar-container"></div> | |
| <!-- Main Content --> | |
| <main class="main-content"> | |
| <div class="page-header"> | |
| <h1>OHLCV Data Sources</h1> | |
| <p class="subtitle">Comprehensive candlestick data from 15+ integrated sources with automatic fallback</p> | |
| </div> | |
| <!-- Controls --> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label for="symbol-select">Symbol</label> | |
| <select id="symbol-select"> | |
| <option value="bitcoin">Bitcoin (BTC)</option> | |
| <option value="ethereum">Ethereum (ETH)</option> | |
| <option value="cardano">Cardano (ADA)</option> | |
| <option value="solana">Solana (SOL)</option> | |
| <option value="ripple">XRP (XRP)</option> | |
| <option value="polkadot">Polkadot (DOT)</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label for="timeframe-select">Timeframe</label> | |
| <select id="timeframe-select"> | |
| <option value="1m">1 Minute</option> | |
| <option value="5m">5 Minutes</option> | |
| <option value="15m">15 Minutes</option> | |
| <option value="30m">30 Minutes</option> | |
| <option value="1h">1 Hour</option> | |
| <option value="4h">4 Hours</option> | |
| <option value="1d" selected>1 Day</option> | |
| <option value="1w">1 Week</option> | |
| <option value="1M">1 Month</option> | |
| </select> | |
| </div> | |
| <div class="control-group"> | |
| <label for="limit-input">Candles</label> | |
| <input type="number" id="limit-input" value="100" min="10" max="1000" step="10"> | |
| </div> | |
| <div class="control-group"> | |
| <label> </label> | |
| <button class="btn btn-primary" id="fetch-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="M3 3v18h18"/> | |
| <path d="M18 7l-5 5-4-4-5 5"/> | |
| </svg> | |
| Fetch OHLCV Data | |
| </button> | |
| </div> | |
| <div class="control-group"> | |
| <label> </label> | |
| <button class="btn btn-secondary" id="test-all-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="M9 11l3 3L22 4"/> | |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> | |
| </svg> | |
| Test All Sources | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Stats --> | |
| <div class="stats-grid" id="stats-grid"> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="total-sources">12</div> | |
| <div class="stat-label">Total OHLCV Sources</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="success-rate">--</div> | |
| <div class="stat-label">Success Rate</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="candles-loaded">0</div> | |
| <div class="stat-label">Candles Loaded</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-value" id="cache-size">0</div> | |
| <div class="stat-label">Cached Queries</div> | |
| </div> | |
| </div> | |
| <!-- Available Sources List --> | |
| <div class="results"> | |
| <h2> | |
| <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 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> | |
| Available OHLCV Sources | |
| </h2> | |
| <div class="source-list" id="source-list"> | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| <span>Loading sources...</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Test Results --> | |
| <div class="results" id="test-results" style="display: none;"> | |
| <h2> | |
| <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="M9 11l3 3L22 4"/> | |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> | |
| </svg> | |
| Test Results | |
| </h2> | |
| <div id="test-results-content"></div> | |
| </div> | |
| <!-- Data Preview --> | |
| <div class="results" id="data-preview" style="display: none;"> | |
| <h2> | |
| <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <rect x="3" y="3" width="18" height="18" rx="2"/> | |
| <path d="M3 9h18"/> | |
| <path d="M9 21V9"/> | |
| </svg> | |
| OHLCV Data Preview | |
| <span style="margin-left: auto; font-size: var(--text-sm); font-weight: normal; color: var(--text-tertiary);" id="data-source">Source: --</span> | |
| </h2> | |
| <div style="overflow-x: auto;"> | |
| <table class="data-table" id="data-table"> | |
| <thead> | |
| <tr> | |
| <th>Date/Time</th> | |
| <th>Open</th> | |
| <th>High</th> | |
| <th>Low</th> | |
| <th>Close</th> | |
| <th>Volume</th> | |
| </tr> | |
| </thead> | |
| <tbody id="data-table-body"> | |
| <tr> | |
| <td colspan="6" style="text-align: center; padding: var(--space-8);">No data loaded</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script type="module"> | |
| import ohlcvClient from '/static/shared/js/ohlcv-client.js'; | |
| import sidebarManager from '/static/shared/js/sidebar-manager.js'; | |
| // Load sidebar | |
| fetch('/static/shared/layouts/sidebar-modern.html') | |
| .then(r => r.text()) | |
| .then(html => { | |
| document.getElementById('sidebar-container').innerHTML = html; | |
| }); | |
| // Display available sources | |
| function displaySources() { | |
| const sources = ohlcvClient.listSources(); | |
| document.getElementById('total-sources').textContent = sources.length; | |
| const listHtml = sources.map(source => ` | |
| <div class="source-item"> | |
| <div class="source-priority">${source.priority}</div> | |
| <div class="source-info"> | |
| <div class="source-name">${source.name}</div> | |
| <div class="source-details"> | |
| Max: ${source.maxLimit.toLocaleString()} candles | |
| ${source.needsAuth ? '• API Key Required' : '• No Auth'} | |
| ${source.needsProxy ? '• Needs Proxy' : '• Direct'} | |
| </div> | |
| </div> | |
| <div class="source-status status-pending">Ready</div> | |
| </div> | |
| `).join(''); | |
| document.getElementById('source-list').innerHTML = listHtml; | |
| } | |
| // Fetch OHLCV data | |
| async function fetchOHLCV() { | |
| const symbol = document.getElementById('symbol-select').value; | |
| const timeframe = document.getElementById('timeframe-select').value; | |
| const limit = parseInt(document.getElementById('limit-input').value); | |
| const btn = document.getElementById('fetch-btn'); | |
| btn.disabled = true; | |
| btn.innerHTML = '<div class="spinner"></div> Fetching...'; | |
| try { | |
| console.log(`📊 Fetching ${symbol} ${timeframe} (${limit} candles)...`); | |
| const data = await ohlcvClient.getOHLCV(symbol, timeframe, limit); | |
| console.log(`✅ Loaded ${data.length} candles`); | |
| displayData(data, symbol, timeframe); | |
| updateStats(); | |
| btn.disabled = false; | |
| btn.innerHTML = ` | |
| <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="M3 3v18h18"/> | |
| <path d="M18 7l-5 5-4-4-5 5"/> | |
| </svg> | |
| Fetch OHLCV Data | |
| `; | |
| } catch (error) { | |
| console.error('❌ Failed to fetch OHLCV:', error); | |
| alert(`Failed to fetch OHLCV data: ${error.message}`); | |
| btn.disabled = false; | |
| btn.innerHTML = ` | |
| <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="M3 3v18h18"/> | |
| <path d="M18 7l-5 5-4-4-5 5"/> | |
| </svg> | |
| Fetch OHLCV Data | |
| `; | |
| } | |
| } | |
| // Display OHLCV data | |
| function displayData(data, symbol, timeframe) { | |
| document.getElementById('data-preview').style.display = 'block'; | |
| document.getElementById('candles-loaded').textContent = data.length.toLocaleString(); | |
| // Show first source used | |
| const stats = ohlcvClient.getStats(); | |
| const lastSuccess = stats.recentRequests.reverse().find(r => r.success); | |
| if (lastSuccess) { | |
| document.getElementById('data-source').textContent = `Source: ${lastSuccess.source}`; | |
| } | |
| // Display table (last 20 candles) | |
| const tableBody = document.getElementById('data-table-body'); | |
| const displayData = data.slice(-20).reverse(); | |
| tableBody.innerHTML = displayData.map(candle => ` | |
| <tr> | |
| <td>${new Date(candle.timestamp).toLocaleString()}</td> | |
| <td>$${candle.open?.toFixed(2) || 'N/A'}</td> | |
| <td>$${candle.high?.toFixed(2) || 'N/A'}</td> | |
| <td>$${candle.low?.toFixed(2) || 'N/A'}</td> | |
| <td>$${candle.close?.toFixed(2) || 'N/A'}</td> | |
| <td>${candle.volume ? candle.volume.toFixed(2) : 'N/A'}</td> | |
| </tr> | |
| `).join(''); | |
| } | |
| // Test all sources | |
| async function testAllSources() { | |
| const symbol = document.getElementById('symbol-select').value; | |
| const timeframe = document.getElementById('timeframe-select').value; | |
| const btn = document.getElementById('test-all-btn'); | |
| btn.disabled = true; | |
| btn.innerHTML = '<div class="spinner"></div> Testing...'; | |
| document.getElementById('test-results').style.display = 'block'; | |
| document.getElementById('test-results-content').innerHTML = ` | |
| <div class="loading"> | |
| <div class="spinner"></div> | |
| <span>Testing all sources... This may take a minute...</span> | |
| </div> | |
| `; | |
| try { | |
| const results = await ohlcvClient.testAllSources(symbol, timeframe, 10); | |
| const resultsHtml = ` | |
| <div class="source-list"> | |
| ${results.map(result => ` | |
| <div class="source-item"> | |
| <div class="source-priority">${result.priority}</div> | |
| <div class="source-info"> | |
| <div class="source-name">${result.source}</div> | |
| <div class="source-details"> | |
| ${result.status === 'SUCCESS' | |
| ? `${result.candles} candles in ${result.duration}` | |
| : result.error} | |
| </div> | |
| </div> | |
| <div class="source-status ${result.status === 'SUCCESS' ? 'status-success' : 'status-failed'}"> | |
| ${result.status} | |
| </div> | |
| </div> | |
| `).join('')} | |
| </div> | |
| `; | |
| document.getElementById('test-results-content').innerHTML = resultsHtml; | |
| updateStats(); | |
| btn.disabled = false; | |
| btn.innerHTML = ` | |
| <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="M9 11l3 3L22 4"/> | |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> | |
| </svg> | |
| Test All Sources | |
| `; | |
| } catch (error) { | |
| console.error('Test failed:', error); | |
| document.getElementById('test-results-content').innerHTML = ` | |
| <div style="color: var(--color-danger); padding: var(--space-4);"> | |
| Test failed: ${error.message} | |
| </div> | |
| `; | |
| btn.disabled = false; | |
| btn.innerHTML = ` | |
| <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="M9 11l3 3L22 4"/> | |
| <path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/> | |
| </svg> | |
| Test All Sources | |
| `; | |
| } | |
| } | |
| // Update statistics | |
| function updateStats() { | |
| const stats = ohlcvClient.getStats(); | |
| document.getElementById('success-rate').textContent = stats.successRate; | |
| document.getElementById('cache-size').textContent = stats.cacheSize; | |
| } | |
| // Event listeners | |
| document.getElementById('fetch-btn').addEventListener('click', fetchOHLCV); | |
| document.getElementById('test-all-btn').addEventListener('click', testAllSources); | |
| // Initialize | |
| displaySources(); | |
| updateStats(); | |
| // Make client available in console | |
| window.ohlcvClient = ohlcvClient; | |
| console.log('💡 OHLCV Client ready! Try: ohlcvClient.getOHLCV("bitcoin", "1d", 100)'); | |
| </script> | |
| </body> | |
| </html> | |