Add currency support and config management enhancements

This commit introduces significant updates to the application, focusing on currency support and improved configuration management. Key changes include:

- Added a `config_reset` flag in `update_metrics_job` in `App.py` to manage configuration resets.
- Modified `update_config` to handle currency changes and update notifications accordingly.
- Updated `MiningDashboardService` to fetch and include exchange rates and configured currency in metrics.
- Introduced utility functions for currency formatting and symbol retrieval in `notification_service.py`.
- Enhanced UI components in `main.js` and `boot.html` to support currency selection and display.
- Adjusted Docker Compose file to remove hardcoded wallet and power settings for better flexibility.

These changes enhance usability by allowing users to view financial data in their preferred currency and manage configurations more effectively.
This commit is contained in:
DJObleezy 2025-04-27 14:43:45 -07:00
parent 2f1cbd3143
commit a50bd552f7
7 changed files with 349 additions and 56 deletions

37
App.py
View File

@ -368,12 +368,25 @@ def update_metrics_job(force=False):
if metrics:
logging.info("Fetched metrics successfully")
# Add config_reset flag to metrics from the config
config = load_config()
metrics["config_reset"] = config.get("config_reset", False)
logging.info(f"Added config_reset flag to metrics: {metrics.get('config_reset')}")
# First check for notifications by comparing new metrics with old cached metrics
notification_service.check_and_generate_notifications(metrics, cached_metrics)
# Then update cached metrics after comparison
cached_metrics = metrics
# Clear the config_reset flag after it's been used
if metrics.get("config_reset"):
config = load_config()
if "config_reset" in config:
del config["config_reset"]
save_config(config)
logging.info("Cleared config_reset flag from configuration after use")
# Update state history (only once)
state_manager.update_metrics_history(metrics)
@ -771,7 +784,7 @@ def get_config():
@app.route("/api/config", methods=["POST"])
def update_config():
"""API endpoint to update configuration."""
global dashboard_service, worker_service # Add this to access the global dashboard_service
global dashboard_service, worker_service
try:
# Get the request data
@ -783,11 +796,19 @@ def update_config():
logging.error("Invalid configuration format")
return jsonify({"error": "Invalid configuration format"}), 400
# Get current config to check if currency is changing
current_config = load_config()
currency_changed = (
"currency" in new_config and
new_config.get("currency") != current_config.get("currency", "USD")
)
# Required fields and default values
defaults = {
"wallet": "yourwallethere",
"power_cost": 0.0,
"power_usage": 0.0
"power_usage": 0.0,
"currency": "USD" # Add default currency
}
# Merge new config with defaults for any missing fields
@ -802,7 +823,8 @@ def update_config():
dashboard_service = MiningDashboardService(
new_config.get("power_cost", 0.0),
new_config.get("power_usage", 0.0),
new_config.get("wallet")
new_config.get("wallet"),
network_fee=new_config.get("network_fee", 0.0) # Include network fee
)
logging.info(f"Dashboard service reinitialized with new wallet: {new_config.get('wallet')}")
@ -810,6 +832,15 @@ def update_config():
worker_service.set_dashboard_service(dashboard_service)
logging.info(f"Worker service updated with the new dashboard service")
# If currency changed, update notifications to use the new currency
if currency_changed:
try:
logging.info(f"Currency changed from {current_config.get('currency', 'USD')} to {new_config['currency']}")
updated_count = notification_service.update_notification_currency()
logging.info(f"Updated {updated_count} notifications to use {new_config['currency']} currency")
except Exception as e:
logging.error(f"Error updating notification currency: {e}")
# Force a metrics update to reflect the new configuration
update_metrics_job(force=True)
logging.info("Forced metrics update after configuration change")

View File

