mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 11:10:44 +02:00
Compare commits
25 Commits
fc5fddb243
...
09cf1cd17f
Author | SHA1 | Date | |
---|---|---|---|
09cf1cd17f | |||
![]() |
b92f8074da | ||
![]() |
923a4826a9 | ||
![]() |
9337dd49d3 | ||
![]() |
f7b2e550f6 | ||
![]() |
39f983563a | ||
![]() |
a50bd552f7 | ||
![]() |
2f1cbd3143 | ||
![]() |
f45ce8a1e8 | ||
![]() |
dec0045244 | ||
![]() |
744ed27279 | ||
![]() |
b4465e5a5c | ||
![]() |
9ef1260347 | ||
![]() |
57d8a9ab45 | ||
![]() |
b253e5aa7c | ||
![]() |
0ab96cb7c1 | ||
![]() |
06f5c646e2 | ||
![]() |
367ba3788f | ||
![]() |
574d5637bc | ||
![]() |
e9825c9006 | ||
![]() |
24c46f058b | ||
![]() |
e0c5f085cc | ||
![]() |
312031dcae | ||
![]() |
0d5ddda2f8 | ||
![]() |
b9d2c39b85 |
41
App.py
41
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")
|
||||
@ -1127,10 +1158,12 @@ def api_clear_notifications():
|
||||
"""API endpoint to clear notifications."""
|
||||
category = request.json.get('category')
|
||||
older_than_days = request.json.get('older_than_days')
|
||||
read_only = request.json.get('read_only', False) # Get the read_only parameter with default False
|
||||
|
||||
cleared_count = notification_service.clear_notifications(
|
||||
category=category,
|
||||
older_than_days=older_than_days
|
||||
older_than_days=older_than_days,
|
||||
read_only=read_only # Pass the parameter to the method
|
||||
)
|
||||
|
||||
return jsonify({
|
||||
|
18
README.md
18
README.md
@ -8,9 +8,9 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
||||
## Gallery:
|
||||
|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||

|
||||
|
||||
@ -28,6 +28,13 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
||||
- **Payout Monitoring**: View unpaid balance and estimated time to next payout
|
||||
- **Pool Fee Analysis**: Monitor pool fee percentages with visual indicator when optimal rates (0.9-1.3%) are detected
|
||||
|
||||
### Multi-Currency Support
|
||||
- **Flexible Currency Configuration: Set your preferred fiat currency for displaying Bitcoin value and earnings
|
||||
- **Wide Currency Selection: Choose from USD, EUR, GBP, JPY, CAD, AUD, CNY, KRW, BRL, CHF and more
|
||||
- **Real-Time Exchange Rates: Automatically fetches up-to-date exchange rates from public APIs
|
||||
- **Persistent Configuration: Currency preferences saved and restored between sessions
|
||||
- **Adaptive Notifications: Financial notifications display in your selected currency
|
||||
|
||||
### Worker Management
|
||||
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
|
||||
- **Status Monitoring**: Real-time status indicators for online and offline devices
|
||||
@ -114,6 +121,7 @@ You can modify the following environment variables in the `docker-compose.yml` f
|
||||
- `POWER_USAGE`: Power usage in watts.
|
||||
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
|
||||
- `TIMEZONE`: Local timezone for displaying time information.
|
||||
- `CURRENCY`: Preferred fiat currency for earnings display.
|
||||
|
||||
Redis data is stored in a persistent volume (`redis_data`), and application logs are saved in the `./logs` directory.
|
||||
|
||||
@ -174,6 +182,9 @@ Built with a modern stack for reliability and performance:
|
||||
- `/api/available_timezones`: Returns a list of supported timezones.
|
||||
- `/api/config`: Fetches or updates the mining configuration.
|
||||
- `/api/health`: Returns the health status of the application.
|
||||
- `/api/notifications`: Manages notifications for the user.
|
||||
- `/api/workers`: Manages worker data and status.
|
||||
- `/api/exchange_rates`: Fetches real-time exchange rates for supported currencies.
|
||||
|
||||
## Project Structure
|
||||
|
||||
@ -244,6 +255,9 @@ For optimal performance:
|
||||
4. Access the health endpoint at `/api/health` for diagnostics
|
||||
5. For stale data issues, use the Force Refresh function
|
||||
6. Use hotkey Shift+R to clear chart and Redis data (as needed, not required)
|
||||
7. Check the currency settings if financial calculations appear incorrect
|
||||
8. Verify timezone settings for accurate time displays
|
||||
9. Alt + W on Dashboard resets wallet configuration and redirects to Boot sequence
|
||||
|
||||
## License
|
||||
|
||||
|
@ -7,7 +7,7 @@ import json
|
||||
import logging
|
||||
|
||||
# Default configuration file path
|
||||
CONFIG_FILE = "config.json"
|
||||
CONFIG_FILE = "data/config.json"
|
||||
|
||||
def load_config():
|
||||
"""
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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."""
|
||||
|
||||
@ -263,35 +312,37 @@ class NotificationService:
|
||||
|
||||
return deleted > 0
|
||||
|
||||
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None) -> int:
|
||||
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None, read_only: bool = False) -> int:
|
||||
"""
|
||||
Clear notifications with optimized filtering.
|
||||
|
||||
|
||||
Args:
|
||||
category (str, optional): Only clear specific category
|
||||
older_than_days (int, optional): Only clear notifications older than this
|
||||
|
||||
read_only (bool, optional): Only clear notifications that have been read
|
||||
|
||||
Returns:
|
||||
int: Number of notifications cleared
|
||||
"""
|
||||
original_count = len(self.notifications)
|
||||
|
||||
|
||||
cutoff_date = None
|
||||
if older_than_days:
|
||||
cutoff_date = self._get_current_time() - timedelta(days=older_than_days)
|
||||
|
||||
# Apply filters in a single pass
|
||||
|
||||
# Apply filters to KEEP notifications that should NOT be cleared
|
||||
self.notifications = [
|
||||
n for n in self.notifications
|
||||
if (not category or n.get("category") != category) and
|
||||
(not cutoff_date or self._parse_timestamp(n.get("timestamp", self._get_current_time().isoformat())) >= cutoff_date)
|
||||
if (category and n.get("category") != category) or # Keep if we're filtering by category and this isn't that category
|
||||
(cutoff_date and self._parse_timestamp(n.get("timestamp", self._get_current_time().isoformat())) >= cutoff_date) or # Keep if newer than cutoff
|
||||
(read_only and not n.get("read", False)) # Keep if we're only clearing read notifications and this is unread
|
||||
]
|
||||
|
||||
|
||||
cleared_count = original_count - len(self.notifications)
|
||||
if cleared_count > 0:
|
||||
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
|
||||
self._save_notifications()
|
||||
|
||||
|
||||
return cleared_count
|
||||
|
||||
def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
@ -312,6 +363,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 +440,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 +467,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:
|
||||
@ -447,38 +514,71 @@ class NotificationService:
|
||||
return 0.0
|
||||
|
||||
def _check_hashrate_change(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||
"""Check for significant hashrate changes using 10-minute average."""
|
||||
"""Check for significant hashrate changes using appropriate time window based on mode."""
|
||||
try:
|
||||
# Get 10min hashrate values
|
||||
current_10min = current.get("hashrate_10min", 0)
|
||||
previous_10min = previous.get("hashrate_10min", 0)
|
||||
|
||||
# Log what we're comparing
|
||||
logging.debug(f"[NotificationService] Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}")
|
||||
|
||||
# Skip if values are missing
|
||||
if not current_10min or not previous_10min:
|
||||
logging.debug("[NotificationService] Skipping hashrate check - missing values")
|
||||
return None
|
||||
|
||||
# Parse values consistently
|
||||
current_value = self._parse_numeric_value(current_10min)
|
||||
previous_value = self._parse_numeric_value(previous_10min)
|
||||
# Check if we're in low hashrate mode
|
||||
# A simple threshold approach: if hashrate_3hr is below 1 TH/s, consider it low hashrate mode
|
||||
is_low_hashrate_mode = False
|
||||
|
||||
if "hashrate_3hr" in current:
|
||||
current_3hr = self._parse_numeric_value(current.get("hashrate_3hr", 0))
|
||||
current_3hr_unit = current.get("hashrate_3hr_unit", "TH/s").lower()
|
||||
|
||||
logging.debug(f"[NotificationService] Converted 10min hashrates - current: {current_value}, previous: {previous_value}")
|
||||
|
||||
# Normalize to TH/s for comparison
|
||||
if "ph/s" in current_3hr_unit:
|
||||
current_3hr *= 1000
|
||||
elif "gh/s" in current_3hr_unit:
|
||||
current_3hr /= 1000
|
||||
elif "mh/s" in current_3hr_unit:
|
||||
current_3hr /= 1000000
|
||||
|
||||
# If hashrate is less than 3 TH/s, consider it low hashrate mode
|
||||
is_low_hashrate_mode = current_3hr < 3.0
|
||||
|
||||
logging.debug(f"[NotificationService] Low hashrate mode: {is_low_hashrate_mode}")
|
||||
|
||||
# Choose the appropriate hashrate metric based on mode
|
||||
if is_low_hashrate_mode:
|
||||
# In low hashrate mode, use 3hr averages for more stability
|
||||
current_hashrate_key = "hashrate_3hr"
|
||||
previous_hashrate_key = "hashrate_3hr"
|
||||
timeframe = "3hr"
|
||||
else:
|
||||
# In normal mode, use 10min averages for faster response
|
||||
current_hashrate_key = "hashrate_10min"
|
||||
previous_hashrate_key = "hashrate_10min"
|
||||
timeframe = "10min"
|
||||
|
||||
# Get hashrate values
|
||||
current_hashrate = current.get(current_hashrate_key, 0)
|
||||
previous_hashrate = previous.get(previous_hashrate_key, 0)
|
||||
|
||||
# Log what we're comparing
|
||||
logging.debug(f"[NotificationService] Comparing {timeframe} hashrates - current: {current_hashrate}, previous: {previous_hashrate}")
|
||||
|
||||
# Skip if values are missing
|
||||
if not current_hashrate or not previous_hashrate:
|
||||
logging.debug(f"[NotificationService] Skipping hashrate check - missing {timeframe} values")
|
||||
return None
|
||||
|
||||
# Parse values consistently
|
||||
current_value = self._parse_numeric_value(current_hashrate)
|
||||
previous_value = self._parse_numeric_value(previous_hashrate)
|
||||
|
||||
logging.debug(f"[NotificationService] Converted {timeframe} hashrates - current: {current_value}, previous: {previous_value}")
|
||||
|
||||
# Skip if previous was zero (prevents division by zero)
|
||||
if previous_value == 0:
|
||||
logging.debug("[NotificationService] Skipping hashrate check - previous was zero")
|
||||
logging.debug(f"[NotificationService] Skipping hashrate check - previous {timeframe} was zero")
|
||||
return None
|
||||
|
||||
|
||||
# Calculate percentage change
|
||||
percent_change = ((current_value - previous_value) / previous_value) * 100
|
||||
logging.debug(f"[NotificationService] 10min hashrate change: {percent_change:.1f}%")
|
||||
|
||||
logging.debug(f"[NotificationService] {timeframe} hashrate change: {percent_change:.1f}%")
|
||||
|
||||
# Significant decrease
|
||||
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
|
||||
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease"
|
||||
message = f"Significant {timeframe} hashrate drop detected: {abs(percent_change):.1f}% decrease"
|
||||
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
|
||||
return self.add_notification(
|
||||
message,
|
||||
@ -488,13 +588,14 @@ class NotificationService:
|
||||
"previous": previous_value,
|
||||
"current": current_value,
|
||||
"change": percent_change,
|
||||
"timeframe": "10min"
|
||||
"timeframe": timeframe,
|
||||
"is_low_hashrate_mode": is_low_hashrate_mode
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# Significant increase
|
||||
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
|
||||
message = f"10min hashrate increase detected: {percent_change:.1f}% increase"
|
||||
message = f"{timeframe} hashrate increase detected: {percent_change:.1f}% increase"
|
||||
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
|
||||
return self.add_notification(
|
||||
message,
|
||||
@ -504,10 +605,11 @@ class NotificationService:
|
||||
"previous": previous_value,
|
||||
"current": current_value,
|
||||
"change": percent_change,
|
||||
"timeframe": "10min"
|
||||
"timeframe": timeframe,
|
||||
"is_low_hashrate_mode": is_low_hashrate_mode
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
return None
|
||||
except Exception as e:
|
||||
logging.error(f"[NotificationService] Error checking hashrate change: {e}")
|
||||
@ -516,8 +618,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")
|
||||
@ -570,9 +684,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
|
||||
return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS
|
||||
|
@ -125,87 +125,63 @@ class StateManager:
|
||||
if not self.redis_client:
|
||||
logging.info("Redis not available, skipping state save.")
|
||||
return
|
||||
|
||||
# Check if we've saved recently to avoid too frequent saves
|
||||
# Only save at most once every 5 minutes
|
||||
|
||||
current_time = time.time()
|
||||
if hasattr(self, 'last_save_time') and current_time - self.last_save_time < 300: # 300 seconds = 5 minutes
|
||||
if hasattr(self, 'last_save_time') and current_time - self.last_save_time < 300: # 5 minutes
|
||||
logging.debug("Skipping Redis save - last save was less than 5 minutes ago")
|
||||
return
|
||||
|
||||
# Update the last save time
|
||||
|
||||
self.last_save_time = current_time
|
||||
|
||||
# Prune data first to reduce volume
|
||||
self.prune_old_data()
|
||||
|
||||
# Create compact versions of the data structures for Redis storage
|
||||
|
||||
try:
|
||||
# 1. Create compact arrow_history with minimal data
|
||||
# Compact arrow_history with unit preservation
|
||||
compact_arrow_history = {}
|
||||
for key, values in arrow_history.items():
|
||||
if isinstance(values, list) and values:
|
||||
# Only store recent history (last 2 hours)
|
||||
recent_values = values[-180:] if len(values) > 180 else values
|
||||
# Use shorter field names and preserve arrow directions
|
||||
compact_arrow_history[key] = [
|
||||
{"t": entry["time"], "v": entry["value"], "a": entry["arrow"]}
|
||||
{"t": entry["time"], "v": entry["value"], "a": entry["arrow"], "u": entry.get("unit", "th/s")}
|
||||
for entry in recent_values
|
||||
]
|
||||
|
||||
# 2. Only keep essential hashrate_history
|
||||
|
||||
# Compact hashrate_history
|
||||
compact_hashrate_history = hashrate_history[-60:] if len(hashrate_history) > 60 else hashrate_history
|
||||
|
||||
# 3. Only keep recent metrics_log entries (last 30 minutes)
|
||||
# This is typically the largest data structure
|
||||
|
||||
# Compact metrics_log with unit preservation
|
||||
compact_metrics_log = []
|
||||
if metrics_log:
|
||||
# Keep only last 30 entries (30 minutes assuming 1-minute updates)
|
||||
recent_logs = metrics_log[-30:]
|
||||
|
||||
recent_logs = metrics_log[-30:]
|
||||
for entry in recent_logs:
|
||||
# Only keep necessary fields from each metrics entry
|
||||
if "metrics" in entry and "timestamp" in entry:
|
||||
metrics_copy = {}
|
||||
original_metrics = entry["metrics"]
|
||||
|
||||
# Only copy the most important metrics for historical tracking
|
||||
essential_keys = [
|
||||
"hashrate_60sec", "hashrate_24hr", "btc_price",
|
||||
"workers_hashing", "unpaid_earnings", "difficulty",
|
||||
"network_hashrate", "daily_profit_usd"
|
||||
]
|
||||
|
||||
for key in essential_keys:
|
||||
if key in original_metrics:
|
||||
metrics_copy[key] = original_metrics[key]
|
||||
|
||||
# Skip arrow_history within metrics as we already stored it separately
|
||||
compact_metrics_log.append({
|
||||
"ts": entry["timestamp"],
|
||||
"m": metrics_copy
|
||||
})
|
||||
|
||||
# Create the final state object
|
||||
metrics_copy = {}
|
||||
original_metrics = entry["metrics"]
|
||||
essential_keys = [
|
||||
"hashrate_60sec", "hashrate_24hr", "btc_price",
|
||||
"workers_hashing", "unpaid_earnings", "difficulty",
|
||||
"network_hashrate", "daily_profit_usd"
|
||||
]
|
||||
for key in essential_keys:
|
||||
if key in original_metrics:
|
||||
metrics_copy[key] = {
|
||||
"value": original_metrics[key],
|
||||
"unit": original_metrics.get(f"{key}_unit", "th/s")
|
||||
}
|
||||
compact_metrics_log.append({
|
||||
"ts": entry["timestamp"],
|
||||
"m": metrics_copy
|
||||
})
|
||||
|
||||
state = {
|
||||
"arrow_history": compact_arrow_history,
|
||||
"hashrate_history": compact_hashrate_history,
|
||||
"metrics_log": compact_metrics_log
|
||||
}
|
||||
|
||||
# Convert to JSON once to reuse and measure size
|
||||
|
||||
state_json = json.dumps(state)
|
||||
data_size_kb = len(state_json) / 1024
|
||||
|
||||
# Log data size for monitoring
|
||||
logging.info(f"Saving graph state to Redis: {data_size_kb:.2f} KB (optimized format)")
|
||||
|
||||
# Only save if data size is reasonable (adjust threshold as needed)
|
||||
if data_size_kb > 2000: # 2MB warning threshold (reduced from 5MB)
|
||||
logging.warning(f"Redis save data size is still large: {data_size_kb:.2f} KB")
|
||||
|
||||
# Store version info to handle future format changes
|
||||
self.redis_client.set(f"{self.STATE_KEY}_version", "2.0")
|
||||
|
||||
self.redis_client.set(f"{self.STATE_KEY}_version", "2.0")
|
||||
self.redis_client.set(self.STATE_KEY, state_json)
|
||||
logging.info(f"Successfully saved graph state to Redis ({data_size_kb:.2f} KB)")
|
||||
except Exception as e:
|
||||
|
@ -1,14 +1,10 @@
|
||||
.footer {
|
||||
margin-top: 30px;
|
||||
padding: 10px 0;
|
||||
color: grey;
|
||||
font-size: 0.9rem;
|
||||
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
||||
}
|
||||
|
||||
</style >
|
||||
<!-- Preload theme to prevent flicker -->
|
||||
<style id="theme-preload" >
|
||||
/* Theme-aware loading state */
|
||||
html.bitcoin-theme {
|
||||
background-color: #111111;
|
||||
|
@ -184,7 +184,7 @@
|
||||
}
|
||||
/* Add bottom padding to accommodate minimized system monitor */
|
||||
.container-fluid {
|
||||
padding-bottom: 60px !important; /* Enough space for minimized monitor */
|
||||
padding-bottom: 100px !important; /* Enough space for minimized monitor */
|
||||
}
|
||||
|
||||
/* Add these styles to dashboard.css */
|
||||
@ -218,7 +218,7 @@
|
||||
}
|
||||
|
||||
.datum-label {
|
||||
color: #ffffff; /* White color */
|
||||
color: cyan; /* cyan color */
|
||||
font-size: 0.95em;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
@ -245,3 +245,10 @@
|
||||
.unlucky {
|
||||
color: #ff5555 !important;
|
||||
}
|
||||
|
||||
/* Ensure the pool fee in SATS is always red regardless of theme */
|
||||
#pool_fees_sats {
|
||||
color: #ff5555 !important;
|
||||
font-weight: bold !important;
|
||||
margin-left: 6px;
|
||||
}
|
@ -283,12 +283,13 @@
|
||||
.notification-actions {
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
width: 100%; /* Full width on small screens */
|
||||
padding: 8px 12px;
|
||||
font-size: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.notification-controls {
|
||||
@ -297,16 +298,27 @@
|
||||
}
|
||||
|
||||
.filter-buttons {
|
||||
overflow-x: auto;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 8px;
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 5px;
|
||||
white-space: nowrap;
|
||||
margin-bottom: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
font-size: 0.9rem;
|
||||
padding: 8px 5px;
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 38px;
|
||||
}
|
||||
|
||||
.notification-actions {
|
||||
justify-content: flex-end;
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
@ -324,4 +336,11 @@
|
||||
.notification-actions {
|
||||
flex: 0 0 60px;
|
||||
}
|
||||
|
||||
/* For very small screens, reduce to 2 columns */
|
||||
@media (max-width: 375px) {
|
||||
.filter-buttons {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,6 +91,22 @@
|
||||
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* Add RGB values for primary colors */
|
||||
html.deepsea-theme {
|
||||
--primary-color: #0088cc;
|
||||
--primary-color-rgb: 0, 136, 204;
|
||||
}
|
||||
|
||||
html.bitcoin-theme {
|
||||
--primary-color: #f2a900;
|
||||
--primary-color-rgb: 242, 169, 0;
|
||||
}
|
||||
|
||||
/* Theme-specific graph container shadow using CSS variable */
|
||||
#graphContainer {
|
||||
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.2) !important;
|
||||
}
|
||||
|
||||
/* Bitcoin theme specific styling (orange) */
|
||||
body:not(.deepsea-theme) #themeToggle,
|
||||
body:not(.deepsea-theme) .theme-toggle-btn {
|
||||
@ -142,15 +158,6 @@ body.deepsea-theme .theme-toggle-btn:focus {
|
||||
box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3);
|
||||
}
|
||||
|
||||
/* Add to your common.css or theme-toggle.css */
|
||||
html.deepsea-theme {
|
||||
--primary-color: #0088cc;
|
||||
}
|
||||
|
||||
html.bitcoin-theme {
|
||||
--primary-color: #f2a900;
|
||||
}
|
||||
|
||||
/* Add these theme-specific loading styles */
|
||||
#theme-loader {
|
||||
position: fixed;
|
||||
|
@ -8,8 +8,10 @@
|
||||
const BitcoinMinuteRefresh = (function () {
|
||||
// Constants
|
||||
const STORAGE_KEY = 'bitcoin_last_refresh_time';
|
||||
const BITCOIN_COLOR = '#f7931a';
|
||||
const DEEPSEA_COLOR = '#0088cc';
|
||||
// Default fallback colors if CSS vars aren't available
|
||||
const FALLBACK_BITCOIN_COLOR = '#f2a900';
|
||||
const FALLBACK_DEEPSEA_COLOR = '#0088cc';
|
||||
|
||||
const DOM_IDS = {
|
||||
TERMINAL: 'bitcoin-terminal',
|
||||
STYLES: 'bitcoin-terminal-styles',
|
||||
@ -45,9 +47,32 @@ const BitcoinMinuteRefresh = (function () {
|
||||
let uptimeInterval = null;
|
||||
let isInitialized = false;
|
||||
let refreshCallback = null;
|
||||
let currentThemeColor = BITCOIN_COLOR; // Default Bitcoin color
|
||||
let currentThemeColor = '';
|
||||
let currentThemeRGB = '';
|
||||
let dragListenersAdded = false;
|
||||
|
||||
/**
|
||||
* Get theme colors from CSS variables
|
||||
*/
|
||||
function getThemeColors() {
|
||||
// Try to get CSS variables from document root
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
let primaryColor = rootStyles.getPropertyValue('--primary-color').trim();
|
||||
let primaryColorRGB = rootStyles.getPropertyValue('--primary-color-rgb').trim();
|
||||
|
||||
// If CSS vars not available, use theme toggle state
|
||||
if (!primaryColor) {
|
||||
const isDeepSea = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
|
||||
primaryColor = isDeepSea ? FALLBACK_DEEPSEA_COLOR : FALLBACK_BITCOIN_COLOR;
|
||||
primaryColorRGB = isDeepSea ? '0, 136, 204' : '242, 169, 0';
|
||||
}
|
||||
|
||||
return {
|
||||
color: primaryColor,
|
||||
rgb: primaryColorRGB
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Logging helper function
|
||||
* @param {string} message - Message to log
|
||||
@ -79,24 +104,22 @@ const BitcoinMinuteRefresh = (function () {
|
||||
* Apply the current theme color
|
||||
*/
|
||||
function applyThemeColor() {
|
||||
// Check if theme toggle is set to DeepSea
|
||||
const isDeepSeaTheme = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
|
||||
currentThemeColor = isDeepSeaTheme ? DEEPSEA_COLOR : BITCOIN_COLOR;
|
||||
// Get current theme colors
|
||||
const theme = getThemeColors();
|
||||
currentThemeColor = theme.color;
|
||||
currentThemeRGB = theme.rgb;
|
||||
|
||||
// Don't try to update DOM elements if they don't exist yet
|
||||
if (!terminalElement) return;
|
||||
|
||||
// Define color values based on theme
|
||||
const rgbValues = isDeepSeaTheme ? '0, 136, 204' : '247, 147, 26';
|
||||
|
||||
// Create theme config
|
||||
const themeConfig = {
|
||||
color: currentThemeColor,
|
||||
borderColor: currentThemeColor,
|
||||
boxShadow: `0 0 5px rgba(${rgbValues}, 0.3)`,
|
||||
textShadow: `0 0 5px rgba(${rgbValues}, 0.8)`,
|
||||
borderColorRGBA: `rgba(${rgbValues}, 0.5)`,
|
||||
textShadowStrong: `0 0 8px rgba(${rgbValues}, 0.8)`
|
||||
boxShadow: `0 0 5px rgba(${currentThemeRGB}, 0.3)`,
|
||||
textShadow: `0 0 5px rgba(${currentThemeRGB}, 0.8)`,
|
||||
borderColorRGBA: `rgba(${currentThemeRGB}, 0.5)`,
|
||||
textShadowStrong: `0 0 8px rgba(${currentThemeRGB}, 0.8)`
|
||||
};
|
||||
|
||||
// Apply styles to terminal
|
||||
@ -144,6 +167,13 @@ const BitcoinMinuteRefresh = (function () {
|
||||
if (miniLabel) {
|
||||
miniLabel.style.color = themeConfig.color;
|
||||
}
|
||||
|
||||
// Update show button if it exists
|
||||
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON);
|
||||
if (showButton) {
|
||||
showButton.style.backgroundColor = themeConfig.color;
|
||||
showButton.style.boxShadow = `0 0 10px rgba(${currentThemeRGB}, 0.5)`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -156,6 +186,25 @@ const BitcoinMinuteRefresh = (function () {
|
||||
applyThemeColor();
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for custom theme change events
|
||||
document.addEventListener('themeChanged', function () {
|
||||
applyThemeColor();
|
||||
});
|
||||
|
||||
// Watch for class changes on HTML element that might indicate theme changes
|
||||
const observer = new MutationObserver(function (mutations) {
|
||||
mutations.forEach(function (mutation) {
|
||||
if (mutation.attributeName === 'class') {
|
||||
applyThemeColor();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.documentElement, {
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -348,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">
|
||||
@ -413,13 +466,12 @@ const BitcoinMinuteRefresh = (function () {
|
||||
* Add CSS styles for the terminal
|
||||
*/
|
||||
function addStyles() {
|
||||
// Use the currentThemeColor variable instead of hardcoded colors
|
||||
// Get current theme colors for initial styling
|
||||
const theme = getThemeColors();
|
||||
|
||||
const styleElement = document.createElement('style');
|
||||
styleElement.id = DOM_IDS.STYLES;
|
||||
|
||||
// Generate RGB values for dynamic colors
|
||||
const rgbValues = currentThemeColor === DEEPSEA_COLOR ? '0, 136, 204' : '247, 147, 26';
|
||||
|
||||
styleElement.textContent = `
|
||||
/* Terminal Container */
|
||||
.bitcoin-terminal {
|
||||
@ -428,14 +480,14 @@ const BitcoinMinuteRefresh = (function () {
|
||||
right: 20px;
|
||||
width: 230px;
|
||||
background-color: #000000;
|
||||
border: 1px solid ${currentThemeColor};
|
||||
color: ${currentThemeColor};
|
||||
border: 1px solid var(--primary-color, ${theme.color});
|
||||
color: var(--primary-color, ${theme.color});
|
||||
font-family: 'VT323', monospace;
|
||||
z-index: 9999;
|
||||
overflow: hidden;
|
||||
padding: 8px;
|
||||
transition: all 0.3s ease;
|
||||
box-shadow: 0 0 5px rgba(${rgbValues}, 0.3);
|
||||
box-shadow: 0 0 5px rgba(var(--primary-color-rgb, ${theme.rgb}), 0.3);
|
||||
}
|
||||
|
||||
/* Terminal Header */
|
||||
@ -443,20 +495,19 @@ const BitcoinMinuteRefresh = (function () {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid ${currentThemeColor};
|
||||
border-bottom: 1px solid var(--primary-color, ${theme.color});
|
||||
padding-bottom: 5px;
|
||||
margin-bottom: 8px;
|
||||
cursor: grab; /* Add grab cursor on hover */
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
/* Apply grabbing cursor during active drag */
|
||||
.terminal-header:active,
|
||||
.bitcoin-terminal.dragging .terminal-header {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.terminal-title {
|
||||
color: ${currentThemeColor};
|
||||
color: var(--primary-color, ${theme.color});
|
||||
font-weight: bold;
|
||||
font-size: 1.1rem;
|
||||
animation: terminal-flicker 4s infinite;
|
||||
@ -470,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;
|
||||
@ -524,14 +598,14 @@ const BitcoinMinuteRefresh = (function () {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
|
||||
/* Uptime Display */
|
||||
.uptime-timer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 5px;
|
||||
background-color: #111;
|
||||
border: 1px solid rgba(${rgbValues}, 0.5);
|
||||
border: 1px solid rgba(var(--primary-color-rgb, ${theme.rgb}), 0.5);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
@ -586,7 +660,7 @@ const BitcoinMinuteRefresh = (function () {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
background-color: ${currentThemeColor};
|
||||
background-color: var(--primary-color, ${theme.color});
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 8px 12px;
|
||||
@ -594,7 +668,7 @@ const BitcoinMinuteRefresh = (function () {
|
||||
cursor: pointer;
|
||||
z-index: 9999;
|
||||
display: none;
|
||||
box-shadow: 0 0 10px rgba(${rgbValues}, 0.5);
|
||||
box-shadow: 0 0 10px rgba(var(--primary-color-rgb, ${theme.rgb}), 0.5);
|
||||
}
|
||||
|
||||
/* CRT scanline effect */
|
||||
@ -660,7 +734,7 @@ const BitcoinMinuteRefresh = (function () {
|
||||
letter-spacing: 1px;
|
||||
opacity: 0.7;
|
||||
margin-left: 45px;
|
||||
color: ${currentThemeColor};
|
||||
color: var(--primary-color, ${theme.color});
|
||||
}
|
||||
|
||||
#${DOM_IDS.MINIMIZED_UPTIME} {
|
||||
@ -855,9 +929,6 @@ const BitcoinMinuteRefresh = (function () {
|
||||
// Store the refresh callback
|
||||
refreshCallback = refreshFunc;
|
||||
|
||||
// Get current theme status
|
||||
applyThemeColor();
|
||||
|
||||
// Create the terminal element if it doesn't exist
|
||||
if (!document.getElementById(DOM_IDS.TERMINAL)) {
|
||||
createTerminalElement();
|
||||
@ -865,11 +936,11 @@ const BitcoinMinuteRefresh = (function () {
|
||||
// Get references to existing elements
|
||||
terminalElement = document.getElementById(DOM_IDS.TERMINAL);
|
||||
uptimeElement = document.getElementById('uptime-timer');
|
||||
|
||||
// Apply theme to existing element
|
||||
applyThemeColor();
|
||||
}
|
||||
|
||||
// Apply theme colors
|
||||
applyThemeColor();
|
||||
|
||||
// Set up listener for theme changes
|
||||
setupThemeChangeListener();
|
||||
|
||||
@ -923,6 +994,9 @@ const BitcoinMinuteRefresh = (function () {
|
||||
} catch (e) {
|
||||
log("Error reading updated server time: " + e.message, 'error');
|
||||
}
|
||||
} else if (event.key === STORAGE_KEYS.THEME) {
|
||||
// Update theme when theme preference changes
|
||||
applyThemeColor();
|
||||
}
|
||||
}
|
||||
|
||||
@ -933,6 +1007,9 @@ const BitcoinMinuteRefresh = (function () {
|
||||
if (!document.hidden) {
|
||||
log("Page became visible, updating");
|
||||
|
||||
// Apply current theme when page becomes visible
|
||||
applyThemeColor();
|
||||
|
||||
// Update immediately when page becomes visible
|
||||
updateClock();
|
||||
updateUptime();
|
||||
@ -989,6 +1066,11 @@ const BitcoinMinuteRefresh = (function () {
|
||||
showButton.textContent = 'Show Monitor';
|
||||
showButton.onclick = showTerminal;
|
||||
document.body.appendChild(showButton);
|
||||
|
||||
// Apply current theme to the button
|
||||
const theme = getThemeColors();
|
||||
showButton.style.backgroundColor = theme.color;
|
||||
showButton.style.boxShadow = `0 0 10px rgba(${theme.rgb}, 0.5)`;
|
||||
}
|
||||
|
||||
document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block';
|
||||
|
1157
static/js/main.js
1157
static/js/main.js
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}BTC-OS MINING DASHBOARD {% endblock %}</title>
|
||||
<title>{% block title %}BTC-OS DASHBOARD {% endblock %}</title>
|
||||
|
||||
<!-- Common fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
|
||||
@ -96,7 +96,7 @@
|
||||
|
||||
<h1 class="text-center">
|
||||
<a href="/" style="text-decoration:none; color:inherit;">
|
||||
{% block header %}BTC-OS MINING DASHBOARD{% endblock %}
|
||||
{% block header %}BTC-OS DASHBOARD{% endblock %}
|
||||
</a>
|
||||
</h1>
|
||||
|
||||
@ -135,6 +135,7 @@
|
||||
<!-- Footer -->
|
||||
<footer class="footer text-center">
|
||||
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
|
||||
<p>v0.9.2 - Public Beta</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
@ -68,9 +68,9 @@
|
||||
});
|
||||
</script>
|
||||
<!-- Theme toggle button (new) -->
|
||||
<button id="themeToggle" class="theme-toggle-btn">
|
||||
<!--<button id="themeToggle" class="theme-toggle-btn">
|
||||
<span>Toggle Theme</span>
|
||||
</button>
|
||||
</button>-->
|
||||
<button id="skip-button">SKIP</button>
|
||||
<div id="debug-info"></div>
|
||||
<div id="loading-message">Loading mining data...</div>
|
||||
@ -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)
|
||||
@ -131,7 +152,7 @@ v.21
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="network-fee">
|
||||
Network Fee (%)
|
||||
Firmware/Other Fees (%)
|
||||
<span class="tooltip">
|
||||
?
|
||||
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -55,43 +55,6 @@
|
||||
<div class="card" id="payoutMiscCard">
|
||||
<div class="card-header">Payout Info</div>
|
||||
<div class="card-body">
|
||||
<p>
|
||||
<strong>Unpaid Earnings:</strong>
|
||||
<span id="unpaid_earnings" class="metric-value green">
|
||||
{% if metrics and metrics.unpaid_earnings %}
|
||||
{{ metrics.unpaid_earnings }} BTC
|
||||
{% else %}
|
||||
0 BTC
|
||||
{% endif %}
|
||||
</span>
|
||||
<span id="indicator_unpaid_earnings"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Last Block:</strong>
|
||||
<span id="last_block_height" class="metric-value white">
|
||||
{{ metrics.last_block_height|commafy if metrics and metrics.last_block_height else "N/A" }}
|
||||
</span>
|
||||
—
|
||||
<span id="last_block_time" class="metric-value blue">
|
||||
{{ metrics.last_block_time if metrics and metrics.last_block_time else "N/A" }}
|
||||
</span>
|
||||
—
|
||||
<span class="green">
|
||||
{% if metrics and metrics.last_block_earnings %}
|
||||
+{{ metrics.last_block_earnings|int|commafy }} SATS
|
||||
{% else %}
|
||||
+0 SATS
|
||||
{% endif %}
|
||||
</span>
|
||||
<span id="indicator_last_block"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Est. Time to Payout:</strong>
|
||||
<span id="est_time_to_payout" class="metric-value yellow">
|
||||
{{ metrics.est_time_to_payout if metrics and metrics.est_time_to_payout else "N/A" }}
|
||||
</span>
|
||||
<span id="indicator_est_time_to_payout"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Pool Fees:</strong>
|
||||
<span id="pool_fees_percentage" class="metric-value">
|
||||
@ -106,6 +69,43 @@
|
||||
</span>
|
||||
<span id="indicator_pool_fees_percentage"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Last Block:</strong>
|
||||
<span id="last_block_height" class="metric-value white">
|
||||
{{ metrics.last_block_height|commafy if metrics and metrics.last_block_height else "N/A" }}
|
||||
</span>
|
||||
—
|
||||
<span id="last_block_time" class="metric-value blue">
|
||||
{{ metrics.last_block_time if metrics and metrics.last_block_time else "N/A" }}
|
||||
</span>
|
||||
—
|
||||
<span id="last_block_earnings" class="metric-value green">
|
||||
{% if metrics and metrics.last_block_earnings %}
|
||||
+{{ metrics.last_block_earnings|int|commafy }} SATS
|
||||
{% else %}
|
||||
+0 SATS
|
||||
{% endif %}
|
||||
</span>
|
||||
<span id="indicator_last_block"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Unpaid Earnings:</strong>
|
||||
<span id="unpaid_earnings" class="metric-value green">
|
||||
{% if metrics and metrics.unpaid_earnings %}
|
||||
{{ metrics.unpaid_earnings }} BTC
|
||||
{% else %}
|
||||
0 BTC
|
||||
{% endif %}
|
||||
</span>
|
||||
<span id="indicator_unpaid_earnings"></span>
|
||||
</p>
|
||||
<p>
|
||||
<strong>Est. Time to Payout:</strong>
|
||||
<span id="est_time_to_payout" class="metric-value yellow">
|
||||
{{ metrics.est_time_to_payout if metrics and metrics.est_time_to_payout else "N/A" }}
|
||||
</span>
|
||||
<span id="indicator_est_time_to_payout"></span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user