Commit 79a2e226 authored by Your Name's avatar Your Name

Add live BTC price display in admin payment settings

- Added live BTC price section showing prices from Coinbase, Binance, and Kraken
- Displays average price calculated from enabled sources
- Shows prices in configured currency (USD by default)
- Auto-refreshes every 60 seconds
- Manual refresh button available
- New API endpoint: GET /api/admin/crypto/btc-prices
- Fetches real-time prices from exchange APIs
- Gracefully handles disabled sources and API errors
- Shows 'Disabled' for unchecked sources, 'Error' for failed fetches
parent de387e96
...@@ -6249,6 +6249,103 @@ async def api_get_encryption_key_status(request: Request): ...@@ -6249,6 +6249,103 @@ async def api_get_encryption_key_status(request: Request):
logger.error(f"Error getting encryption key status: {e}") logger.error(f"Error getting encryption key status: {e}")
return JSONResponse({"error": str(e)}, status_code=500) 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"""
auth_check = require_admin(request)
if auth_check:
return auth_check
try:
db = DatabaseRegistry.get_config_database()
# Get enabled price sources
with db._get_connection() as conn:
cursor = conn.cursor()
try:
cursor.execute("""
SELECT name, is_enabled
FROM crypto_price_sources
""")
sources = {row[0].lower(): bool(row[1]) for row in cursor.fetchall()}
except:
# Default if table doesn't exist yet
sources = {'coinbase': True, 'binance': True, 'kraken': True}
# Get currency settings
currency_settings = db.get_currency_settings()
currency_code = currency_settings.get('currency_code', 'USD')
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/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}")
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
enabled_prices.append(price)
except Exception as e:
logger.warning(f"Error fetching Kraken BTC price: {e}")
prices['kraken'] = None
else:
prices['kraken'] = None
# Calculate average
if enabled_prices:
prices['average'] = sum(enabled_prices) / len(enabled_prices)
else:
prices['average'] = None
return JSONResponse(prices)
except Exception as e:
logger.error(f"Error getting BTC prices: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
return JSONResponse({"error": str(e)}, status_code=500)
@app.post("/api/admin/settings/encryption-key") @app.post("/api/admin/settings/encryption-key")
async def api_save_encryption_key(request: Request): async def api_save_encryption_key(request: Request):
"""Save encryption key - API endpoint""" """Save encryption key - API endpoint"""
......
...@@ -305,6 +305,51 @@ ...@@ -305,6 +305,51 @@
</label> </label>
</div> </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;">
<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;">
<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;">
<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;">
<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)
</div>
<div id="priceAverageValue" style="color: #f7931a; font-size: 28px; font-weight: bold;">
<i class="fas fa-spinner fa-spin"></i>
</div>
<div style="color: #888; font-size: 12px; margin-top: 5px;">
Based on enabled sources • Last updated: <span id="priceLastUpdated">-</span>
</div>
</div>
</div>
<button type="button" class="btn" onclick="savePriceSources()" style="background: #f39c12; color: white;"> <button type="button" class="btn" onclick="savePriceSources()" style="background: #f39c12; color: white;">
<i class="fas fa-save me-2"></i>Save Price Sources <i class="fas fa-save me-2"></i>Save Price Sources
</button> </button>
...@@ -823,6 +868,74 @@ document.addEventListener('DOMContentLoaded', function() { ...@@ -823,6 +868,74 @@ document.addEventListener('DOMContentLoaded', function() {
loadSystemStatus(); loadSystemStatus();
loadConfiguration(); loadConfiguration();
loadPaymentGateways(); loadPaymentGateways();
loadBtcPrices();
// Auto-refresh BTC prices every 60 seconds
setInterval(loadBtcPrices, 60000);
}); });
// Load BTC prices from all sources
async function loadBtcPrices() {
try {
const response = await fetch('{{ url_for(request, "/api/admin/crypto/btc-prices") }}');
const data = await response.json();
// Get currency settings for symbol
const currencyResponse = await fetch('{{ url_for(request, "/api/admin/settings/currency") }}');
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>';
}
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>';
}
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 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 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>';
}
}
// 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();
}
</script> </script>
{% endblock %} {% endblock %}
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