diff --git a/App.py b/App.py index 3ce6c53..e1206c4 100644 --- a/App.py +++ b/App.py @@ -367,12 +367,25 @@ def update_metrics_job(force=False): metrics = dashboard_service.fetch_metrics() 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") diff --git a/data_service.py b/data_service.py index 9ff6f46..e5df4fc 100644 --- a/data_service.py +++ b/data_service.py @@ -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. diff --git a/docker-compose.yml b/docker-compose.yml index cb75a02..899ea98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/notification_service.py b/notification_service.py index fe73a7e..f4a0b06 100644 --- a/notification_service.py +++ b/notification_service.py @@ -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.""" @@ -312,6 +361,11 @@ class NotificationService: if not current_metrics: 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") @@ -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,8 +616,20 @@ 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 if current.get("est_time_to_payout"): est_time = current.get("est_time_to_payout") @@ -605,9 +682,27 @@ 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: return True time_since_last_notification = self._get_current_time() - self.last_payout_notification_time - return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS \ No newline at end of file + return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS diff --git a/static/js/BitcoinProgressBar.js b/static/js/BitcoinProgressBar.js index f105350..13d4b14 100644 --- a/static/js/BitcoinProgressBar.js +++ b/static/js/BitcoinProgressBar.js @@ -397,8 +397,12 @@ const BitcoinMinuteRefresh = (function () {
SYSTEM MONITOR v.3
-
-
+
+ - +
+
+ x +
@@ -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; diff --git a/static/js/main.js b/static/js/main.js index 6eae195..96f021d 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -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,37 +2125,51 @@ 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; - const dailyProfitElement = document.getElementById("daily_profit_usd"); - if (dailyProfitElement) { - dailyProfitElement.textContent = "$" + numberWithCommas(dailyProfitUSD.toFixed(2)); - if (dailyProfitUSD < 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 - dailyProfitElement.removeAttribute("style"); + // 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 = 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 + dailyProfitElement.removeAttribute("style"); + } } } - // Monthly profit USD - Add red color if negative - const monthlyProfitUSD = data.monthly_profit_usd; - const monthlyProfitElement = document.getElementById("monthly_profit_usd"); - if (monthlyProfitElement) { - monthlyProfitElement.textContent = "$" + numberWithCommas(monthlyProfitUSD.toFixed(2)); - if (monthlyProfitUSD < 0) { - // Use setAttribute to properly set the style with !important - monthlyProfitElement.setAttribute("style", "color: #ff5555 !important; font-weight: bold !important;"); - } else { - // Clear the style attribute completely - monthlyProfitElement.removeAttribute("style"); + // 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 = 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 { + // Clear the style attribute completely + monthlyProfitElement.removeAttribute("style"); + } } } @@ -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 { diff --git a/templates/boot.html b/templates/boot.html index 22d2471..4ac52ee 100644 --- a/templates/boot.html +++ b/templates/boot.html @@ -109,6 +109,27 @@ v.21
+
+ + +