Commit f0f3d96f authored by Your Name's avatar Your Name

Expand crypto price display to show BTC, ETH, USDT, USDC

- Updated UI to show prices for all 4 supported cryptocurrencies
- Each crypto has its own section with color-coded borders
- Shows prices from Coinbase, Binance, and Kraken for each
- Displays average price (applied) for each cryptocurrency
- New API endpoint: GET /api/admin/crypto/prices (returns all 4 cryptos)
- Legacy endpoint /api/admin/crypto/btc-prices still works (redirects)
- Handles stablecoin pricing correctly (USDT/USDC near 1.00)
- Auto-refreshes all prices every 60 seconds
- Single refresh button updates all cryptocurrencies
parent 79a2e226
......@@ -6249,9 +6249,9 @@ async def api_get_encryption_key_status(request: Request):
logger.error(f"Error getting encryption key status: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/admin/crypto/btc-prices")
async def api_get_btc_prices(request: Request):
"""Get BTC prices from all enabled sources - API endpoint"""
@app.get("/api/admin/crypto/prices")
async def api_get_crypto_prices(request: Request):
"""Get crypto prices (BTC, ETH, USDT, USDC) from all enabled sources - API endpoint"""
auth_check = require_admin(request)
if auth_check:
return auth_check
......@@ -6276,74 +6276,110 @@ async def api_get_btc_prices(request: Request):
currency_settings = db.get_currency_settings()
currency_code = currency_settings.get('currency_code', 'USD')
prices = {}
enabled_prices = []
result = {}
# Fetch from Coinbase
if sources.get('coinbase', False):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(f'https://api.coinbase.com/v2/prices/BTC-{currency_code}/spot')
if response.status_code == 200:
data = response.json()
price = float(data['data']['amount'])
prices['coinbase'] = price
enabled_prices.append(price)
except Exception as e:
logger.warning(f"Error fetching Coinbase BTC price: {e}")
# Fetch prices for each cryptocurrency
for crypto_symbol, crypto_name in [('BTC', 'btc'), ('ETH', 'eth'), ('USDT', 'usdt'), ('USDC', 'usdc')]:
prices = {}
enabled_prices = []
# Fetch from Coinbase
if sources.get('coinbase', False):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
response = await client.get(f'https://api.coinbase.com/v2/prices/{crypto_symbol}-{currency_code}/spot')
if response.status_code == 200:
data = response.json()
price = float(data['data']['amount'])
prices['coinbase'] = price
enabled_prices.append(price)
except Exception as e:
logger.warning(f"Error fetching Coinbase {crypto_symbol} price: {e}")
prices['coinbase'] = None
else:
prices['coinbase'] = None
else:
prices['coinbase'] = None
# Fetch from Binance
if sources.get('binance', False):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Binance uses USDT pairs, convert if needed
symbol = 'BTCUSDT' if currency_code == 'USD' else f'BTC{currency_code}'
response = await client.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
if response.status_code == 200:
data = response.json()
price = float(data['price'])
prices['binance'] = price
enabled_prices.append(price)
except Exception as e:
logger.warning(f"Error fetching Binance BTC price: {e}")
prices['binance'] = None
else:
prices['binance'] = None
# Fetch from Kraken
if sources.get('kraken', False):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Kraken uses XBT instead of BTC
pair = f'XXBTZ{currency_code}' if currency_code == 'USD' else f'XXBTZ{currency_code}'
response = await client.get(f'https://api.kraken.com/0/public/Ticker?pair={pair}')
if response.status_code == 200:
data = response.json()
if 'result' in data and data['result']:
# Get first result key
result_key = list(data['result'].keys())[0]
price = float(data['result'][result_key]['c'][0])
prices['kraken'] = price
# Fetch from Binance
if sources.get('binance', False):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Binance uses USDT pairs for most, but handle stablecoins differently
if crypto_symbol in ['USDT', 'USDC']:
symbol = f'{crypto_symbol}USDT' if crypto_symbol == 'USDC' else 'USDCUSDT'
else:
symbol = f'{crypto_symbol}USDT' if currency_code == 'USD' else f'{crypto_symbol}{currency_code}'
response = await client.get(f'https://api.binance.com/api/v3/ticker/price?symbol={symbol}')
if response.status_code == 200:
data = response.json()
price = float(data['price'])
prices['binance'] = price
enabled_prices.append(price)
except Exception as e:
logger.warning(f"Error fetching Kraken BTC price: {e}")
except Exception as e:
logger.warning(f"Error fetching Binance {crypto_symbol} price: {e}")
prices['binance'] = None
else:
prices['binance'] = None
# Fetch from Kraken
if sources.get('kraken', False):
try:
async with httpx.AsyncClient(timeout=10.0) as client:
# Kraken uses different symbols
if crypto_symbol == 'BTC':
pair = f'XXBTZ{currency_code}'
elif crypto_symbol == 'ETH':
pair = f'XETHZ{currency_code}'
elif crypto_symbol == 'USDT':
pair = f'USDTZ{currency_code}'
elif crypto_symbol == 'USDC':
pair = f'USDCZ{currency_code}'
response = await client.get(f'https://api.kraken.com/0/public/Ticker?pair={pair}')
if response.status_code == 200:
data = response.json()
if 'result' in data and data['result']:
# Get first result key
result_key = list(data['result'].keys())[0]
price = float(data['result'][result_key]['c'][0])
prices['kraken'] = price
enabled_prices.append(price)
except Exception as e:
logger.warning(f"Error fetching Kraken {crypto_symbol} price: {e}")
prices['kraken'] = None
else:
prices['kraken'] = None
else:
prices['kraken'] = None
# Calculate average
if enabled_prices:
prices['average'] = sum(enabled_prices) / len(enabled_prices)
else:
prices['average'] = None
# Calculate average
if enabled_prices:
prices['average'] = sum(enabled_prices) / len(enabled_prices)
else:
prices['average'] = None
result[crypto_name] = prices
return JSONResponse(prices)
return JSONResponse(result)
except Exception as e:
logger.error(f"Error getting BTC prices: {e}")
logger.error(f"Error getting crypto prices: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.get("/api/admin/crypto/btc-prices")
async def api_get_btc_prices(request: Request):
"""Get BTC prices from all enabled sources - API endpoint (legacy, redirects to /prices)"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
# Call the new endpoint and extract BTC data
full_response = await api_get_crypto_prices(request)
if isinstance(full_response, JSONResponse):
import json
data = json.loads(full_response.body.decode())
if 'btc' in data:
return JSONResponse(data['btc'])
return full_response
except Exception as e:
logger.error(f"Error getting BTC prices: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@app.post("/api/admin/settings/encryption-key")
......
......@@ -305,49 +305,150 @@
</label>
</div>
<!-- Live BTC Price Display -->
<div style="margin-bottom: 20px; padding: 20px; background: #1a1a2e; border-radius: 8px; border-left: 4px solid #f7931a;">
<div style="color: #f7931a; font-weight: bold; margin-bottom: 15px; display: flex; align-items: center;">
<i class="fab fa-bitcoin me-2"></i>Live Bitcoin Price
<button onclick="refreshBtcPrices()" style="margin-left: auto; background: transparent; border: 1px solid #f7931a; color: #f7931a; padding: 5px 15px; border-radius: 5px; cursor: pointer; font-size: 12px;">
<!-- Live Crypto Prices Display -->
<div style="margin-bottom: 20px; padding: 20px; background: #1a1a2e; border-radius: 8px; border-left: 4px solid #4a9eff;">
<div style="color: #4a9eff; font-weight: bold; margin-bottom: 15px; display: flex; align-items: center;">
<i class="fas fa-coins me-2"></i>Live Cryptocurrency Prices
<button onclick="refreshCryptoPrices()" style="margin-left: auto; background: transparent; border: 1px solid #4a9eff; color: #4a9eff; padding: 5px 15px; border-radius: 5px; cursor: pointer; font-size: 12px;">
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 15px;">
<div style="background: #16213e; padding: 15px; border-radius: 8px;">
<div style="color: #888; font-size: 12px; margin-bottom: 5px;">Coinbase</div>
<div id="priceCoinbaseValue" style="color: #4a9eff; font-size: 20px; font-weight: bold;">
<!-- Bitcoin -->
<div style="margin-bottom: 20px; padding: 15px; background: #16213e; border-radius: 8px; border-left: 3px solid #f7931a;">
<div style="color: #f7931a; font-weight: bold; margin-bottom: 10px;">
<i class="fab fa-bitcoin me-2"></i>Bitcoin (BTC)
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 10px;">
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Coinbase</div>
<div id="priceBtcCoinbase" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Binance</div>
<div id="priceBtcBinance" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Kraken</div>
<div id="priceBtcKraken" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px; border: 2px solid #f7931a;">
<div style="color: #f7931a; font-size: 12px; font-weight: bold;">Average (Applied)</div>
<div id="priceBtcAverage" style="color: #f7931a; font-size: 20px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #16213e; padding: 15px; border-radius: 8px;">
<div style="color: #888; font-size: 12px; margin-bottom: 5px;">Binance</div>
<div id="priceBinanceValue" style="color: #4a9eff; font-size: 20px; font-weight: bold;">
</div>
<!-- Ethereum -->
<div style="margin-bottom: 20px; padding: 15px; background: #16213e; border-radius: 8px; border-left: 3px solid #627eea;">
<div style="color: #627eea; font-weight: bold; margin-bottom: 10px;">
<i class="fab fa-ethereum me-2"></i>Ethereum (ETH)
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 10px;">
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Coinbase</div>
<div id="priceEthCoinbase" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Binance</div>
<div id="priceEthBinance" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Kraken</div>
<div id="priceEthKraken" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px; border: 2px solid #627eea;">
<div style="color: #627eea; font-size: 12px; font-weight: bold;">Average (Applied)</div>
<div id="priceEthAverage" style="color: #627eea; font-size: 20px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #16213e; padding: 15px; border-radius: 8px;">
<div style="color: #888; font-size: 12px; margin-bottom: 5px;">Kraken</div>
<div id="priceKrakenValue" style="color: #4a9eff; font-size: 20px; font-weight: bold;">
</div>
<!-- USDT -->
<div style="margin-bottom: 20px; padding: 15px; background: #16213e; border-radius: 8px; border-left: 3px solid #26a17b;">
<div style="color: #26a17b; font-weight: bold; margin-bottom: 10px;">
<i class="fas fa-dollar-sign me-2"></i>Tether (USDT)
</div>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 10px;">
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Coinbase</div>
<div id="priceUsdtCoinbase" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Binance</div>
<div id="priceUsdtBinance" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Kraken</div>
<div id="priceUsdtKraken" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px; border: 2px solid #26a17b;">
<div style="color: #26a17b; font-size: 12px; font-weight: bold;">Average (Applied)</div>
<div id="priceUsdtAverage" style="color: #26a17b; font-size: 20px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
</div>
<div style="background: #16213e; padding: 15px; border-radius: 8px; border: 2px solid #f7931a;">
<div style="color: #f7931a; font-size: 14px; font-weight: bold; margin-bottom: 5px;">
<i class="fas fa-calculator me-2"></i>Average Price (Applied)
<!-- USDC -->
<div style="margin-bottom: 15px; padding: 15px; background: #16213e; border-radius: 8px; border-left: 3px solid #2775ca;">
<div style="color: #2775ca; font-weight: bold; margin-bottom: 10px;">
<i class="fas fa-coins me-2"></i>USD Coin (USDC)
</div>
<div id="priceAverageValue" style="color: #f7931a; font-size: 28px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 10px; margin-bottom: 10px;">
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Coinbase</div>
<div id="priceUsdcCoinbase" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Binance</div>
<div id="priceUsdcBinance" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
<div style="background: #0f3460; padding: 10px; border-radius: 5px;">
<div style="color: #888; font-size: 11px;">Kraken</div>
<div id="priceUsdcKraken" style="color: #4a9eff; font-size: 16px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
</div>
<div style="color: #888; font-size: 12px; margin-top: 5px;">
Based on enabled sources • Last updated: <span id="priceLastUpdated">-</span>
<div style="background: #0f3460; padding: 10px; border-radius: 5px; border: 2px solid #2775ca;">
<div style="color: #2775ca; font-size: 12px; font-weight: bold;">Average (Applied)</div>
<div id="priceUsdcAverage" style="color: #2775ca; font-size: 20px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
</div>
</div>
<div style="color: #888; font-size: 12px; text-align: center;">
Last updated: <span id="priceLastUpdated">-</span>
</div>
</div>
<button type="button" class="btn" onclick="savePriceSources()" style="background: #f39c12; color: white;">
......@@ -868,16 +969,16 @@ document.addEventListener('DOMContentLoaded', function() {
loadSystemStatus();
loadConfiguration();
loadPaymentGateways();
loadBtcPrices();
loadCryptoPrices();
// Auto-refresh BTC prices every 60 seconds
setInterval(loadBtcPrices, 60000);
// Auto-refresh crypto prices every 60 seconds
setInterval(loadCryptoPrices, 60000);
});
// Load BTC prices from all sources
async function loadBtcPrices() {
// Load crypto prices from all sources
async function loadCryptoPrices() {
try {
const response = await fetch('{{ url_for(request, "/api/admin/crypto/btc-prices") }}');
const response = await fetch('{{ url_for(request, "/api/admin/crypto/prices") }}');
const data = await response.json();
// Get currency settings for symbol
......@@ -885,56 +986,76 @@ async function loadBtcPrices() {
const currencyData = await currencyResponse.json();
const currencySymbol = currencyData.currency_symbol || '$';
// Update individual prices
if (data.coinbase !== null && data.coinbase !== undefined) {
document.getElementById('priceCoinbaseValue').textContent = currencySymbol + data.coinbase.toFixed(2);
document.getElementById('priceCoinbaseValue').style.color = '#27ae60';
} else {
document.getElementById('priceCoinbaseValue').innerHTML = '<span style="color: #888;">Disabled</span>';
}
// Update BTC prices
updateCryptoPrice('Btc', data.btc, currencySymbol);
if (data.binance !== null && data.binance !== undefined) {
document.getElementById('priceBinanceValue').textContent = currencySymbol + data.binance.toFixed(2);
document.getElementById('priceBinanceValue').style.color = '#27ae60';
} else {
document.getElementById('priceBinanceValue').innerHTML = '<span style="color: #888;">Disabled</span>';
}
// Update ETH prices
updateCryptoPrice('Eth', data.eth, currencySymbol);
if (data.kraken !== null && data.kraken !== undefined) {
document.getElementById('priceKrakenValue').textContent = currencySymbol + data.kraken.toFixed(2);
document.getElementById('priceKrakenValue').style.color = '#27ae60';
} else {
document.getElementById('priceKrakenValue').innerHTML = '<span style="color: #888;">Disabled</span>';
}
// Update USDT prices
updateCryptoPrice('Usdt', data.usdt, currencySymbol);
// Update average price
if (data.average !== null && data.average !== undefined) {
document.getElementById('priceAverageValue').textContent = currencySymbol + data.average.toFixed(2);
document.getElementById('priceAverageValue').style.color = '#f7931a';
} else {
document.getElementById('priceAverageValue').innerHTML = '<span style="color: #e74c3c;">No sources enabled</span>';
}
// Update USDC prices
updateCryptoPrice('Usdc', data.usdc, currencySymbol);
// Update last updated time
const now = new Date();
document.getElementById('priceLastUpdated').textContent = now.toLocaleTimeString();
} catch (error) {
console.error('Error loading BTC prices:', error);
document.getElementById('priceCoinbaseValue').innerHTML = '<span style="color: #e74c3c;">Error</span>';
document.getElementById('priceBinanceValue').innerHTML = '<span style="color: #e74c3c;">Error</span>';
document.getElementById('priceKrakenValue').innerHTML = '<span style="color: #e74c3c;">Error</span>';
document.getElementById('priceAverageValue').innerHTML = '<span style="color: #e74c3c;">Error loading</span>';
console.error('Error loading crypto prices:', error);
['Btc', 'Eth', 'Usdt', 'Usdc'].forEach(crypto => {
document.getElementById(`price${crypto}Coinbase`).innerHTML = '<span style="color: #e74c3c;">Error</span>';
document.getElementById(`price${crypto}Binance`).innerHTML = '<span style="color: #e74c3c;">Error</span>';
document.getElementById(`price${crypto}Kraken`).innerHTML = '<span style="color: #e74c3c;">Error</span>';
document.getElementById(`price${crypto}Average`).innerHTML = '<span style="color: #e74c3c;">Error</span>';
});
}
}
// Helper function to update individual crypto prices
function updateCryptoPrice(crypto, priceData, currencySymbol) {
// Update Coinbase
if (priceData.coinbase !== null && priceData.coinbase !== undefined) {
document.getElementById(`price${crypto}Coinbase`).textContent = currencySymbol + priceData.coinbase.toFixed(2);
document.getElementById(`price${crypto}Coinbase`).style.color = '#27ae60';
} else {
document.getElementById(`price${crypto}Coinbase`).innerHTML = '<span style="color: #888;">Disabled</span>';
}
// Update Binance
if (priceData.binance !== null && priceData.binance !== undefined) {
document.getElementById(`price${crypto}Binance`).textContent = currencySymbol + priceData.binance.toFixed(2);
document.getElementById(`price${crypto}Binance`).style.color = '#27ae60';
} else {
document.getElementById(`price${crypto}Binance`).innerHTML = '<span style="color: #888;">Disabled</span>';
}
// Update Kraken
if (priceData.kraken !== null && priceData.kraken !== undefined) {
document.getElementById(`price${crypto}Kraken`).textContent = currencySymbol + priceData.kraken.toFixed(2);
document.getElementById(`price${crypto}Kraken`).style.color = '#27ae60';
} else {
document.getElementById(`price${crypto}Kraken`).innerHTML = '<span style="color: #888;">Disabled</span>';
}
// Update Average
if (priceData.average !== null && priceData.average !== undefined) {
document.getElementById(`price${crypto}Average`).textContent = currencySymbol + priceData.average.toFixed(2);
} else {
document.getElementById(`price${crypto}Average`).innerHTML = '<span style="color: #e74c3c;">No sources</span>';
}
}
// Refresh BTC prices manually
function refreshBtcPrices() {
document.getElementById('priceCoinbaseValue').innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById('priceBinanceValue').innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById('priceKrakenValue').innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById('priceAverageValue').innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
loadBtcPrices();
// Refresh crypto prices manually
function refreshCryptoPrices() {
['Btc', 'Eth', 'Usdt', 'Usdc'].forEach(crypto => {
document.getElementById(`price${crypto}Coinbase`).innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById(`price${crypto}Binance`).innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById(`price${crypto}Kraken`).innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
document.getElementById(`price${crypto}Average`).innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
});
loadCryptoPrices();
}
</script>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment