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
<!DOCTYPE html>
<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>&nbsp;</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>&nbsp;</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>