@ -164,6 +164,18 @@ class MiningDashboardService:
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
metrics["server_start_time"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
# Get the configured currency
from config import load_config
config = load_config()
selected_currency = config.get("currency", "USD")
# Fetch exchange rates
exchange_rates = self.fetch_exchange_rates()
# Add to metrics
metrics["currency"] = selected_currency
metrics["exchange_rates"] = exchange_rates
# Log execution time
execution_time = time.time() - start_time
metrics["execution_time"] = execution_time
@ -452,6 +464,32 @@ class MiningDashboardService:
logging.error(f"Error fetching {url}: {e}")
return None
# Add the fetch_exchange_rates method after the fetch_url method
def fetch_exchange_rates(self, base_currency="USD"):
"""
Fetch currency exchange rates from a public API.
Args:
base_currency (str): Base currency for rates (default: USD)
Returns:
dict: Exchange rates for supported currencies
"""
try:
# Use exchangerate-api for currency rates
url = f"https://api.exchangerate-api.com/v4/latest/{base_currency}"
response = self.session.get(url, timeout=5)
if response.ok:
data = response.json()
return data.get('rates', {})
else:
logging.error(f"Failed to fetch exchange rates: {response.status_code}")
return {}
except Exception as e:
logging.error(f"Error fetching exchange rates: {e}")
return {}
def get_bitcoin_stats(self):
"""
Fetch Bitcoin network statistics with improved error handling and caching.

View File

@ -16,11 +16,6 @@ services:
- "5000:5000"
environment:
- REDIS_URL=redis://redis:6379
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS
- POWER_COST=0
- POWER_USAGE=0
- NETWORK_FEE=0
- TIMEZONE=America/Los_Angeles
- LOG_LEVEL=INFO
volumes:
- ./logs:/app/logs

View File

@ -1,14 +1,17 @@
# notification_service.py
# notification_service.py
import logging
import json
import time
import uuid
import pytz
import re
from datetime import datetime, timedelta
from enum import Enum
from collections import deque
from typing import List, Dict, Any, Optional, Union
from config import get_timezone
from config import get_timezone, load_config
from data_service import MiningDashboardService
# Constants to replace magic values
ONE_DAY_SECONDS = 86400
@ -29,6 +32,52 @@ class NotificationCategory(Enum):
EARNINGS = "earnings"
SYSTEM = "system"
# Currency utility functions
def get_currency_symbol(currency):
"""Return symbol for the specified currency"""
symbols = {
'USD': '$',
'EUR': '',
'GBP': '£',
'JPY': '¥',
'CAD': 'CA$',
'AUD': 'A$',
'CNY': '¥',
'KRW': '',
'BRL': 'R$',
'CHF': 'Fr'
}
return symbols.get(currency, '$')
def format_currency_value(value, currency, exchange_rates):
"""Format a USD value in the selected currency"""
if value is None or value == "N/A":
return "N/A"
# Get exchange rate (default to 1.0 if not found)
exchange_rate = exchange_rates.get(currency, 1.0)
converted_value = value * exchange_rate
# Get currency symbol
symbol = get_currency_symbol(currency)
# Format with or without decimals based on currency
if currency in ['JPY', 'KRW']:
return f"{symbol}{int(converted_value):,}"
else:
return f"{symbol}{converted_value:.2f}"
def get_exchange_rates():
"""Get exchange rates with caching"""
try:
# Create a dashboard service instance for fetching exchange rates
dashboard_service = MiningDashboardService(0, 0, "", 0)
exchange_rates = dashboard_service.fetch_exchange_rates()
return exchange_rates
except Exception as e:
logging.error(f"Error fetching exchange rates for notifications: {e}")
return {} # Return empty dict if failed
class NotificationService:
"""Service for managing mining dashboard notifications."""
@ -313,6 +362,11 @@ class NotificationService:
logging.warning("[NotificationService] No current metrics available, skipping notification checks")
return new_notifications
# Skip notification generation after configuration reset
if current_metrics.get("wallet") == "yourwallethere" or current_metrics.get("config_reset", False):
logging.info("[NotificationService] Configuration reset detected, skipping all notifications")
return new_notifications
# Check for block updates (using persistent storage)
last_block_height = current_metrics.get("last_block_height")
if last_block_height and last_block_height != "N/A":
@ -384,12 +438,22 @@ class NotificationService:
hashrate_24hr = metrics.get("hashrate_24hr", 0)
hashrate_unit = metrics.get("hashrate_24hr_unit", "TH/s")
# Format daily earnings
# Format daily earnings with user's currency
daily_mined_sats = metrics.get("daily_mined_sats", 0)
daily_profit_usd = metrics.get("daily_profit_usd", 0)
# Get user's currency preference
config = load_config()
user_currency = config.get("currency", "USD")
# Get exchange rates
exchange_rates = get_exchange_rates()
# Format with the user's currency
formatted_profit = format_currency_value(daily_profit_usd, user_currency, exchange_rates)
# Build message
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined (${daily_profit_usd:.2f})"
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined ({formatted_profit})"
# Add notification
logging.info(f"[NotificationService] Generating daily stats notification: {message}")
@ -401,7 +465,8 @@ class NotificationService:
"hashrate": hashrate_24hr,
"unit": hashrate_unit,
"daily_sats": daily_mined_sats,
"daily_profit": daily_profit_usd
"daily_profit": daily_profit_usd,
"currency": user_currency
}
)
except Exception as e:
@ -551,6 +616,18 @@ class NotificationService:
def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Check for significant earnings progress or payout approach."""
try:
# First check for configuration reset via Alt+W (this is a more robust check)
# This specifically looks for the default "yourwallethere" wallet which indicates Alt+W was used
current_wallet = str(current.get("wallet", ""))
if current_wallet == "yourwallethere":
logging.info("[NotificationService] Detected wallet reset to default (likely Alt+W) - skipping payout notification")
return None
# Check if ANY config value changed that might affect balance
if self._is_configuration_change_affecting_balance(current, previous):
logging.info("[NotificationService] Configuration change detected - skipping payout notification")
return None
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0"))
# Check if approaching payout
@ -605,6 +682,24 @@ class NotificationService:
logging.error(f"[NotificationService] Error checking earnings progress: {e}")
return None
def _is_configuration_change_affecting_balance(self, current: Dict[str, Any], previous: Dict[str, Any]) -> bool:
"""Check if any configuration changed that would affect balance calculations."""
# Check wallet
if "wallet" in current and "wallet" in previous:
if current.get("wallet") != previous.get("wallet"):
return True
# Check currency
if "currency" in current and "currency" in previous:
if current.get("currency") != previous.get("currency"):
return True
# Check for emergency reset flag
if current.get("config_reset", False):
return True
return False
def _should_send_payout_notification(self) -> bool:
"""Check if enough time has passed since the last payout notification."""
if self.last_payout_notification_time is None:

View File

@ -397,8 +397,12 @@ const BitcoinMinuteRefresh = (function () {
<div class="terminal-header">
<div class="terminal-title">SYSTEM MONITOR v.3</div>
<div class="terminal-controls">
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()"></div>
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()"></div>
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()">
<span class="control-symbol">-</span>
</div>
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()">
<span class="control-symbol">x</span>
</div>
</div>
</div>
<div class="terminal-content">
@ -517,22 +521,45 @@ const BitcoinMinuteRefresh = (function () {
}
.terminal-dot {
width: 8px;
height: 8px;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #555;
cursor: pointer;
transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.control-symbol {
color: #333;
font-size: 9px;
font-weight: bold;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
line-height: 1;
}
.terminal-dot.minimize:hover {
background-color: #ffcc00;
}
.terminal-dot.minimize:hover .control-symbol {
color: #664e00;
}
.terminal-dot.close:hover {
background-color: #ff3b30;
}
.terminal-dot.close:hover .control-symbol {
color: #7a0200;
}
/* Terminal Content */
.terminal-content {
position: relative;

View File

@ -1634,7 +1634,7 @@ function calculatePoolFeeInSats(poolFeePercentage, lastBlockEarnings) {
return -Math.round(feeAmount);
}
// Main UI update function with hashrate normalization
// Main UI update function with currency support
function updateUI() {
function ensureElementStyles() {
// Create a style element if it doesn't exist
@ -1735,15 +1735,31 @@ function updateUI() {
try {
const data = latestMetrics;
// Get currency and exchange rate information
const currency = data.currency || 'USD';
const exchangeRate = data.exchange_rates && data.exchange_rates[currency] ?
data.exchange_rates[currency] : 1.0;
// Update currency-related labels
const earningsHeader = document.querySelector('.card-header');
if (earningsHeader && earningsHeader.textContent.includes("EARNINGS")) {
// Find the card header that says "USD EARNINGS" and update it
const headers = document.querySelectorAll('.card-header');
headers.forEach(header => {
if (header.textContent.includes("EARNINGS") &&
header.textContent.match(/[A-Z]{3} EARNINGS/)) {
header.textContent = `${currency} EARNINGS`;
}
});
}
// If this is the initial load, force a reset of all arrows
if (initialLoad) {
arrowIndicator.forceApplyArrows();
initialLoad = false;
}
// Cache jQuery selectors for performance and use safe update methods
// Format each hashrate with proper normalization
// Pool Hashrate
let formattedPoolHashrate = "N/A";
if (data.pool_total_hashrate != null) {
@ -2079,9 +2095,15 @@ function updateUI() {
// Update other non-hashrate metrics
updateElementText("block_number", numberWithCommas(data.block_number));
updateElementText("btc_price",
data.btc_price != null ? "$" + numberWithCommas(parseFloat(data.btc_price).toFixed(2)) : "N/A"
);
// Update BTC price with currency conversion and symbol
if (data.btc_price != null) {
const btcPriceValue = data.btc_price * exchangeRate;
const symbol = getCurrencySymbol(currency);
updateElementText("btc_price", formatCurrencyValue(btcPriceValue, currency));
} else {
updateElementText("btc_price", formatCurrencyValue(0, currency));
}
// Update last block earnings
if (data.last_block_earnings !== undefined) {
@ -2103,32 +2125,45 @@ function updateUI() {
}
updateElementText("difficulty", numberWithCommas(Math.round(data.difficulty)));
// Daily revenue
updateElementText("daily_revenue", "$" + numberWithCommas(data.daily_revenue.toFixed(2)));
// Daily revenue with currency conversion
if (data.daily_revenue != null) {
const dailyRevenue = data.daily_revenue * exchangeRate;
updateElementText("daily_revenue", formatCurrencyValue(dailyRevenue, currency));
} else {
updateElementText("daily_revenue", formatCurrencyValue(0, currency));
}
// Daily power cost
updateElementText("daily_power_cost", "$" + numberWithCommas(data.daily_power_cost.toFixed(2)));
// Daily power cost with currency conversion
if (data.daily_power_cost != null) {
const dailyPowerCost = data.daily_power_cost * exchangeRate;
updateElementText("daily_power_cost", formatCurrencyValue(dailyPowerCost, currency));
} else {
updateElementText("daily_power_cost", formatCurrencyValue(0, currency));
}
// Daily profit USD - Add red color if negative
const dailyProfitUSD = data.daily_profit_usd;
// Daily profit with currency conversion and color
if (data.daily_profit_usd != null) {
const dailyProfit = data.daily_profit_usd * exchangeRate;
const dailyProfitElement = document.getElementById("daily_profit_usd");
if (dailyProfitElement) {
dailyProfitElement.textContent = "$" + numberWithCommas(dailyProfitUSD.toFixed(2));
if (dailyProfitUSD < 0) {
dailyProfitElement.textContent = formatCurrencyValue(dailyProfit, currency);
if (dailyProfit < 0) {
// Use setAttribute to properly set the style with !important
dailyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important;");
} else {
// Clear the style attribute completely instead of setting it to empty
// Clear the style attribute completely
dailyProfitElement.removeAttribute("style");
}
}
}
// Monthly profit USD - Add red color if negative
const monthlyProfitUSD = data.monthly_profit_usd;
// Monthly profit with currency conversion and color
if (data.monthly_profit_usd != null) {
const monthlyProfit = data.monthly_profit_usd * exchangeRate;
const monthlyProfitElement = document.getElementById("monthly_profit_usd");
if (monthlyProfitElement) {
monthlyProfitElement.textContent = "$" + numberWithCommas(monthlyProfitUSD.toFixed(2));
if (monthlyProfitUSD < 0) {
monthlyProfitElement.textContent = formatCurrencyValue(monthlyProfit, currency);
if (monthlyProfit < 0) {
// Use setAttribute to properly set the style with !important
monthlyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important;");
} else {
@ -2136,6 +2171,7 @@ function updateUI() {
monthlyProfitElement.removeAttribute("style");
}
}
}
updateElementText("daily_mined_sats", numberWithCommas(data.daily_mined_sats) + " SATS");
updateElementText("monthly_mined_sats", numberWithCommas(data.monthly_mined_sats) + " SATS");
@ -2254,7 +2290,6 @@ function updateUI() {
}
}
// Update unread notifications badge in navigation
function updateNotificationBadge() {
$.ajax({
@ -2348,6 +2383,8 @@ function resetWalletAddress() {
.then(config => {
// Reset the wallet address to default
config.wallet = "yourwallethere";
// Add special flag to indicate config reset
config.config_reset = true;
// Save the updated configuration
return fetch('/api/config', {
@ -2510,6 +2547,44 @@ function checkAndShowHashrateNotice() {
}
}
// Currency conversion functions
function getCurrencySymbol(currency) {
const symbols = {
'USD': '$',
'EUR': '€',
'GBP': '£',
'JPY': '¥',
'CAD': 'CA$',
'AUD': 'A$',
'CNY': '¥',
'KRW': '₩',
'BRL': 'R$',
'CHF': 'Fr'
};
return symbols[currency] || '$';
}
function formatCurrencyValue(value, currency) {
if (value == null || isNaN(value)) return "N/A";
const symbol = getCurrencySymbol(currency);
// For JPY and KRW, show without decimal places
if (currency === 'JPY' || currency === 'KRW') {
return `${symbol}${numberWithCommas(Math.round(value))}`;
}
return `${symbol}${numberWithCommas(value.toFixed(2))}`;
}
// Update the BTC price and earnings card header with the selected currency
function updateCurrencyLabels(currency) {
const earningsHeader = document.querySelector('.card-header:contains("USD EARNINGS")');
if (earningsHeader) {
earningsHeader.textContent = `${currency} EARNINGS`;
}
}
$(document).ready(function () {
// Apply theme based on stored preference - moved to beginning for better initialization
try {

View File

@ -109,6 +109,27 @@ v.21
</label>
<input type="text" id="wallet-address" placeholder="bc1..." value="">
</div>
<div class="form-group">
<label for="currency">
Preferred Currency
<span class="tooltip">
?
<span class="tooltip-text">Currency for displaying BTC value and earnings</span>
</span>
</label>
<select id="currency" class="form-control">
<option value="USD">USD ($)</option>
<option value="EUR">EUR (€)</option>
<option value="GBP">GBP (£)</option>
<option value="JPY">JPY (¥)</option>
<option value="CAD">CAD ($)</option>
<option value="AUD">AUD ($)</option>
<option value="CNY">CNY (¥)</option>
<option value="KRW">KRW (₩)</option>
<option value="BRL">BRL (R$)</option>
<option value="CHF">CHF (Fr)</option>
</select>
</div>
<div class="form-group">
<label for="power-cost">
Power Cost ($/kWh)
@ -281,20 +302,22 @@ v.21
loadTimezoneFromConfig();
});
// Update saveConfig to include network fee
// Update saveConfig to include currency
function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const timezone = document.getElementById('timezone').value;
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
const currency = document.getElementById('currency').value;
const updatedConfig = {
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
power_cost: powerCost,
power_usage: powerUsage,
timezone: timezone,
network_fee: networkFee
network_fee: networkFee,
currency: currency
};
return fetch('/api/config', {
@ -345,7 +368,7 @@ v.21
power_usage: 0.0
};
// Update loadConfig function to include network fee
// Update loadConfig function to handle currency
function loadConfig() {
return new Promise((resolve, reject) => {
fetch('/api/config?nocache=' + new Date().getTime())
@ -364,6 +387,12 @@ v.21
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || "";
// Set currency dropdown value if available
if (currentConfig.currency) {
document.getElementById('currency').value = currentConfig.currency;
}
configLoaded = true;
resolve(currentConfig);
})
@ -374,13 +403,16 @@ v.21
wallet: "yourwallethere",
power_cost: 0.0,
power_usage: 0.0,
network_fee: 0.0
network_fee: 0.0,
currency: "USD"
};
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || "";
document.getElementById('currency').value = currentConfig.currency || "USD";
resolve(currentConfig);
});
});