mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00
Compare commits
25 Commits
16638ca05d
...
fe1ce30bfe
Author | SHA1 | Date | |
---|---|---|---|
fe1ce30bfe | |||
![]() |
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()
|
metrics = dashboard_service.fetch_metrics()
|
||||||
if metrics:
|
if metrics:
|
||||||
logging.info("Fetched metrics successfully")
|
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
|
# First check for notifications by comparing new metrics with old cached metrics
|
||||||
notification_service.check_and_generate_notifications(metrics, cached_metrics)
|
notification_service.check_and_generate_notifications(metrics, cached_metrics)
|
||||||
|
|
||||||
# Then update cached metrics after comparison
|
# Then update cached metrics after comparison
|
||||||
cached_metrics = metrics
|
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)
|
# Update state history (only once)
|
||||||
state_manager.update_metrics_history(metrics)
|
state_manager.update_metrics_history(metrics)
|
||||||
@ -771,7 +784,7 @@ def get_config():
|
|||||||
@app.route("/api/config", methods=["POST"])
|
@app.route("/api/config", methods=["POST"])
|
||||||
def update_config():
|
def update_config():
|
||||||
"""API endpoint to update configuration."""
|
"""API endpoint to update configuration."""
|
||||||
global dashboard_service, worker_service # Add this to access the global dashboard_service
|
global dashboard_service, worker_service
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get the request data
|
# Get the request data
|
||||||
@ -783,11 +796,19 @@ def update_config():
|
|||||||
logging.error("Invalid configuration format")
|
logging.error("Invalid configuration format")
|
||||||
return jsonify({"error": "Invalid configuration format"}), 400
|
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
|
# Required fields and default values
|
||||||
defaults = {
|
defaults = {
|
||||||
"wallet": "yourwallethere",
|
"wallet": "yourwallethere",
|
||||||
"power_cost": 0.0,
|
"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
|
# Merge new config with defaults for any missing fields
|
||||||
@ -802,7 +823,8 @@ def update_config():
|
|||||||
dashboard_service = MiningDashboardService(
|
dashboard_service = MiningDashboardService(
|
||||||
new_config.get("power_cost", 0.0),
|
new_config.get("power_cost", 0.0),
|
||||||
new_config.get("power_usage", 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')}")
|
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)
|
worker_service.set_dashboard_service(dashboard_service)
|
||||||
logging.info(f"Worker service updated with the new 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
|
# Force a metrics update to reflect the new configuration
|
||||||
update_metrics_job(force=True)
|
update_metrics_job(force=True)
|
||||||
logging.info("Forced metrics update after configuration change")
|
logging.info("Forced metrics update after configuration change")
|
||||||
@ -1127,10 +1158,12 @@ def api_clear_notifications():
|
|||||||
"""API endpoint to clear notifications."""
|
"""API endpoint to clear notifications."""
|
||||||
category = request.json.get('category')
|
category = request.json.get('category')
|
||||||
older_than_days = request.json.get('older_than_days')
|
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(
|
cleared_count = notification_service.clear_notifications(
|
||||||
category=category,
|
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({
|
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:
|
## 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
|
- **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
|
- **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
|
### Worker Management
|
||||||
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
|
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
|
||||||
- **Status Monitoring**: Real-time status indicators for online and offline devices
|
- **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.
|
- `POWER_USAGE`: Power usage in watts.
|
||||||
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
|
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
|
||||||
- `TIMEZONE`: Local timezone for displaying time information.
|
- `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.
|
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/available_timezones`: Returns a list of supported timezones.
|
||||||
- `/api/config`: Fetches or updates the mining configuration.
|
- `/api/config`: Fetches or updates the mining configuration.
|
||||||
- `/api/health`: Returns the health status of the application.
|
- `/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
|
## Project Structure
|
||||||
|
|
||||||
@ -244,6 +255,9 @@ For optimal performance:
|
|||||||
4. Access the health endpoint at `/api/health` for diagnostics
|
4. Access the health endpoint at `/api/health` for diagnostics
|
||||||
5. For stale data issues, use the Force Refresh function
|
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)
|
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
|
## License
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Default configuration file path
|
# Default configuration file path
|
||||||
CONFIG_FILE = "config.json"
|
CONFIG_FILE = "/root/config.json"
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
|
@ -164,6 +164,18 @@ class MiningDashboardService:
|
|||||||
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
|
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
metrics["server_start_time"] = 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
|
# Log execution time
|
||||||
execution_time = time.time() - start_time
|
execution_time = time.time() - start_time
|
||||||
metrics["execution_time"] = execution_time
|
metrics["execution_time"] = execution_time
|
||||||
@ -452,6 +464,32 @@ class MiningDashboardService:
|
|||||||
logging.error(f"Error fetching {url}: {e}")
|
logging.error(f"Error fetching {url}: {e}")
|
||||||
return None
|
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):
|
def get_bitcoin_stats(self):
|
||||||
"""
|
"""
|
||||||
Fetch Bitcoin network statistics with improved error handling and caching.
|
Fetch Bitcoin network statistics with improved error handling and caching.
|
||||||
|
@ -16,11 +16,6 @@ services:
|
|||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
environment:
|
environment:
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS
|
|
||||||
- POWER_COST=0
|
|
||||||
- POWER_USAGE=0
|
|
||||||
- NETWORK_FEE=0
|
|
||||||
- TIMEZONE=America/Los_Angeles
|
|
||||||
- LOG_LEVEL=INFO
|
- LOG_LEVEL=INFO
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
|
@ -1,14 +1,17 @@
|
|||||||
# notification_service.py
|
# notification_service.py
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
import pytz
|
import pytz
|
||||||
|
import re
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from typing import List, Dict, Any, Optional, Union
|
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
|
# Constants to replace magic values
|
||||||
ONE_DAY_SECONDS = 86400
|
ONE_DAY_SECONDS = 86400
|
||||||
@ -29,6 +32,52 @@ class NotificationCategory(Enum):
|
|||||||
EARNINGS = "earnings"
|
EARNINGS = "earnings"
|
||||||
SYSTEM = "system"
|
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:
|
class NotificationService:
|
||||||
"""Service for managing mining dashboard notifications."""
|
"""Service for managing mining dashboard notifications."""
|
||||||
|
|
||||||
@ -263,35 +312,37 @@ class NotificationService:
|
|||||||
|
|
||||||
return deleted > 0
|
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.
|
Clear notifications with optimized filtering.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
category (str, optional): Only clear specific category
|
category (str, optional): Only clear specific category
|
||||||
older_than_days (int, optional): Only clear notifications older than this
|
older_than_days (int, optional): Only clear notifications older than this
|
||||||
|
read_only (bool, optional): Only clear notifications that have been read
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
int: Number of notifications cleared
|
int: Number of notifications cleared
|
||||||
"""
|
"""
|
||||||
original_count = len(self.notifications)
|
original_count = len(self.notifications)
|
||||||
|
|
||||||
cutoff_date = None
|
cutoff_date = None
|
||||||
if older_than_days:
|
if older_than_days:
|
||||||
cutoff_date = self._get_current_time() - timedelta(days=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 = [
|
self.notifications = [
|
||||||
n for n in self.notifications
|
n for n in self.notifications
|
||||||
if (not category or n.get("category") != category) and
|
if (category and n.get("category") != category) or # Keep if we're filtering by category and this isn't that category
|
||||||
(not cutoff_date or self._parse_timestamp(n.get("timestamp", self._get_current_time().isoformat())) >= cutoff_date)
|
(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)
|
cleared_count = original_count - len(self.notifications)
|
||||||
if cleared_count > 0:
|
if cleared_count > 0:
|
||||||
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
|
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
|
||||||
self._save_notifications()
|
self._save_notifications()
|
||||||
|
|
||||||
return cleared_count
|
return cleared_count
|
||||||
|
|
||||||
def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
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:
|
if not current_metrics:
|
||||||
logging.warning("[NotificationService] No current metrics available, skipping notification checks")
|
logging.warning("[NotificationService] No current metrics available, skipping notification checks")
|
||||||
return new_notifications
|
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)
|
# Check for block updates (using persistent storage)
|
||||||
last_block_height = current_metrics.get("last_block_height")
|
last_block_height = current_metrics.get("last_block_height")
|
||||||
@ -384,12 +440,22 @@ class NotificationService:
|
|||||||
hashrate_24hr = metrics.get("hashrate_24hr", 0)
|
hashrate_24hr = metrics.get("hashrate_24hr", 0)
|
||||||
hashrate_unit = metrics.get("hashrate_24hr_unit", "TH/s")
|
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_mined_sats = metrics.get("daily_mined_sats", 0)
|
||||||
daily_profit_usd = metrics.get("daily_profit_usd", 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
|
# 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
|
# Add notification
|
||||||
logging.info(f"[NotificationService] Generating daily stats notification: {message}")
|
logging.info(f"[NotificationService] Generating daily stats notification: {message}")
|
||||||
@ -401,7 +467,8 @@ class NotificationService:
|
|||||||
"hashrate": hashrate_24hr,
|
"hashrate": hashrate_24hr,
|
||||||
"unit": hashrate_unit,
|
"unit": hashrate_unit,
|
||||||
"daily_sats": daily_mined_sats,
|
"daily_sats": daily_mined_sats,
|
||||||
"daily_profit": daily_profit_usd
|
"daily_profit": daily_profit_usd,
|
||||||
|
"currency": user_currency
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -447,38 +514,71 @@ class NotificationService:
|
|||||||
return 0.0
|
return 0.0
|
||||||
|
|
||||||
def _check_hashrate_change(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
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:
|
try:
|
||||||
# Get 10min hashrate values
|
# Check if we're in low hashrate mode
|
||||||
current_10min = current.get("hashrate_10min", 0)
|
# A simple threshold approach: if hashrate_3hr is below 1 TH/s, consider it low hashrate mode
|
||||||
previous_10min = previous.get("hashrate_10min", 0)
|
is_low_hashrate_mode = False
|
||||||
|
|
||||||
# Log what we're comparing
|
if "hashrate_3hr" in current:
|
||||||
logging.debug(f"[NotificationService] Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}")
|
current_3hr = self._parse_numeric_value(current.get("hashrate_3hr", 0))
|
||||||
|
current_3hr_unit = current.get("hashrate_3hr_unit", "TH/s").lower()
|
||||||
# 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)
|
|
||||||
|
|
||||||
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)
|
# Skip if previous was zero (prevents division by zero)
|
||||||
if previous_value == 0:
|
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
|
return None
|
||||||
|
|
||||||
# Calculate percentage change
|
# Calculate percentage change
|
||||||
percent_change = ((current_value - previous_value) / previous_value) * 100
|
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
|
# Significant decrease
|
||||||
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
|
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}")
|
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
|
||||||
return self.add_notification(
|
return self.add_notification(
|
||||||
message,
|
message,
|
||||||
@ -488,13 +588,14 @@ class NotificationService:
|
|||||||
"previous": previous_value,
|
"previous": previous_value,
|
||||||
"current": current_value,
|
"current": current_value,
|
||||||
"change": percent_change,
|
"change": percent_change,
|
||||||
"timeframe": "10min"
|
"timeframe": timeframe,
|
||||||
|
"is_low_hashrate_mode": is_low_hashrate_mode
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Significant increase
|
# Significant increase
|
||||||
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
|
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}")
|
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
|
||||||
return self.add_notification(
|
return self.add_notification(
|
||||||
message,
|
message,
|
||||||
@ -504,10 +605,11 @@ class NotificationService:
|
|||||||
"previous": previous_value,
|
"previous": previous_value,
|
||||||
"current": current_value,
|
"current": current_value,
|
||||||
"change": percent_change,
|
"change": percent_change,
|
||||||
"timeframe": "10min"
|
"timeframe": timeframe,
|
||||||
|
"is_low_hashrate_mode": is_low_hashrate_mode
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"[NotificationService] Error checking hashrate change: {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]]:
|
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."""
|
"""Check for significant earnings progress or payout approach."""
|
||||||
try:
|
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"))
|
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0"))
|
||||||
|
|
||||||
# Check if approaching payout
|
# Check if approaching payout
|
||||||
if current.get("est_time_to_payout"):
|
if current.get("est_time_to_payout"):
|
||||||
est_time = 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}")
|
logging.error(f"[NotificationService] Error checking earnings progress: {e}")
|
||||||
return None
|
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:
|
def _should_send_payout_notification(self) -> bool:
|
||||||
"""Check if enough time has passed since the last payout notification."""
|
"""Check if enough time has passed since the last payout notification."""
|
||||||
if self.last_payout_notification_time is None:
|
if self.last_payout_notification_time is None:
|
||||||
return True
|
return True
|
||||||
time_since_last_notification = self._get_current_time() - self.last_payout_notification_time
|
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:
|
if not self.redis_client:
|
||||||
logging.info("Redis not available, skipping state save.")
|
logging.info("Redis not available, skipping state save.")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Check if we've saved recently to avoid too frequent saves
|
|
||||||
# Only save at most once every 5 minutes
|
|
||||||
current_time = time.time()
|
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")
|
logging.debug("Skipping Redis save - last save was less than 5 minutes ago")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Update the last save time
|
|
||||||
self.last_save_time = current_time
|
self.last_save_time = current_time
|
||||||
|
|
||||||
# Prune data first to reduce volume
|
|
||||||
self.prune_old_data()
|
self.prune_old_data()
|
||||||
|
|
||||||
# Create compact versions of the data structures for Redis storage
|
|
||||||
try:
|
try:
|
||||||
# 1. Create compact arrow_history with minimal data
|
# Compact arrow_history with unit preservation
|
||||||
compact_arrow_history = {}
|
compact_arrow_history = {}
|
||||||
for key, values in arrow_history.items():
|
for key, values in arrow_history.items():
|
||||||
if isinstance(values, list) and values:
|
if isinstance(values, list) and values:
|
||||||
# Only store recent history (last 2 hours)
|
|
||||||
recent_values = values[-180:] if len(values) > 180 else values
|
recent_values = values[-180:] if len(values) > 180 else values
|
||||||
# Use shorter field names and preserve arrow directions
|
|
||||||
compact_arrow_history[key] = [
|
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
|
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
|
compact_hashrate_history = hashrate_history[-60:] if len(hashrate_history) > 60 else hashrate_history
|
||||||
|
|
||||||
# 3. Only keep recent metrics_log entries (last 30 minutes)
|
# Compact metrics_log with unit preservation
|
||||||
# This is typically the largest data structure
|
|
||||||
compact_metrics_log = []
|
compact_metrics_log = []
|
||||||
if 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:
|
for entry in recent_logs:
|
||||||
# Only keep necessary fields from each metrics entry
|
metrics_copy = {}
|
||||||
if "metrics" in entry and "timestamp" in entry:
|
original_metrics = entry["metrics"]
|
||||||
metrics_copy = {}
|
essential_keys = [
|
||||||
original_metrics = entry["metrics"]
|
"hashrate_60sec", "hashrate_24hr", "btc_price",
|
||||||
|
"workers_hashing", "unpaid_earnings", "difficulty",
|
||||||
# Only copy the most important metrics for historical tracking
|
"network_hashrate", "daily_profit_usd"
|
||||||
essential_keys = [
|
]
|
||||||
"hashrate_60sec", "hashrate_24hr", "btc_price",
|
for key in essential_keys:
|
||||||
"workers_hashing", "unpaid_earnings", "difficulty",
|
if key in original_metrics:
|
||||||
"network_hashrate", "daily_profit_usd"
|
metrics_copy[key] = {
|
||||||
]
|
"value": original_metrics[key],
|
||||||
|
"unit": original_metrics.get(f"{key}_unit", "th/s")
|
||||||
for key in essential_keys:
|
}
|
||||||
if key in original_metrics:
|
compact_metrics_log.append({
|
||||||
metrics_copy[key] = original_metrics[key]
|
"ts": entry["timestamp"],
|
||||||
|
"m": metrics_copy
|
||||||
# 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
|
|
||||||
state = {
|
state = {
|
||||||
"arrow_history": compact_arrow_history,
|
"arrow_history": compact_arrow_history,
|
||||||
"hashrate_history": compact_hashrate_history,
|
"hashrate_history": compact_hashrate_history,
|
||||||
"metrics_log": compact_metrics_log
|
"metrics_log": compact_metrics_log
|
||||||
}
|
}
|
||||||
|
|
||||||
# Convert to JSON once to reuse and measure size
|
|
||||||
state_json = json.dumps(state)
|
state_json = json.dumps(state)
|
||||||
data_size_kb = len(state_json) / 1024
|
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)")
|
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)
|
self.redis_client.set(f"{self.STATE_KEY}_version", "2.0")
|
||||||
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(self.STATE_KEY, state_json)
|
self.redis_client.set(self.STATE_KEY, state_json)
|
||||||
logging.info(f"Successfully saved graph state to Redis ({data_size_kb:.2f} KB)")
|
logging.info(f"Successfully saved graph state to Redis ({data_size_kb:.2f} KB)")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
@ -1,14 +1,10 @@
|
|||||||
.footer {
|
.footer {
|
||||||
margin-top: 30px;
|
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
color: grey;
|
color: grey;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style >
|
|
||||||
<!-- Preload theme to prevent flicker -->
|
|
||||||
<style id="theme-preload" >
|
|
||||||
/* Theme-aware loading state */
|
/* Theme-aware loading state */
|
||||||
html.bitcoin-theme {
|
html.bitcoin-theme {
|
||||||
background-color: #111111;
|
background-color: #111111;
|
||||||
|
@ -184,7 +184,7 @@
|
|||||||
}
|
}
|
||||||
/* Add bottom padding to accommodate minimized system monitor */
|
/* Add bottom padding to accommodate minimized system monitor */
|
||||||
.container-fluid {
|
.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 */
|
/* Add these styles to dashboard.css */
|
||||||
@ -218,7 +218,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datum-label {
|
.datum-label {
|
||||||
color: #ffffff; /* White color */
|
color: cyan; /* cyan color */
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
@ -245,3 +245,10 @@
|
|||||||
.unlucky {
|
.unlucky {
|
||||||
color: #ff5555 !important;
|
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 {
|
.notification-actions {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
width: 100%; /* Full width on small screens */
|
width: 100%; /* Full width on small screens */
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
font-size: 1rem;
|
font-size: 0.95rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-controls {
|
.notification-controls {
|
||||||
@ -297,16 +298,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.filter-buttons {
|
.filter-buttons {
|
||||||
overflow-x: auto;
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 8px;
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 10px;
|
||||||
white-space: nowrap;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-button {
|
||||||
|
text-align: center;
|
||||||
|
white-space: normal;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
padding: 8px 5px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 38px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-actions {
|
.notification-actions {
|
||||||
justify-content: flex-end;
|
justify-content: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-item {
|
.notification-item {
|
||||||
@ -324,4 +336,11 @@
|
|||||||
.notification-actions {
|
.notification-actions {
|
||||||
flex: 0 0 60px;
|
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);
|
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) */
|
/* Bitcoin theme specific styling (orange) */
|
||||||
body:not(.deepsea-theme) #themeToggle,
|
body:not(.deepsea-theme) #themeToggle,
|
||||||
body:not(.deepsea-theme) .theme-toggle-btn {
|
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);
|
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 */
|
/* Add these theme-specific loading styles */
|
||||||
#theme-loader {
|
#theme-loader {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
@ -8,8 +8,10 @@
|
|||||||
const BitcoinMinuteRefresh = (function () {
|
const BitcoinMinuteRefresh = (function () {
|
||||||
// Constants
|
// Constants
|
||||||
const STORAGE_KEY = 'bitcoin_last_refresh_time';
|
const STORAGE_KEY = 'bitcoin_last_refresh_time';
|
||||||
const BITCOIN_COLOR = '#f7931a';
|
// Default fallback colors if CSS vars aren't available
|
||||||
const DEEPSEA_COLOR = '#0088cc';
|
const FALLBACK_BITCOIN_COLOR = '#f2a900';
|
||||||
|
const FALLBACK_DEEPSEA_COLOR = '#0088cc';
|
||||||
|
|
||||||
const DOM_IDS = {
|
const DOM_IDS = {
|
||||||
TERMINAL: 'bitcoin-terminal',
|
TERMINAL: 'bitcoin-terminal',
|
||||||
STYLES: 'bitcoin-terminal-styles',
|
STYLES: 'bitcoin-terminal-styles',
|
||||||
@ -45,9 +47,32 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
let uptimeInterval = null;
|
let uptimeInterval = null;
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let refreshCallback = null;
|
let refreshCallback = null;
|
||||||
let currentThemeColor = BITCOIN_COLOR; // Default Bitcoin color
|
let currentThemeColor = '';
|
||||||
|
let currentThemeRGB = '';
|
||||||
let dragListenersAdded = false;
|
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
|
* Logging helper function
|
||||||
* @param {string} message - Message to log
|
* @param {string} message - Message to log
|
||||||
@ -79,24 +104,22 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
* Apply the current theme color
|
* Apply the current theme color
|
||||||
*/
|
*/
|
||||||
function applyThemeColor() {
|
function applyThemeColor() {
|
||||||
// Check if theme toggle is set to DeepSea
|
// Get current theme colors
|
||||||
const isDeepSeaTheme = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
|
const theme = getThemeColors();
|
||||||
currentThemeColor = isDeepSeaTheme ? DEEPSEA_COLOR : BITCOIN_COLOR;
|
currentThemeColor = theme.color;
|
||||||
|
currentThemeRGB = theme.rgb;
|
||||||
|
|
||||||
// Don't try to update DOM elements if they don't exist yet
|
// Don't try to update DOM elements if they don't exist yet
|
||||||
if (!terminalElement) return;
|
if (!terminalElement) return;
|
||||||
|
|
||||||
// Define color values based on theme
|
|
||||||
const rgbValues = isDeepSeaTheme ? '0, 136, 204' : '247, 147, 26';
|
|
||||||
|
|
||||||
// Create theme config
|
// Create theme config
|
||||||
const themeConfig = {
|
const themeConfig = {
|
||||||
color: currentThemeColor,
|
color: currentThemeColor,
|
||||||
borderColor: currentThemeColor,
|
borderColor: currentThemeColor,
|
||||||
boxShadow: `0 0 5px rgba(${rgbValues}, 0.3)`,
|
boxShadow: `0 0 5px rgba(${currentThemeRGB}, 0.3)`,
|
||||||
textShadow: `0 0 5px rgba(${rgbValues}, 0.8)`,
|
textShadow: `0 0 5px rgba(${currentThemeRGB}, 0.8)`,
|
||||||
borderColorRGBA: `rgba(${rgbValues}, 0.5)`,
|
borderColorRGBA: `rgba(${currentThemeRGB}, 0.5)`,
|
||||||
textShadowStrong: `0 0 8px rgba(${rgbValues}, 0.8)`
|
textShadowStrong: `0 0 8px rgba(${currentThemeRGB}, 0.8)`
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply styles to terminal
|
// Apply styles to terminal
|
||||||
@ -144,6 +167,13 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
if (miniLabel) {
|
if (miniLabel) {
|
||||||
miniLabel.style.color = themeConfig.color;
|
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();
|
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-header">
|
||||||
<div class="terminal-title">SYSTEM MONITOR v.3</div>
|
<div class="terminal-title">SYSTEM MONITOR v.3</div>
|
||||||
<div class="terminal-controls">
|
<div class="terminal-controls">
|
||||||
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()"></div>
|
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()">
|
||||||
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()"></div>
|
<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>
|
</div>
|
||||||
<div class="terminal-content">
|
<div class="terminal-content">
|
||||||
@ -413,13 +466,12 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
* Add CSS styles for the terminal
|
* Add CSS styles for the terminal
|
||||||
*/
|
*/
|
||||||
function addStyles() {
|
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');
|
const styleElement = document.createElement('style');
|
||||||
styleElement.id = DOM_IDS.STYLES;
|
styleElement.id = DOM_IDS.STYLES;
|
||||||
|
|
||||||
// Generate RGB values for dynamic colors
|
|
||||||
const rgbValues = currentThemeColor === DEEPSEA_COLOR ? '0, 136, 204' : '247, 147, 26';
|
|
||||||
|
|
||||||
styleElement.textContent = `
|
styleElement.textContent = `
|
||||||
/* Terminal Container */
|
/* Terminal Container */
|
||||||
.bitcoin-terminal {
|
.bitcoin-terminal {
|
||||||
@ -428,14 +480,14 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
right: 20px;
|
right: 20px;
|
||||||
width: 230px;
|
width: 230px;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
border: 1px solid ${currentThemeColor};
|
border: 1px solid var(--primary-color, ${theme.color});
|
||||||
color: ${currentThemeColor};
|
color: var(--primary-color, ${theme.color});
|
||||||
font-family: 'VT323', monospace;
|
font-family: 'VT323', monospace;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
transition: all 0.3s ease;
|
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 */
|
/* Terminal Header */
|
||||||
@ -443,20 +495,19 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid ${currentThemeColor};
|
border-bottom: 1px solid var(--primary-color, ${theme.color});
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: grab; /* Add grab cursor on hover */
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply grabbing cursor during active drag */
|
|
||||||
.terminal-header:active,
|
.terminal-header:active,
|
||||||
.bitcoin-terminal.dragging .terminal-header {
|
.bitcoin-terminal.dragging .terminal-header {
|
||||||
cursor: grabbing;
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.terminal-title {
|
.terminal-title {
|
||||||
color: ${currentThemeColor};
|
color: var(--primary-color, ${theme.color});
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
animation: terminal-flicker 4s infinite;
|
animation: terminal-flicker 4s infinite;
|
||||||
@ -470,22 +521,45 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.terminal-dot {
|
.terminal-dot {
|
||||||
width: 8px;
|
width: 12px;
|
||||||
height: 8px;
|
height: 12px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background-color: #555;
|
background-color: #555;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.3s;
|
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 {
|
.terminal-dot.minimize:hover {
|
||||||
background-color: #ffcc00;
|
background-color: #ffcc00;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-dot.minimize:hover .control-symbol {
|
||||||
|
color: #664e00;
|
||||||
|
}
|
||||||
|
|
||||||
.terminal-dot.close:hover {
|
.terminal-dot.close:hover {
|
||||||
background-color: #ff3b30;
|
background-color: #ff3b30;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.terminal-dot.close:hover .control-symbol {
|
||||||
|
color: #7a0200;
|
||||||
|
}
|
||||||
|
|
||||||
/* Terminal Content */
|
/* Terminal Content */
|
||||||
.terminal-content {
|
.terminal-content {
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -524,14 +598,14 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
|
/* Uptime Display */
|
||||||
.uptime-timer {
|
.uptime-timer {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: #111;
|
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;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -586,7 +660,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
background-color: ${currentThemeColor};
|
background-color: var(--primary-color, ${theme.color});
|
||||||
color: #000;
|
color: #000;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@ -594,7 +668,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: none;
|
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 */
|
/* CRT scanline effect */
|
||||||
@ -660,7 +734,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-left: 45px;
|
margin-left: 45px;
|
||||||
color: ${currentThemeColor};
|
color: var(--primary-color, ${theme.color});
|
||||||
}
|
}
|
||||||
|
|
||||||
#${DOM_IDS.MINIMIZED_UPTIME} {
|
#${DOM_IDS.MINIMIZED_UPTIME} {
|
||||||
@ -855,9 +929,6 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
// Store the refresh callback
|
// Store the refresh callback
|
||||||
refreshCallback = refreshFunc;
|
refreshCallback = refreshFunc;
|
||||||
|
|
||||||
// Get current theme status
|
|
||||||
applyThemeColor();
|
|
||||||
|
|
||||||
// Create the terminal element if it doesn't exist
|
// Create the terminal element if it doesn't exist
|
||||||
if (!document.getElementById(DOM_IDS.TERMINAL)) {
|
if (!document.getElementById(DOM_IDS.TERMINAL)) {
|
||||||
createTerminalElement();
|
createTerminalElement();
|
||||||
@ -865,11 +936,11 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
// Get references to existing elements
|
// Get references to existing elements
|
||||||
terminalElement = document.getElementById(DOM_IDS.TERMINAL);
|
terminalElement = document.getElementById(DOM_IDS.TERMINAL);
|
||||||
uptimeElement = document.getElementById('uptime-timer');
|
uptimeElement = document.getElementById('uptime-timer');
|
||||||
|
|
||||||
// Apply theme to existing element
|
|
||||||
applyThemeColor();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Apply theme colors
|
||||||
|
applyThemeColor();
|
||||||
|
|
||||||
// Set up listener for theme changes
|
// Set up listener for theme changes
|
||||||
setupThemeChangeListener();
|
setupThemeChangeListener();
|
||||||
|
|
||||||
@ -923,6 +994,9 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
log("Error reading updated server time: " + e.message, 'error');
|
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) {
|
if (!document.hidden) {
|
||||||
log("Page became visible, updating");
|
log("Page became visible, updating");
|
||||||
|
|
||||||
|
// Apply current theme when page becomes visible
|
||||||
|
applyThemeColor();
|
||||||
|
|
||||||
// Update immediately when page becomes visible
|
// Update immediately when page becomes visible
|
||||||
updateClock();
|
updateClock();
|
||||||
updateUptime();
|
updateUptime();
|
||||||
@ -989,6 +1066,11 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
showButton.textContent = 'Show Monitor';
|
showButton.textContent = 'Show Monitor';
|
||||||
showButton.onclick = showTerminal;
|
showButton.onclick = showTerminal;
|
||||||
document.body.appendChild(showButton);
|
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';
|
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>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<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 -->
|
<!-- Common fonts -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
|
<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">
|
<h1 class="text-center">
|
||||||
<a href="/" style="text-decoration:none; color:inherit;">
|
<a href="/" style="text-decoration:none; color:inherit;">
|
||||||
{% block header %}BTC-OS MINING DASHBOARD{% endblock %}
|
{% block header %}BTC-OS DASHBOARD{% endblock %}
|
||||||
</a>
|
</a>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
@ -135,6 +135,7 @@
|
|||||||
<!-- Footer -->
|
<!-- Footer -->
|
||||||
<footer class="footer text-center">
|
<footer class="footer text-center">
|
||||||
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
|
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
|
||||||
|
<p>v0.9.2 - Public Beta</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -68,9 +68,9 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
<!-- Theme toggle button (new) -->
|
<!-- Theme toggle button (new) -->
|
||||||
<button id="themeToggle" class="theme-toggle-btn">
|
<!--<button id="themeToggle" class="theme-toggle-btn">
|
||||||
<span>Toggle Theme</span>
|
<span>Toggle Theme</span>
|
||||||
</button>
|
</button>-->
|
||||||
<button id="skip-button">SKIP</button>
|
<button id="skip-button">SKIP</button>
|
||||||
<div id="debug-info"></div>
|
<div id="debug-info"></div>
|
||||||
<div id="loading-message">Loading mining data...</div>
|
<div id="loading-message">Loading mining data...</div>
|
||||||
@ -109,6 +109,27 @@ v.21
|
|||||||
</label>
|
</label>
|
||||||
<input type="text" id="wallet-address" placeholder="bc1..." value="">
|
<input type="text" id="wallet-address" placeholder="bc1..." value="">
|
||||||
</div>
|
</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">
|
<div class="form-group">
|
||||||
<label for="power-cost">
|
<label for="power-cost">
|
||||||
Power Cost ($/kWh)
|
Power Cost ($/kWh)
|
||||||
@ -131,7 +152,7 @@ v.21
|
|||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="network-fee">
|
<label for="network-fee">
|
||||||
Network Fee (%)
|
Firmware/Other Fees (%)
|
||||||
<span class="tooltip">
|
<span class="tooltip">
|
||||||
?
|
?
|
||||||
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
|
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
|
||||||
@ -281,20 +302,22 @@ v.21
|
|||||||
loadTimezoneFromConfig();
|
loadTimezoneFromConfig();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update saveConfig to include network fee
|
// Update saveConfig to include currency
|
||||||
function saveConfig() {
|
function saveConfig() {
|
||||||
const wallet = document.getElementById('wallet-address').value.trim();
|
const wallet = document.getElementById('wallet-address').value.trim();
|
||||||
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
|
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
|
||||||
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
|
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
|
||||||
const timezone = document.getElementById('timezone').value;
|
const timezone = document.getElementById('timezone').value;
|
||||||
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
|
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
|
||||||
|
const currency = document.getElementById('currency').value;
|
||||||
|
|
||||||
const updatedConfig = {
|
const updatedConfig = {
|
||||||
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
|
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
|
||||||
power_cost: powerCost,
|
power_cost: powerCost,
|
||||||
power_usage: powerUsage,
|
power_usage: powerUsage,
|
||||||
timezone: timezone,
|
timezone: timezone,
|
||||||
network_fee: networkFee
|
network_fee: networkFee,
|
||||||
|
currency: currency
|
||||||
};
|
};
|
||||||
|
|
||||||
return fetch('/api/config', {
|
return fetch('/api/config', {
|
||||||
@ -345,7 +368,7 @@ v.21
|
|||||||
power_usage: 0.0
|
power_usage: 0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Update loadConfig function to include network fee
|
// Update loadConfig function to handle currency
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetch('/api/config?nocache=' + new Date().getTime())
|
fetch('/api/config?nocache=' + new Date().getTime())
|
||||||
@ -364,6 +387,12 @@ v.21
|
|||||||
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
||||||
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
||||||
document.getElementById('network-fee').value = currentConfig.network_fee || "";
|
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;
|
configLoaded = true;
|
||||||
resolve(currentConfig);
|
resolve(currentConfig);
|
||||||
})
|
})
|
||||||
@ -374,13 +403,16 @@ v.21
|
|||||||
wallet: "yourwallethere",
|
wallet: "yourwallethere",
|
||||||
power_cost: 0.0,
|
power_cost: 0.0,
|
||||||
power_usage: 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('wallet-address').value = currentConfig.wallet || "";
|
||||||
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
||||||
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
||||||
document.getElementById('network-fee').value = currentConfig.network_fee || "";
|
document.getElementById('network-fee').value = currentConfig.network_fee || "";
|
||||||
|
document.getElementById('currency').value = currentConfig.currency || "USD";
|
||||||
|
|
||||||
resolve(currentConfig);
|
resolve(currentConfig);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -55,43 +55,6 @@
|
|||||||
<div class="card" id="payoutMiscCard">
|
<div class="card" id="payoutMiscCard">
|
||||||
<div class="card-header">Payout Info</div>
|
<div class="card-header">Payout Info</div>
|
||||||
<div class="card-body">
|
<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>
|
<p>
|
||||||
<strong>Pool Fees:</strong>
|
<strong>Pool Fees:</strong>
|
||||||
<span id="pool_fees_percentage" class="metric-value">
|
<span id="pool_fees_percentage" class="metric-value">
|
||||||
@ -106,6 +69,43 @@
|
|||||||
</span>
|
</span>
|
||||||
<span id="indicator_pool_fees_percentage"></span>
|
<span id="indicator_pool_fees_percentage"></span>
|
||||||
</p>
|
</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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user