mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00
Compare commits
7 Commits
09cf1cd17f
...
7ef24db7e2
Author | SHA1 | Date | |
---|---|---|---|
7ef24db7e2 | |||
![]() |
2f0da50d93 | ||
![]() |
034aec6d12 | ||
![]() |
bdb757c1db | ||
![]() |
bf53b9159d | ||
![]() |
05301cc1ea | ||
![]() |
5a6331d032 |
194
App.py
194
App.py
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Main application module for the Bitcoin Mining Dashboard.
|
Main application module for the Bitcoin Mining Dashboard.
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
@ -533,7 +533,19 @@ def create_scheduler():
|
|||||||
def commafy(value):
|
def commafy(value):
|
||||||
"""Add commas to numbers for better readability."""
|
"""Add commas to numbers for better readability."""
|
||||||
try:
|
try:
|
||||||
return "{:,}".format(int(value))
|
# Check if the value is already a string with decimal places
|
||||||
|
if isinstance(value, str) and '.' in value:
|
||||||
|
# Split by decimal point
|
||||||
|
integer_part, decimal_part = value.split('.')
|
||||||
|
# Format integer part with commas and rejoin with decimal part
|
||||||
|
return "{:,}.{}".format(int(integer_part), decimal_part)
|
||||||
|
elif isinstance(value, (int, float)):
|
||||||
|
# If it's a float, preserve decimal places
|
||||||
|
if isinstance(value, float):
|
||||||
|
return "{:,.2f}".format(value)
|
||||||
|
# If it's an integer, format without decimal places
|
||||||
|
return "{:,}".format(value)
|
||||||
|
return value
|
||||||
except Exception:
|
except Exception:
|
||||||
return value
|
return value
|
||||||
|
|
||||||
@ -1227,6 +1239,184 @@ def reset_chart_data():
|
|||||||
logging.error(f"Error resetting chart data: {e}")
|
logging.error(f"Error resetting chart data: {e}")
|
||||||
return jsonify({"status": "error", "message": str(e)}), 500
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
|
# First, register the template filter outside of any route function
|
||||||
|
# Add this near the top of your file with other template filters
|
||||||
|
@app.template_filter('format_datetime')
|
||||||
|
def format_datetime(value, timezone=None):
|
||||||
|
"""Format a datetime string according to the specified timezone using AM/PM format."""
|
||||||
|
if not value:
|
||||||
|
return "None"
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
if timezone is None:
|
||||||
|
# Use default timezone if none provided
|
||||||
|
timezone = get_timezone()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if isinstance(value, str):
|
||||||
|
# Parse the string to a datetime object
|
||||||
|
dt = datetime.datetime.strptime(value, "%Y-%m-%d %H:%M")
|
||||||
|
else:
|
||||||
|
dt = value
|
||||||
|
|
||||||
|
# Make datetime timezone aware
|
||||||
|
if dt.tzinfo is None:
|
||||||
|
dt = pytz.UTC.localize(dt)
|
||||||
|
|
||||||
|
# Convert to user's timezone
|
||||||
|
user_tz = pytz.timezone(timezone)
|
||||||
|
dt = dt.astimezone(user_tz)
|
||||||
|
|
||||||
|
# Format according to user preference with AM/PM format
|
||||||
|
return dt.strftime("%b %d, %Y %I:%M %p")
|
||||||
|
except ValueError:
|
||||||
|
return value
|
||||||
|
|
||||||
|
# Then update your earnings route
|
||||||
|
@app.route('/earnings')
|
||||||
|
def earnings():
|
||||||
|
"""Serve the earnings page with user's currency and timezone preferences."""
|
||||||
|
try:
|
||||||
|
# Get user's currency and timezone preferences
|
||||||
|
from config import get_currency, get_timezone
|
||||||
|
user_currency = get_currency()
|
||||||
|
user_timezone = get_timezone()
|
||||||
|
|
||||||
|
# Define currency symbols for common currencies
|
||||||
|
currency_symbols = {
|
||||||
|
'USD': '$',
|
||||||
|
'EUR': '€',
|
||||||
|
'GBP': '£',
|
||||||
|
'JPY': '¥',
|
||||||
|
'CAD': 'C$',
|
||||||
|
'AUD': 'A$',
|
||||||
|
'CNY': '¥',
|
||||||
|
'KRW': '₩',
|
||||||
|
'BRL': 'R$',
|
||||||
|
'CHF': 'Fr'
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add graceful error handling for earnings data
|
||||||
|
try:
|
||||||
|
# Get earnings data with a longer timeout
|
||||||
|
earnings_data = dashboard_service.get_earnings_data()
|
||||||
|
except requests.exceptions.ReadTimeout:
|
||||||
|
logging.warning("Timeout fetching earnings data from ocean.xyz - using cached or fallback data")
|
||||||
|
# Try to use cached metrics as fallback
|
||||||
|
if cached_metrics and 'unpaid_earnings' in cached_metrics:
|
||||||
|
# Create minimal earnings data from cached metrics
|
||||||
|
earnings_data = {
|
||||||
|
'payments': [],
|
||||||
|
'total_payments': 0,
|
||||||
|
'total_paid_btc': 0,
|
||||||
|
'total_paid_sats': 0,
|
||||||
|
'total_paid_usd': 0,
|
||||||
|
'unpaid_earnings': cached_metrics.get('unpaid_earnings', 0),
|
||||||
|
'unpaid_earnings_sats': int(float(cached_metrics.get('unpaid_earnings', 0)) * 100000000),
|
||||||
|
'est_time_to_payout': cached_metrics.get('est_time_to_payout', 'Unknown'),
|
||||||
|
'monthly_summaries': [],
|
||||||
|
'timestamp': datetime.now(ZoneInfo(user_timezone)).isoformat()
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
# Create empty fallback data
|
||||||
|
earnings_data = {
|
||||||
|
'payments': [],
|
||||||
|
'total_payments': 0,
|
||||||
|
'total_paid_btc': 0,
|
||||||
|
'total_paid_sats': 0,
|
||||||
|
'total_paid_usd': 0,
|
||||||
|
'unpaid_earnings': 0,
|
||||||
|
'unpaid_earnings_sats': 0,
|
||||||
|
'est_time_to_payout': 'Unknown',
|
||||||
|
'monthly_summaries': [],
|
||||||
|
'timestamp': datetime.now(ZoneInfo(user_timezone)).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add notification about timeout
|
||||||
|
notification_service.add_notification(
|
||||||
|
"Data fetch timeout",
|
||||||
|
"Unable to fetch payment history data from Ocean.xyz. Showing limited earnings data.",
|
||||||
|
NotificationLevel.WARNING,
|
||||||
|
NotificationCategory.DATA
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching earnings data: {e}")
|
||||||
|
# Create empty fallback data
|
||||||
|
earnings_data = {
|
||||||
|
'payments': [],
|
||||||
|
'total_payments': 0,
|
||||||
|
'total_paid_btc': 0,
|
||||||
|
'total_paid_sats': 0,
|
||||||
|
'total_paid_usd': 0,
|
||||||
|
'unpaid_earnings': 0,
|
||||||
|
'unpaid_earnings_sats': 0,
|
||||||
|
'est_time_to_payout': 'Unknown',
|
||||||
|
'monthly_summaries': [],
|
||||||
|
'error': str(e),
|
||||||
|
'timestamp': datetime.now(ZoneInfo(user_timezone)).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add notification about error
|
||||||
|
notification_service.add_notification(
|
||||||
|
"Error fetching earnings data",
|
||||||
|
f"Error: {str(e)}",
|
||||||
|
NotificationLevel.ERROR,
|
||||||
|
NotificationCategory.DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
# Convert USD values to user's preferred currency if needed
|
||||||
|
if user_currency != 'USD' and earnings_data:
|
||||||
|
# Get exchange rate
|
||||||
|
try:
|
||||||
|
exchange_rates = dashboard_service.fetch_exchange_rates()
|
||||||
|
exchange_rate = exchange_rates.get(user_currency, 1.0)
|
||||||
|
|
||||||
|
# Total paid conversion
|
||||||
|
if 'total_paid_usd' in earnings_data:
|
||||||
|
earnings_data['total_paid_fiat'] = earnings_data['total_paid_usd'] * exchange_rate
|
||||||
|
|
||||||
|
# Monthly summaries conversion
|
||||||
|
if 'monthly_summaries' in earnings_data:
|
||||||
|
for month in earnings_data['monthly_summaries']:
|
||||||
|
if 'total_usd' in month:
|
||||||
|
month['total_fiat'] = month['total_usd'] * exchange_rate
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error converting currency: {e}")
|
||||||
|
# Set fiat values equal to USD as fallback
|
||||||
|
if 'total_paid_usd' in earnings_data:
|
||||||
|
earnings_data['total_paid_fiat'] = earnings_data['total_paid_usd']
|
||||||
|
|
||||||
|
if 'monthly_summaries' in earnings_data:
|
||||||
|
for month in earnings_data['monthly_summaries']:
|
||||||
|
if 'total_usd' in month:
|
||||||
|
month['total_fiat'] = month['total_usd']
|
||||||
|
else:
|
||||||
|
# If currency is USD, just copy USD values
|
||||||
|
if earnings_data:
|
||||||
|
if 'total_paid_usd' in earnings_data:
|
||||||
|
earnings_data['total_paid_fiat'] = earnings_data['total_paid_usd']
|
||||||
|
|
||||||
|
if 'monthly_summaries' in earnings_data:
|
||||||
|
for month in earnings_data['monthly_summaries']:
|
||||||
|
if 'total_usd' in month:
|
||||||
|
month['total_fiat'] = month['total_usd']
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'earnings.html',
|
||||||
|
earnings=earnings_data,
|
||||||
|
user_currency=user_currency,
|
||||||
|
user_timezone=user_timezone,
|
||||||
|
currency_symbols=currency_symbols,
|
||||||
|
current_time=datetime.now(ZoneInfo(user_timezone)).strftime("%b %d, %Y %I:%M:%S %p")
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error rendering earnings page: {e}")
|
||||||
|
import traceback
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
return render_template("error.html", message="Failed to load earnings data. Please try again later."), 500
|
||||||
|
|
||||||
# Add the middleware
|
# Add the middleware
|
||||||
app.wsgi_app = RobustMiddleware(app.wsgi_app)
|
app.wsgi_app = RobustMiddleware(app.wsgi_app)
|
||||||
|
|
||||||
|
32
README.md
32
README.md
@ -46,6 +46,11 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
|||||||
- **Block Details**: Examine transaction counts, fees, and mining pool information
|
- **Block Details**: Examine transaction counts, fees, and mining pool information
|
||||||
- **Visual Indicators**: Track network difficulty and block discovery times
|
- **Visual Indicators**: Track network difficulty and block discovery times
|
||||||
|
|
||||||
|
### Earnings Page
|
||||||
|
- **Detailed Earnings Breakdown**: View earnings by time period (daily, weekly, monthly)
|
||||||
|
- **Currency Conversion**: Automatically convert earnings to your preferred fiat currency
|
||||||
|
- **Historical Data**: Access past earnings data for analysis
|
||||||
|
|
||||||
### System Resilience
|
### System Resilience
|
||||||
- **Connection Recovery**: Automatic reconnection after network interruptions
|
- **Connection Recovery**: Automatic reconnection after network interruptions
|
||||||
- **Backup Polling**: Fallback to traditional polling if real-time connection fails
|
- **Backup Polling**: Fallback to traditional polling if real-time connection fails
|
||||||
@ -145,6 +150,11 @@ For more details, refer to the [docker-compose documentation](https://docs.docke
|
|||||||
- Search and filtering functionality
|
- Search and filtering functionality
|
||||||
- Performance trend mini-charts
|
- Performance trend mini-charts
|
||||||
|
|
||||||
|
### Earnings Page
|
||||||
|
- Detailed earnings breakdown by time period
|
||||||
|
- Currency conversion for earnings in selected fiat
|
||||||
|
- Historical data for earnings analysis
|
||||||
|
|
||||||
### Blocks Explorer
|
### Blocks Explorer
|
||||||
|
|
||||||
- Recent block visualization with mining details
|
- Recent block visualization with mining details
|
||||||
@ -152,6 +162,10 @@ For more details, refer to the [docker-compose documentation](https://docs.docke
|
|||||||
- Mining pool attribution
|
- Mining pool attribution
|
||||||
- Block details modal with comprehensive data
|
- Block details modal with comprehensive data
|
||||||
|
|
||||||
|
### Notifications
|
||||||
|
- Real-time alerts for important events
|
||||||
|
- Notification history with read/unread status
|
||||||
|
|
||||||
### System Monitor
|
### System Monitor
|
||||||
|
|
||||||
- Floating interface providing system statistics
|
- Floating interface providing system statistics
|
||||||
@ -184,7 +198,19 @@ Built with a modern stack for reliability and performance:
|
|||||||
- `/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/notifications`: Manages notifications for the user.
|
||||||
- `/api/workers`: Manages worker data and status.
|
- `/api/workers`: Manages worker data and status.
|
||||||
- `/api/exchange_rates`: Fetches real-time exchange rates for supported currencies.
|
- `api/time`: Returns the current server time.
|
||||||
|
- `api/timezone`: Returns the current timezone.
|
||||||
|
- `api/scheduler-health`: Returns the health status of the scheduler.
|
||||||
|
- `api/fix-scheduler`: Fixes the scheduler if it is not running.
|
||||||
|
- `api/force-refresh`: Forces a refresh of the data.
|
||||||
|
- `api/reset-chart-data`: Resets the chart data.
|
||||||
|
- `api/memory-profile`: Returns the memory profile of the application.
|
||||||
|
- `api/memory-history`: Returns the memory history of the application.
|
||||||
|
- `api/force-gc`: Forces garbage collection to free up memory.
|
||||||
|
- `api/notifications/clear`: Clears all notifications.
|
||||||
|
- `api/notifications/delete`: Deletes a specific notification.
|
||||||
|
- `api/notifications/mark_read`: Marks a notification as read.
|
||||||
|
- `api/notifications/unread_count`: Returns the count of unread notifications.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
@ -206,6 +232,7 @@ DeepSea-Dashboard/
|
|||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
├── Dockerfile # Docker configuration
|
├── Dockerfile # Docker configuration
|
||||||
├── docker-compose.yml # Docker Compose configuration
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
|
DeepSea-Dashboard/
|
||||||
│
|
│
|
||||||
├── templates/ # HTML templates
|
├── templates/ # HTML templates
|
||||||
│ ├── base.html # Base template with common elements
|
│ ├── base.html # Base template with common elements
|
||||||
@ -214,6 +241,7 @@ DeepSea-Dashboard/
|
|||||||
│ ├── workers.html # Workers dashboard template
|
│ ├── workers.html # Workers dashboard template
|
||||||
│ ├── blocks.html # Bitcoin blocks template
|
│ ├── blocks.html # Bitcoin blocks template
|
||||||
│ ├── notifications.html # Notifications template
|
│ ├── notifications.html # Notifications template
|
||||||
|
│ ├── earnings.html # Earnings page template
|
||||||
│ └── error.html # Error page template
|
│ └── error.html # Error page template
|
||||||
│
|
│
|
||||||
├── static/ # Static assets
|
├── static/ # Static assets
|
||||||
@ -224,6 +252,7 @@ DeepSea-Dashboard/
|
|||||||
│ │ ├── boot.css # Boot sequence styles
|
│ │ ├── boot.css # Boot sequence styles
|
||||||
│ │ ├── blocks.css # Blocks page styles
|
│ │ ├── blocks.css # Blocks page styles
|
||||||
│ │ ├── notifications.css # Notifications page styles
|
│ │ ├── notifications.css # Notifications page styles
|
||||||
|
│ │ ├── earnings.css # Earnings page styles
|
||||||
│ │ ├── error.css # Error page styles
|
│ │ ├── error.css # Error page styles
|
||||||
│ │ ├── retro-refresh.css # Floating refresh bar styles
|
│ │ ├── retro-refresh.css # Floating refresh bar styles
|
||||||
│ │ └── theme-toggle.css # Theme toggle styles
|
│ │ └── theme-toggle.css # Theme toggle styles
|
||||||
@ -233,6 +262,7 @@ DeepSea-Dashboard/
|
|||||||
│ ├── workers.js # Workers page functionality
|
│ ├── workers.js # Workers page functionality
|
||||||
│ ├── blocks.js # Blocks page functionality
|
│ ├── blocks.js # Blocks page functionality
|
||||||
│ ├── notifications.js # Notifications functionality
|
│ ├── notifications.js # Notifications functionality
|
||||||
|
│ ├── earnings.js # Earnings page functionality
|
||||||
│ ├── block-animation.js # Block mining animation
|
│ ├── block-animation.js # Block mining animation
|
||||||
│ ├── BitcoinProgressBar.js # System monitor functionality
|
│ ├── BitcoinProgressBar.js # System monitor functionality
|
||||||
│ └── theme.js # Theme toggle functionality
|
│ └── theme.js # Theme toggle functionality
|
||||||
|
@ -3,5 +3,6 @@
|
|||||||
"power_usage": 0.0,
|
"power_usage": 0.0,
|
||||||
"wallet": "yourwallethere",
|
"wallet": "yourwallethere",
|
||||||
"timezone": "America/Los_Angeles",
|
"timezone": "America/Los_Angeles",
|
||||||
"network_fee": 0.0
|
"network_fee": 0.0,
|
||||||
|
"currency": "USD"
|
||||||
}
|
}
|
||||||
|
27
config.py
27
config.py
@ -7,7 +7,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
# Default configuration file path
|
# Default configuration file path
|
||||||
CONFIG_FILE = "config.json"
|
CONFIG_FILE = "data/config.json"
|
||||||
|
|
||||||
def load_config():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
@ -18,7 +18,8 @@ def load_config():
|
|||||||
"power_usage": 0.0,
|
"power_usage": 0.0,
|
||||||
"wallet": "yourwallethere",
|
"wallet": "yourwallethere",
|
||||||
"timezone": "America/Los_Angeles",
|
"timezone": "America/Los_Angeles",
|
||||||
"network_fee": 0.0 # Add default network fee
|
"network_fee": 0.0, # Add default network fee
|
||||||
|
"currency": "USD" # Default currency
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.path.exists(CONFIG_FILE):
|
if os.path.exists(CONFIG_FILE):
|
||||||
@ -94,3 +95,25 @@ def get_value(key, default=None):
|
|||||||
"""
|
"""
|
||||||
config = load_config()
|
config = load_config()
|
||||||
return config.get(key, default)
|
return config.get(key, default)
|
||||||
|
|
||||||
|
def get_currency():
|
||||||
|
"""
|
||||||
|
Get the configured currency with fallback to default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Currency code (e.g., 'USD', 'EUR', etc.)
|
||||||
|
"""
|
||||||
|
# First check environment variable (for Docker)
|
||||||
|
import os
|
||||||
|
env_currency = os.environ.get("CURRENCY")
|
||||||
|
if env_currency:
|
||||||
|
return env_currency
|
||||||
|
|
||||||
|
# Then check config file
|
||||||
|
config = load_config()
|
||||||
|
currency = config.get("currency")
|
||||||
|
if currency:
|
||||||
|
return currency
|
||||||
|
|
||||||
|
# Default to USD
|
||||||
|
return "USD"
|
||||||
|
287
data_service.py
287
data_service.py
@ -1,4 +1,4 @@
|
|||||||
"""
|
"""
|
||||||
Data service module for fetching and processing mining data.
|
Data service module for fetching and processing mining data.
|
||||||
"""
|
"""
|
||||||
import logging
|
import logging
|
||||||
@ -489,6 +489,291 @@ class MiningDashboardService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error fetching exchange rates: {e}")
|
logging.error(f"Error fetching exchange rates: {e}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
|
def get_payment_history(self, max_pages=5, timeout=30, max_retries=3):
|
||||||
|
"""
|
||||||
|
Get payment history data from Ocean.xyz with retry logic.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_pages (int): Maximum number of pages to fetch
|
||||||
|
timeout (int): Timeout in seconds for each request
|
||||||
|
max_retries (int): Maximum number of retry attempts
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: List of payment history records
|
||||||
|
"""
|
||||||
|
logging.info(f"Fetching payment history data for wallet: {self.wallet}")
|
||||||
|
|
||||||
|
base_url = "https://ocean.xyz"
|
||||||
|
stats_url = f"{base_url}/stats/{self.wallet}"
|
||||||
|
headers = {
|
||||||
|
'User-Agent': 'Mozilla/5.0',
|
||||||
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||||
|
'Cache-Control': 'no-cache'
|
||||||
|
}
|
||||||
|
|
||||||
|
all_payments = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Start with the main page
|
||||||
|
page_num = 0
|
||||||
|
|
||||||
|
while page_num < max_pages:
|
||||||
|
url = f"{stats_url}?ppage={page_num}#payouts-fulltable"
|
||||||
|
logging.info(f"Fetching payment history from: {url} (page {page_num+1} of max {max_pages})")
|
||||||
|
|
||||||
|
# Add retry logic
|
||||||
|
retry_count = 0
|
||||||
|
while retry_count < max_retries:
|
||||||
|
try:
|
||||||
|
response = self.session.get(url, headers=headers, timeout=timeout)
|
||||||
|
if response.ok:
|
||||||
|
break # Success, exit retry loop
|
||||||
|
else:
|
||||||
|
logging.warning(f"Got status code {response.status_code} fetching page {page_num}, retry {retry_count+1}/{max_retries}")
|
||||||
|
retry_count += 1
|
||||||
|
time.sleep(1) # Wait before retrying
|
||||||
|
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
||||||
|
retry_count += 1
|
||||||
|
logging.warning(f"Request timeout or connection error on page {page_num}, retry {retry_count}/{max_retries}: {e}")
|
||||||
|
if retry_count >= max_retries:
|
||||||
|
logging.error(f"Max retries reached for page {page_num}")
|
||||||
|
if page_num == 0:
|
||||||
|
# If we can't even get the first page, return empty list
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
# If we got some data but hit timeout on later pages, just return what we have
|
||||||
|
logging.info(f"Returning {len(all_payments)} payments collected so far")
|
||||||
|
return all_payments
|
||||||
|
time.sleep(2) # Wait longer between retries
|
||||||
|
|
||||||
|
# If we've exhausted all retries and still failed, break out of the loop
|
||||||
|
if retry_count >= max_retries and (not 'response' in locals() or not response.ok):
|
||||||
|
logging.error(f"Failed to fetch payment history page {page_num} after {max_retries} retries")
|
||||||
|
break
|
||||||
|
|
||||||
|
soup = BeautifulSoup(response.text, 'html.parser')
|
||||||
|
|
||||||
|
# Find the payments table
|
||||||
|
payments_table = soup.find('tbody', id='payouts-tablerows')
|
||||||
|
if not payments_table:
|
||||||
|
logging.warning(f"No payment table found on page {page_num}")
|
||||||
|
if page_num == 0:
|
||||||
|
# If we can't find the table on the first page, something is wrong
|
||||||
|
logging.error("Payment history table not found on main page")
|
||||||
|
return []
|
||||||
|
else:
|
||||||
|
# If we found payments before but not on this page, we've reached the end
|
||||||
|
break
|
||||||
|
|
||||||
|
# Find all payment rows
|
||||||
|
payment_rows = payments_table.find_all('tr', class_='table-row')
|
||||||
|
if not payment_rows:
|
||||||
|
logging.warning(f"No payment rows found on page {page_num}")
|
||||||
|
break
|
||||||
|
|
||||||
|
logging.info(f"Found {len(payment_rows)} payment records on page {page_num}")
|
||||||
|
|
||||||
|
# Process each payment row
|
||||||
|
for row in payment_rows:
|
||||||
|
cells = row.find_all('td', class_='table-cell')
|
||||||
|
|
||||||
|
# Skip rows with insufficient data
|
||||||
|
if len(cells) < 3:
|
||||||
|
logging.warning(f"Payment row has too few cells: {len(cells)}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Initialize payment record
|
||||||
|
payment = {
|
||||||
|
"date": "",
|
||||||
|
"txid": "",
|
||||||
|
"amount_btc": 0.0,
|
||||||
|
"amount_sats": 0,
|
||||||
|
"status": "confirmed" # Default status
|
||||||
|
}
|
||||||
|
|
||||||
|
# Extract date from the first cell
|
||||||
|
date_cell = cells[0]
|
||||||
|
date_div = date_cell.find('div', class_='date-text')
|
||||||
|
if date_div:
|
||||||
|
payment["date"] = date_div.get_text(strip=True)
|
||||||
|
else:
|
||||||
|
payment["date"] = date_cell.get_text(strip=True)
|
||||||
|
|
||||||
|
# Extract transaction ID from the second cell
|
||||||
|
tx_cell = cells[1]
|
||||||
|
tx_link = tx_cell.find('a')
|
||||||
|
if tx_link and tx_link.has_attr('href'):
|
||||||
|
tx_href = tx_link['href']
|
||||||
|
# Extract transaction ID from the href
|
||||||
|
txid_match = re.search(r'/tx/([a-zA-Z0-9]+)', tx_href)
|
||||||
|
if txid_match:
|
||||||
|
payment["txid"] = txid_match.group(1)
|
||||||
|
else:
|
||||||
|
# If we can't extract from href, use the text but check for truncation
|
||||||
|
tx_text = tx_link.get_text(strip=True).replace('⛏️', '').strip()
|
||||||
|
# Check if this looks like a truncated TXID with ellipsis
|
||||||
|
if '...' in tx_text:
|
||||||
|
# Don't use truncated TXIDs as they're incomplete
|
||||||
|
payment["txid"] = ""
|
||||||
|
payment["truncated_txid"] = tx_text
|
||||||
|
else:
|
||||||
|
# Only use text content if it's a complete TXID (should be ~64 chars for BTC)
|
||||||
|
payment["txid"] = tx_text
|
||||||
|
|
||||||
|
# Extract BTC amount from the third cell
|
||||||
|
amount_cell = cells[2]
|
||||||
|
amount_text = amount_cell.get_text(strip=True)
|
||||||
|
# Extract numeric amount from text like "0.01093877 BTC"
|
||||||
|
amount_match = re.search(r'([\d\.]+)', amount_text)
|
||||||
|
if amount_match:
|
||||||
|
try:
|
||||||
|
payment["amount_btc"] = float(amount_match.group(1))
|
||||||
|
payment["amount_sats"] = int(round(payment["amount_btc"] * self.sats_per_btc))
|
||||||
|
except ValueError as e:
|
||||||
|
logging.warning(f"Could not parse payment amount '{amount_text}': {e}")
|
||||||
|
|
||||||
|
# Convert date string to ISO format if possible
|
||||||
|
try:
|
||||||
|
parsed_date = datetime.strptime(payment["date"], "%Y-%m-%d %H:%M")
|
||||||
|
utc_date = parsed_date.replace(tzinfo=ZoneInfo("UTC"))
|
||||||
|
local_date = utc_date.astimezone(ZoneInfo(get_timezone()))
|
||||||
|
payment["date_iso"] = local_date.isoformat()
|
||||||
|
except Exception as e:
|
||||||
|
logging.warning(f"Could not parse payment date '{payment['date']}': {e}")
|
||||||
|
payment["date_iso"] = None
|
||||||
|
|
||||||
|
all_payments.append(payment)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error processing payment row: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If we have no payments after processing the first page, stop
|
||||||
|
if page_num == 0 and not all_payments:
|
||||||
|
logging.warning("No payment data could be extracted from the first page")
|
||||||
|
break
|
||||||
|
|
||||||
|
# Move to the next page
|
||||||
|
page_num += 1
|
||||||
|
|
||||||
|
logging.info(f"Retrieved {len(all_payments)} payment records across {page_num} pages")
|
||||||
|
return all_payments
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching payment history: {e}")
|
||||||
|
import traceback
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_earnings_data(self):
|
||||||
|
"""
|
||||||
|
Get comprehensive earnings data from Ocean.xyz with improved error handling.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Earnings data including payment history and statistics
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Get the payment history with longer timeout and retries
|
||||||
|
payments = self.get_payment_history(max_pages=30, timeout=20, max_retries=3)
|
||||||
|
|
||||||
|
# Get basic Ocean data for summary metrics (with timeout handling)
|
||||||
|
try:
|
||||||
|
ocean_data = self.get_ocean_data()
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching ocean data for earnings: {e}")
|
||||||
|
ocean_data = None
|
||||||
|
|
||||||
|
# Calculate summary statistics
|
||||||
|
total_paid = sum(payment["amount_btc"] for payment in payments)
|
||||||
|
total_paid_sats = sum(payment["amount_sats"] for payment in payments)
|
||||||
|
|
||||||
|
# Get the latest BTC price with fallback
|
||||||
|
try:
|
||||||
|
_, _, btc_price, _ = self.get_bitcoin_stats()
|
||||||
|
if not btc_price:
|
||||||
|
btc_price = 75000 # Default value if fetch fails
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error getting BTC price: {e}")
|
||||||
|
btc_price = 75000 # Default value
|
||||||
|
|
||||||
|
# Calculate USD value
|
||||||
|
total_paid_usd = round(total_paid * btc_price, 2)
|
||||||
|
|
||||||
|
# Organize payments by month
|
||||||
|
payments_by_month = {}
|
||||||
|
for payment in payments:
|
||||||
|
if payment.get("date_iso"):
|
||||||
|
try:
|
||||||
|
month_key = payment["date_iso"][:7] # YYYY-MM format
|
||||||
|
month_name = datetime.strptime(month_key, "%Y-%m").strftime("%B %Y")
|
||||||
|
|
||||||
|
if month_key not in payments_by_month:
|
||||||
|
payments_by_month[month_key] = {
|
||||||
|
"month": month_key,
|
||||||
|
"month_name": month_name,
|
||||||
|
"payments": [],
|
||||||
|
"total_btc": 0.0,
|
||||||
|
"total_sats": 0,
|
||||||
|
"total_usd": 0.0
|
||||||
|
}
|
||||||
|
payments_by_month[month_key]["payments"].append(payment)
|
||||||
|
payments_by_month[month_key]["total_btc"] += payment["amount_btc"]
|
||||||
|
payments_by_month[month_key]["total_sats"] += payment["amount_sats"]
|
||||||
|
payments_by_month[month_key]["total_usd"] = round(
|
||||||
|
payments_by_month[month_key]["total_btc"] * btc_price, 2
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error processing payment for monthly grouping: {e}")
|
||||||
|
|
||||||
|
# Convert to list and sort by month (newest first)
|
||||||
|
monthly_summaries = list(payments_by_month.values())
|
||||||
|
monthly_summaries.sort(key=lambda x: x["month"], reverse=True)
|
||||||
|
|
||||||
|
# Calculate additional statistics
|
||||||
|
avg_payment = total_paid / len(payments) if payments else 0
|
||||||
|
avg_payment_sats = int(round(avg_payment * self.sats_per_btc)) if avg_payment else 0
|
||||||
|
|
||||||
|
# Get unpaid earnings from Ocean data
|
||||||
|
unpaid_earnings = ocean_data.unpaid_earnings if ocean_data else None
|
||||||
|
unpaid_earnings_sats = int(round(unpaid_earnings * self.sats_per_btc)) if unpaid_earnings is not None else None
|
||||||
|
|
||||||
|
# Create result dictionary
|
||||||
|
result = {
|
||||||
|
"payments": payments,
|
||||||
|
"total_payments": len(payments),
|
||||||
|
"total_paid_btc": total_paid,
|
||||||
|
"total_paid_sats": total_paid_sats,
|
||||||
|
"total_paid_usd": total_paid_usd,
|
||||||
|
"avg_payment_btc": avg_payment,
|
||||||
|
"avg_payment_sats": avg_payment_sats,
|
||||||
|
"btc_price": btc_price,
|
||||||
|
"monthly_summaries": monthly_summaries,
|
||||||
|
"unpaid_earnings": unpaid_earnings,
|
||||||
|
"unpaid_earnings_sats": unpaid_earnings_sats,
|
||||||
|
"est_time_to_payout": ocean_data.est_time_to_payout if ocean_data else None,
|
||||||
|
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add last payment date if available
|
||||||
|
if payments:
|
||||||
|
result["last_payment_date"] = payments[0]["date"]
|
||||||
|
result["last_payment_amount_btc"] = payments[0]["amount_btc"]
|
||||||
|
result["last_payment_amount_sats"] = payments[0]["amount_sats"]
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error fetching earnings data: {e}")
|
||||||
|
import traceback
|
||||||
|
logging.error(traceback.format_exc())
|
||||||
|
return {
|
||||||
|
"payments": [],
|
||||||
|
"total_payments": 0,
|
||||||
|
"error": str(e),
|
||||||
|
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
|
}
|
||||||
|
|
||||||
def get_bitcoin_stats(self):
|
def get_bitcoin_stats(self):
|
||||||
"""
|
"""
|
||||||
|
6
setup.py
6
setup.py
@ -61,6 +61,7 @@ FILE_MAPPINGS = {
|
|||||||
'blocks.css': 'static/css/blocks.css',
|
'blocks.css': 'static/css/blocks.css',
|
||||||
'notifications.css': 'static/css/notifications.css',
|
'notifications.css': 'static/css/notifications.css',
|
||||||
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
|
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
|
||||||
|
'earnings.css': 'static/css/earnings.css', # Added earnings.css
|
||||||
|
|
||||||
# JS files
|
# JS files
|
||||||
'main.js': 'static/js/main.js',
|
'main.js': 'static/js/main.js',
|
||||||
@ -69,6 +70,7 @@ FILE_MAPPINGS = {
|
|||||||
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
|
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
|
||||||
'notifications.js': 'static/js/notifications.js',
|
'notifications.js': 'static/js/notifications.js',
|
||||||
'theme.js': 'static/js/theme.js', # Added theme.js
|
'theme.js': 'static/js/theme.js', # Added theme.js
|
||||||
|
'earnings.js': 'static/js/earnings.js', # Added earnings.js
|
||||||
|
|
||||||
# Template files
|
# Template files
|
||||||
'base.html': 'templates/base.html',
|
'base.html': 'templates/base.html',
|
||||||
@ -78,6 +80,7 @@ FILE_MAPPINGS = {
|
|||||||
'error.html': 'templates/error.html',
|
'error.html': 'templates/error.html',
|
||||||
'blocks.html': 'templates/blocks.html',
|
'blocks.html': 'templates/blocks.html',
|
||||||
'notifications.html': 'templates/notifications.html',
|
'notifications.html': 'templates/notifications.html',
|
||||||
|
'earnings.html': 'templates/earnings.html', # Added earnings.html
|
||||||
}
|
}
|
||||||
|
|
||||||
# Default configuration
|
# Default configuration
|
||||||
@ -86,7 +89,8 @@ DEFAULT_CONFIG = {
|
|||||||
"power_usage": 0.0,
|
"power_usage": 0.0,
|
||||||
"wallet": "yourwallethere",
|
"wallet": "yourwallethere",
|
||||||
"timezone": "America/Los_Angeles", # Added default timezone
|
"timezone": "America/Los_Angeles", # Added default timezone
|
||||||
"network_fee": 0.0 # Added default network fee
|
"network_fee": 0.0, # Added default network fee
|
||||||
|
"currency": "USD"
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
|
@ -280,11 +280,12 @@ h1 {
|
|||||||
|
|
||||||
/* Container */
|
/* Container */
|
||||||
.container-fluid {
|
.container-fluid {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding-bottom: 6rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Status indicators */
|
/* Status indicators */
|
||||||
@ -482,6 +483,14 @@ h1 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 576px) {
|
||||||
|
.nav-link {
|
||||||
|
font-size: 0.8rem; /* Reduce font size for smaller screens */
|
||||||
|
padding: 3px 10px; /* Adjust padding to save space */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Navigation badges for notifications */
|
/* Navigation badges for notifications */
|
||||||
.nav-badge {
|
.nav-badge {
|
||||||
background-color: var(--primary-color);
|
background-color: var(--primary-color);
|
||||||
@ -495,3 +504,4 @@ h1 {
|
|||||||
margin-left: 5px;
|
margin-left: 5px;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -182,10 +182,6 @@
|
|||||||
box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);
|
box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
/* Add bottom padding to accommodate minimized system monitor */
|
|
||||||
.container-fluid {
|
|
||||||
padding-bottom: 100px !important; /* Enough space for minimized monitor */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Add these styles to dashboard.css */
|
/* Add these styles to dashboard.css */
|
||||||
@keyframes pulse-block-marker {
|
@keyframes pulse-block-marker {
|
||||||
@ -217,15 +213,283 @@
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rainbow Glitch animation for DATUM text */
|
||||||
|
@keyframes rainbow-shift {
|
||||||
|
0% {
|
||||||
|
color: #ff2a6d;
|
||||||
|
text-shadow: 2px 0 #05d9e8, -2px 0 #ff2a6d, 0 0 10px #ff2a6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
color: #ff2a6d;
|
||||||
|
text-shadow: 2px 0 #05d9e8, -2px 0 #ff2a6d, 0 0 10px #ff2a6d;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
color: #ff7f00;
|
||||||
|
text-shadow: -2px 0 #05d9e8, 2px 0 #ff7f00, 0 0 10px #ff7f00;
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
color: #d8ff00;
|
||||||
|
text-shadow: 2px 0 #05d9e8, -2px 0 #d8ff00, 0 0 10px #d8ff00;
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
color: #00ff80;
|
||||||
|
text-shadow: -2px 0 #ff2a6d, 2px 0 #00ff80, 0 0 10px #00ff80;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
color: #05d9e8;
|
||||||
|
text-shadow: 2px 0 #ff2a6d, -2px 0 #05d9e8, 0 0 10px #05d9e8;
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
color: #0000ff;
|
||||||
|
text-shadow: -2px 0 #ff2a6d, 2px 0 #0000ff, 0 0 10px #0000ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
color: #8000ff;
|
||||||
|
text-shadow: 2px 0 #05d9e8, -2px 0 #8000ff, 0 0 10px #8000ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
color: #ff00ff;
|
||||||
|
text-shadow: -2px 0 #05d9e8, 2px 0 #ff00ff, 0 0 10px #ff00ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
color: #ff007f;
|
||||||
|
text-shadow: 2px 0 #05d9e8, -2px 0 #ff007f, 0 0 10px #ff007f;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
color: #ff2a6d;
|
||||||
|
text-shadow: -2px 0 #05d9e8, 2px 0 #ff2a6d, 0 0 10px #ff2a6d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch-anim {
|
||||||
|
0% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
7% {
|
||||||
|
transform: skew(-0.5deg, -0.9deg);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
27% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
transform: skew(0.8deg, -0.1deg);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
35% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
52% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
55% {
|
||||||
|
transform: skew(-1deg, 0.2deg);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
72% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: skew(0.4deg, 1deg);
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-glow {
|
||||||
|
0% {
|
||||||
|
filter: brightness(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
filter: brightness(1.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
filter: brightness(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.datum-label {
|
.datum-label {
|
||||||
color: cyan; /* cyan color */
|
position: relative;
|
||||||
|
color: cyan;
|
||||||
font-size: 0.95em;
|
font-size: 0.95em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
padding: 2px 5px;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
|
display: inline-block;
|
||||||
|
animation: rainbow-shift 8s infinite linear, glitch-anim 3s infinite, pulse-glow 2s infinite;
|
||||||
|
transform-origin: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Create a glitch effect with duplicated text */
|
||||||
|
.datum-label::before,
|
||||||
|
.datum-label::after {
|
||||||
|
content: "DATUM";
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background-color: rgba(0, 0, 0, 0.3);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 5px;
|
||||||
|
clip: rect(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datum-label::before {
|
||||||
|
left: -1px;
|
||||||
|
animation: glitch-frames 2s infinite linear alternate-reverse;
|
||||||
|
text-shadow: -2px 0 #ff2a6d;
|
||||||
|
clip: rect(44px, 450px, 56px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.datum-label::after {
|
||||||
|
left: 1px;
|
||||||
|
animation: glitch-frames 3s infinite linear alternate-reverse;
|
||||||
|
text-shadow: 2px 0 #05d9e8;
|
||||||
|
clip: rect(24px, 450px, 36px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes glitch-frames {
|
||||||
|
0% {
|
||||||
|
clip: rect(19px, 450px, 23px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
5% {
|
||||||
|
clip: rect(36px, 450px, 16px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
10% {
|
||||||
|
clip: rect(11px, 450px, 41px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
15% {
|
||||||
|
clip: rect(22px, 450px, 33px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
clip: rect(9px, 450px, 47px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
clip: rect(31px, 450px, 21px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
30% {
|
||||||
|
clip: rect(44px, 450px, 9px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
35% {
|
||||||
|
clip: rect(17px, 450px, 38px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
clip: rect(26px, 450px, 25px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
45% {
|
||||||
|
clip: rect(12px, 450px, 43px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
clip: rect(35px, 450px, 18px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
55% {
|
||||||
|
clip: rect(8px, 450px, 49px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
clip: rect(29px, 450px, 23px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
65% {
|
||||||
|
clip: rect(42px, 450px, 11px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
70% {
|
||||||
|
clip: rect(15px, 450px, 40px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
clip: rect(24px, 450px, 27px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
clip: rect(10px, 450px, 45px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
85% {
|
||||||
|
clip: rect(33px, 450px, 20px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
90% {
|
||||||
|
clip: rect(46px, 450px, 7px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
95% {
|
||||||
|
clip: rect(13px, 450px, 42px, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
clip: rect(28px, 450px, 26px, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add some hover effects for interactivity */
|
||||||
|
.datum-label:hover {
|
||||||
|
animation-play-state: paused;
|
||||||
|
filter: brightness(1.5);
|
||||||
|
cursor: pointer;
|
||||||
|
transform: scale(1.1);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Pool luck indicators */
|
/* Pool luck indicators */
|
||||||
|
622
static/css/earnings.css
Normal file
622
static/css/earnings.css
Normal file
@ -0,0 +1,622 @@
|
|||||||
|
/* earnings.css - Clean theme-switching friendly version */
|
||||||
|
|
||||||
|
/* Color variables - Defined at root level for global access */
|
||||||
|
:root {
|
||||||
|
/* Shared colors for both themes */
|
||||||
|
--yellow-color: #ffd700;
|
||||||
|
--green-color: #32CD32;
|
||||||
|
--light-green-color: #90EE90;
|
||||||
|
--red-color: #ff5555;
|
||||||
|
--accent-color: #00dfff;
|
||||||
|
--accent-color-rgb: 0, 223, 255;
|
||||||
|
--bg-color: #000;
|
||||||
|
--card-header-bg: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Theme-specific variables */
|
||||||
|
html.bitcoin-theme {
|
||||||
|
--primary-color: #f2a900;
|
||||||
|
--primary-color-rgb: 242, 169, 0;
|
||||||
|
--border-color: #333;
|
||||||
|
--primary-gradient-end: #bf7d00; /* Darker orange */
|
||||||
|
}
|
||||||
|
|
||||||
|
html.deepsea-theme {
|
||||||
|
--primary-color: #0088cc;
|
||||||
|
--primary-color-rgb: 0, 136, 204;
|
||||||
|
--border-color: #224;
|
||||||
|
--primary-gradient-end: #006699; /* Darker blue */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== LAYOUT COMPONENTS ===== */
|
||||||
|
|
||||||
|
/* Main section styling */
|
||||||
|
.earnings-section {
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Section headers */
|
||||||
|
.earnings-section h2 {
|
||||||
|
color: white;
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--header-font);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Stats grid for summary cards */
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== CARD STYLING ===== */
|
||||||
|
|
||||||
|
/* Stat card - Blue background with white text */
|
||||||
|
.stat-card {
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.3);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card h2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-family: var(--header-font);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card content elements */
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-value {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-unit {
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-unit,
|
||||||
|
.stat-card .stat-secondary {
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-secondary {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-time {
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--accent-color, #00dfff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-card .stat-time {
|
||||||
|
color: var(--accent-color, #a0ffff);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== TABLE STYLING ===== */
|
||||||
|
|
||||||
|
.table-container {
|
||||||
|
overflow-x: auto;
|
||||||
|
margin: 1rem 0;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.2);
|
||||||
|
position: relative;
|
||||||
|
-webkit-overflow-scrolling: touch; /* Smooth scrolling on iOS */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline effect for tables */
|
||||||
|
.table-container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 2px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table th,
|
||||||
|
.earnings-table td {
|
||||||
|
padding: 0.75rem;
|
||||||
|
text-align: left;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table th {
|
||||||
|
background-color: var(--card-header-bg);
|
||||||
|
color: var(--primary-color);
|
||||||
|
font-family: var(--header-font);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Empty state message */
|
||||||
|
.earnings-table tr td[colspan] {
|
||||||
|
text-align: center;
|
||||||
|
padding: 2rem;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SPECIFIC DATA COLORS ===== */
|
||||||
|
|
||||||
|
/* Yellow color - BTC/sats values */
|
||||||
|
#unpaid-sats,
|
||||||
|
#total-paid-sats,
|
||||||
|
.earnings-table td:nth-child(4),
|
||||||
|
#unpaid-btc,
|
||||||
|
#total-paid-btc,
|
||||||
|
.earnings-table td:nth-child(3) {
|
||||||
|
color: var(--yellow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Green color - Earnings/profits */
|
||||||
|
#total-paid-usd,
|
||||||
|
#total-paid-fiat,
|
||||||
|
.earnings-table td:nth-child(5) {
|
||||||
|
color: var(--green-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Red color - Fees/costs */
|
||||||
|
.earnings-fee,
|
||||||
|
.earnings-cost {
|
||||||
|
color: var(--red-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Blue color - Dates */
|
||||||
|
.earnings-table td:nth-child(1) {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== MISC ELEMENTS ===== */
|
||||||
|
|
||||||
|
/* Transaction links */
|
||||||
|
.tx-link {
|
||||||
|
color: var(--accent-color);
|
||||||
|
text-decoration: none;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-link:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
text-shadow: 0 0 5px rgba(var(--accent-color-rgb), 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Status indicators */
|
||||||
|
.status-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: bold;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-confirmed {
|
||||||
|
background-color: rgba(75, 181, 67, 0.15);
|
||||||
|
color: var(--green-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-pending {
|
||||||
|
background-color: rgba(247, 147, 26, 0.15);
|
||||||
|
color: var(--yellow-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-processing {
|
||||||
|
background-color: rgba(52, 152, 219, 0.15);
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User settings info */
|
||||||
|
.settings-info {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-left: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item strong {
|
||||||
|
color: var(--accent-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Currency symbol */
|
||||||
|
.currency-symbol {
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== SPECIFIC ELEMENT STYLING ===== */
|
||||||
|
|
||||||
|
/* Payment count */
|
||||||
|
#payment-count {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Latest payment timestamp */
|
||||||
|
#latest-payment {
|
||||||
|
color: var(--accent-color);
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pool luck indicators */
|
||||||
|
.very-lucky {
|
||||||
|
color: var(--green-color) !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucky {
|
||||||
|
color: var(--light-green-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal-luck {
|
||||||
|
color: var(--yellow-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlucky {
|
||||||
|
color: var(--red-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== ANIMATIONS ===== */
|
||||||
|
|
||||||
|
/* Bounce animations */
|
||||||
|
@keyframes bounceUp {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes bounceDown {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
transform: translateY(2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce-up {
|
||||||
|
animation: bounceUp 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.bounce-down {
|
||||||
|
animation: bounceDown 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Pulse animation for new payments */
|
||||||
|
@keyframes pulse-highlight {
|
||||||
|
0% {
|
||||||
|
background-color: color-mix(in srgb, var(--primary-color) 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-color: color-mix(in srgb, var(--primary-color) 30%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-color: color-mix(in srgb, var(--primary-color) 10%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-payment {
|
||||||
|
animation: pulse-highlight 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== FUTURE CHART STYLING ===== */
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
height: 230px;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.2);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chart-container::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 2px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== UTILITY CLASSES ===== */
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metric value styling */
|
||||||
|
.metric-value {
|
||||||
|
color: var(--text-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Card body general styling */
|
||||||
|
.card-body strong {
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-body p {
|
||||||
|
margin: 0.25rem 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== RESPONSIVE STYLING ===== */
|
||||||
|
|
||||||
|
/* Tablets and smaller desktops */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.stats-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table th,
|
||||||
|
.earnings-table td {
|
||||||
|
padding: 0.5rem;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
padding: 0.15rem 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-value {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tx-link {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-info {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile phones */
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
/* Card-like table layout for small screens */
|
||||||
|
.earnings-table thead {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table,
|
||||||
|
.earnings-table tbody,
|
||||||
|
.earnings-table tr {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table tr {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table td {
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 3px;
|
||||||
|
border: none;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-table td:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive labels for table cells */
|
||||||
|
.earnings-table td::before {
|
||||||
|
content: attr(data-label);
|
||||||
|
font-weight: bold;
|
||||||
|
width: 40%;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-right: 5%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table-specific cell labels */
|
||||||
|
#payment-history-table td:nth-child(1)::before {
|
||||||
|
content: "Date: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#payment-history-table td:nth-child(2)::before {
|
||||||
|
content: "BTC: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#payment-history-table td:nth-child(3)::before {
|
||||||
|
content: "Sats: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#payment-history-table td:nth-child(4)::before {
|
||||||
|
content: "TX: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#payment-history-table td:nth-child(5)::before {
|
||||||
|
content: "Status: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#monthly-summary-table td:nth-child(1)::before {
|
||||||
|
content: "Month: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#monthly-summary-table td:nth-child(2)::before {
|
||||||
|
content: "Payments: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#monthly-summary-table td:nth-child(3)::before {
|
||||||
|
content: "BTC: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#monthly-summary-table td:nth-child(4)::before {
|
||||||
|
content: "Sats: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
#monthly-summary-table td:nth-child(5)::before {
|
||||||
|
content: "Fiat: ";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fix truncated transaction links */
|
||||||
|
.tx-link {
|
||||||
|
word-break: break-all;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add scanline effect to the main dashboard container */
|
||||||
|
.dashboard-container {
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scanline effect for the entire dashboard container */
|
||||||
|
.dashboard-container::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 2px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update stat card with scanline effect */
|
||||||
|
.stat-card {
|
||||||
|
color: white;
|
||||||
|
padding: 1rem;
|
||||||
|
border: 1px solid var(--primary-color);
|
||||||
|
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.3);
|
||||||
|
position: relative;
|
||||||
|
background-color: var(--bg-color);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add scanline effect to stat cards */
|
||||||
|
.stat-card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.1) 1px, transparent 1px, transparent 2px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure h2 headers in stat cards show above scanlines */
|
||||||
|
.stat-card h2 {
|
||||||
|
padding: 0.5rem;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
margin: -1rem -1rem 0.5rem -1rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
font-family: var(--header-font);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ensure content in stat cards shows above scanlines */
|
||||||
|
.stat-card .stat-value,
|
||||||
|
.stat-card .stat-secondary,
|
||||||
|
.stat-card .stat-time,
|
||||||
|
.stat-card .stat-unit {
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add scanline overlay to section headers */
|
||||||
|
.earnings-section h2 {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.earnings-section h2::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: repeating-linear-gradient(0deg, rgba(0, 0, 0, 0.05), rgba(0, 0, 0, 0.05) 1px, transparent 1px, transparent 2px);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
@ -20,8 +20,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-box:focus {
|
.search-box:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
|
box-shadow: 0 0 8px var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-button {
|
.filter-button {
|
||||||
@ -330,7 +330,7 @@
|
|||||||
}
|
}
|
||||||
/* Add extra padding at bottom of worker grid to avoid overlap */
|
/* Add extra padding at bottom of worker grid to avoid overlap */
|
||||||
.worker-grid {
|
.worker-grid {
|
||||||
margin-bottom: 120px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure summary stats have proper spacing on mobile */
|
/* Ensure summary stats have proper spacing on mobile */
|
||||||
|
169
static/js/earnings.js
Normal file
169
static/js/earnings.js
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
// earnings.js
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
console.log('Earnings page loaded');
|
||||||
|
|
||||||
|
// Add refresh functionality if needed
|
||||||
|
setupAutoRefresh();
|
||||||
|
|
||||||
|
// Format all currency values with commas
|
||||||
|
formatCurrencyValues();
|
||||||
|
|
||||||
|
// Apply user timezone formatting to all dates
|
||||||
|
applyUserTimezoneFormatting();
|
||||||
|
|
||||||
|
// Initialize the system monitor
|
||||||
|
initializeSystemMonitor();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize the BitcoinMinuteRefresh system monitor
|
||||||
|
function initializeSystemMonitor() {
|
||||||
|
// Define refresh function for the system monitor
|
||||||
|
window.manualRefresh = function () {
|
||||||
|
console.log("Manual refresh triggered by system monitor");
|
||||||
|
location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Initialize system monitor if it's available
|
||||||
|
if (typeof BitcoinMinuteRefresh !== 'undefined') {
|
||||||
|
// Get server time and initialize
|
||||||
|
fetchServerTimeAndInitializeMonitor();
|
||||||
|
} else {
|
||||||
|
console.warn("BitcoinMinuteRefresh not available");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch server time and initialize the monitor
|
||||||
|
function fetchServerTimeAndInitializeMonitor() {
|
||||||
|
fetch('/api/time')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.server_time) {
|
||||||
|
const serverTime = new Date(data.server_time).getTime();
|
||||||
|
const clientTime = Date.now();
|
||||||
|
const offset = serverTime - clientTime;
|
||||||
|
|
||||||
|
// Get server start time
|
||||||
|
fetch('/api/server-start')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(startData => {
|
||||||
|
if (startData && startData.start_time) {
|
||||||
|
const startTime = new Date(startData.start_time).getTime();
|
||||||
|
|
||||||
|
// Initialize the system monitor with server time info
|
||||||
|
if (typeof BitcoinMinuteRefresh !== 'undefined') {
|
||||||
|
BitcoinMinuteRefresh.initialize(window.manualRefresh);
|
||||||
|
BitcoinMinuteRefresh.updateServerTime(offset, startTime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching server start time:", error);
|
||||||
|
// Initialize with just time offset if server start time fails
|
||||||
|
if (typeof BitcoinMinuteRefresh !== 'undefined') {
|
||||||
|
BitcoinMinuteRefresh.initialize(window.manualRefresh);
|
||||||
|
BitcoinMinuteRefresh.updateServerTime(offset, Date.now() - 3600000); // fallback to 1 hour ago
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching server time:", error);
|
||||||
|
// Initialize without server time if API fails
|
||||||
|
if (typeof BitcoinMinuteRefresh !== 'undefined') {
|
||||||
|
BitcoinMinuteRefresh.initialize(window.manualRefresh);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format currency values with commas
|
||||||
|
function formatCurrency(amount) {
|
||||||
|
return amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format all currency values on the page
|
||||||
|
function formatCurrencyValues() {
|
||||||
|
// Format USD/fiat values in monthly summaries table
|
||||||
|
const monthlyFiatCells = document.querySelectorAll('.earnings-table td:nth-child(5)');
|
||||||
|
monthlyFiatCells.forEach(cell => {
|
||||||
|
const currencySymbol = cell.querySelector('.currency-symbol');
|
||||||
|
const symbol = currencySymbol ? currencySymbol.textContent : '';
|
||||||
|
|
||||||
|
// Remove symbol temporarily to parse the value
|
||||||
|
let valueText = cell.textContent;
|
||||||
|
if (currencySymbol) {
|
||||||
|
valueText = valueText.replace(symbol, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = parseFloat(valueText.replace(/[^\d.-]/g, ''));
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
// Keep the currency symbol and add commas to the number
|
||||||
|
cell.innerHTML = `<span class="currency-symbol">${symbol}</span>${formatCurrency(value.toFixed(2))}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format all sats values
|
||||||
|
const satsElements = document.querySelectorAll('#unpaid-sats, #total-paid-sats, .earnings-table td:nth-child(4)');
|
||||||
|
satsElements.forEach(element => {
|
||||||
|
if (element) {
|
||||||
|
const rawValue = element.textContent.replace(/,/g, '').trim();
|
||||||
|
if (!isNaN(parseInt(rawValue))) {
|
||||||
|
element.textContent = formatCurrency(parseInt(rawValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format payment count
|
||||||
|
const paymentCount = document.getElementById('payment-count');
|
||||||
|
if (paymentCount && !isNaN(parseInt(paymentCount.textContent))) {
|
||||||
|
paymentCount.textContent = formatCurrency(parseInt(paymentCount.textContent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupAutoRefresh() {
|
||||||
|
// Check if refresh is enabled in the UI
|
||||||
|
const refreshToggle = document.getElementById('refresh-toggle');
|
||||||
|
if (refreshToggle && refreshToggle.checked) {
|
||||||
|
// Set a refresh interval (e.g., every 5 minutes)
|
||||||
|
setInterval(function () {
|
||||||
|
location.reload();
|
||||||
|
}, 5 * 60 * 1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format BTC values
|
||||||
|
function formatBTC(btcValue) {
|
||||||
|
return parseFloat(btcValue).toFixed(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format sats with commas
|
||||||
|
function formatSats(satsValue) {
|
||||||
|
return formatCurrency(parseInt(satsValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format USD values with commas
|
||||||
|
function formatUSD(usdValue) {
|
||||||
|
return formatCurrency(parseFloat(usdValue).toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to apply user timezone formatting to dates
|
||||||
|
function applyUserTimezoneFormatting() {
|
||||||
|
// Store timezone for use by system monitor
|
||||||
|
window.dashboardTimezone = userTimezone || 'America/Los_Angeles';
|
||||||
|
|
||||||
|
// This function would format dates according to user timezone preference
|
||||||
|
// when dates are dynamically loaded or updated via JavaScript
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to format a timestamp based on user timezone
|
||||||
|
function formatDateToUserTimezone(timestamp) {
|
||||||
|
const timezone = window.userTimezone || 'America/Los_Angeles';
|
||||||
|
|
||||||
|
return new Date(timestamp).toLocaleString('en-US', {
|
||||||
|
timeZone: timezone,
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
}
|
@ -345,24 +345,41 @@ if (window['chartjs-plugin-annotation']) {
|
|||||||
|
|
||||||
// Hashrate Normalization Utilities
|
// Hashrate Normalization Utilities
|
||||||
// Enhanced normalizeHashrate function with better error handling for units
|
// Enhanced normalizeHashrate function with better error handling for units
|
||||||
function normalizeHashrate(value, unit) {
|
/**
|
||||||
if (!value || isNaN(value)) return 0;
|
* Normalizes hashrate values to TH/s (terahashes per second) for consistent comparison
|
||||||
|
* @param {number|string} value - The hashrate value to normalize
|
||||||
|
* @param {string} unit - The unit of the provided hashrate (e.g., 'ph/s', 'th/s', 'gh/s')
|
||||||
|
* @param {boolean} [debug=false] - Whether to output detailed debugging information
|
||||||
|
* @returns {number} - The normalized hashrate value in TH/s
|
||||||
|
*/
|
||||||
|
function normalizeHashrate(value, unit, debug = false) {
|
||||||
|
// Handle null, undefined, empty strings or non-numeric values
|
||||||
|
if (value === null || value === undefined || value === '' || isNaN(parseFloat(value))) {
|
||||||
|
if (debug) console.warn(`Invalid hashrate value: ${value}`);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Validate and normalize input value
|
// Convert to number and handle scientific notation (e.g., "1.23e+5")
|
||||||
value = parseFloat(value);
|
value = parseFloat(value);
|
||||||
|
|
||||||
// Standardize unit handling with a lookup table
|
// Standardize unit handling with a lookup table
|
||||||
const unit_normalized = (unit || 'th/s').toLowerCase().trim();
|
const unit_normalized = (unit || 'th/s').toLowerCase().trim();
|
||||||
|
|
||||||
|
// Store original values for logging
|
||||||
|
const originalValue = value;
|
||||||
|
const originalUnit = unit;
|
||||||
|
|
||||||
// Lookup table for conversion factors (all relative to TH/s)
|
// Lookup table for conversion factors (all relative to TH/s)
|
||||||
const unitConversions = {
|
const unitConversions = {
|
||||||
'ph/s': 1000,
|
// Zettahash (ZH/s) - 1 ZH/s = 1,000,000,000 TH/s
|
||||||
'p/s': 1000,
|
'zh/s': 1000000000,
|
||||||
'p': 1000,
|
'z/s': 1000000000,
|
||||||
'petahash': 1000,
|
'z': 1000000000,
|
||||||
'petahash/s': 1000,
|
'zettahash': 1000000000,
|
||||||
'peta': 1000,
|
'zettahash/s': 1000000000,
|
||||||
|
'zetta': 1000000000,
|
||||||
|
|
||||||
|
// Exahash (EH/s) - 1 EH/s = 1,000,000 TH/s
|
||||||
'eh/s': 1000000,
|
'eh/s': 1000000,
|
||||||
'e/s': 1000000,
|
'e/s': 1000000,
|
||||||
'e': 1000000,
|
'e': 1000000,
|
||||||
@ -370,6 +387,15 @@ function normalizeHashrate(value, unit) {
|
|||||||
'exahash/s': 1000000,
|
'exahash/s': 1000000,
|
||||||
'exa': 1000000,
|
'exa': 1000000,
|
||||||
|
|
||||||
|
// Petahash (PH/s) - 1 PH/s = 1,000 TH/s
|
||||||
|
'ph/s': 1000,
|
||||||
|
'p/s': 1000,
|
||||||
|
'p': 1000,
|
||||||
|
'petahash': 1000,
|
||||||
|
'petahash/s': 1000,
|
||||||
|
'peta': 1000,
|
||||||
|
|
||||||
|
// Terahash (TH/s) - Base unit
|
||||||
'th/s': 1,
|
'th/s': 1,
|
||||||
't/s': 1,
|
't/s': 1,
|
||||||
't': 1,
|
't': 1,
|
||||||
@ -377,6 +403,7 @@ function normalizeHashrate(value, unit) {
|
|||||||
'terahash/s': 1,
|
'terahash/s': 1,
|
||||||
'tera': 1,
|
'tera': 1,
|
||||||
|
|
||||||
|
// Gigahash (GH/s) - 1 TH/s = 1,000 GH/s
|
||||||
'gh/s': 1 / 1000,
|
'gh/s': 1 / 1000,
|
||||||
'g/s': 1 / 1000,
|
'g/s': 1 / 1000,
|
||||||
'g': 1 / 1000,
|
'g': 1 / 1000,
|
||||||
@ -384,6 +411,7 @@ function normalizeHashrate(value, unit) {
|
|||||||
'gigahash/s': 1 / 1000,
|
'gigahash/s': 1 / 1000,
|
||||||
'giga': 1 / 1000,
|
'giga': 1 / 1000,
|
||||||
|
|
||||||
|
// Megahash (MH/s) - 1 TH/s = 1,000,000 MH/s
|
||||||
'mh/s': 1 / 1000000,
|
'mh/s': 1 / 1000000,
|
||||||
'm/s': 1 / 1000000,
|
'm/s': 1 / 1000000,
|
||||||
'm': 1 / 1000000,
|
'm': 1 / 1000000,
|
||||||
@ -391,6 +419,7 @@ function normalizeHashrate(value, unit) {
|
|||||||
'megahash/s': 1 / 1000000,
|
'megahash/s': 1 / 1000000,
|
||||||
'mega': 1 / 1000000,
|
'mega': 1 / 1000000,
|
||||||
|
|
||||||
|
// Kilohash (KH/s) - 1 TH/s = 1,000,000,000 KH/s
|
||||||
'kh/s': 1 / 1000000000,
|
'kh/s': 1 / 1000000000,
|
||||||
'k/s': 1 / 1000000000,
|
'k/s': 1 / 1000000000,
|
||||||
'k': 1 / 1000000000,
|
'k': 1 / 1000000000,
|
||||||
@ -398,44 +427,72 @@ function normalizeHashrate(value, unit) {
|
|||||||
'kilohash/s': 1 / 1000000000,
|
'kilohash/s': 1 / 1000000000,
|
||||||
'kilo': 1 / 1000000000,
|
'kilo': 1 / 1000000000,
|
||||||
|
|
||||||
|
// Hash (H/s) - 1 TH/s = 1,000,000,000,000 H/s
|
||||||
'h/s': 1 / 1000000000000,
|
'h/s': 1 / 1000000000000,
|
||||||
'h': 1 / 1000000000000,
|
'h': 1 / 1000000000000,
|
||||||
'hash': 1 / 1000000000000,
|
'hash': 1 / 1000000000000,
|
||||||
'hash/s': 1 / 1000000000000
|
'hash/s': 1 / 1000000000000
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to find the conversion factor
|
|
||||||
let conversionFactor = null;
|
let conversionFactor = null;
|
||||||
|
let matchedUnit = null;
|
||||||
|
|
||||||
// First try direct lookup
|
// Direct lookup for exact matches
|
||||||
if (unitConversions.hasOwnProperty(unit_normalized)) {
|
if (unitConversions.hasOwnProperty(unit_normalized)) {
|
||||||
conversionFactor = unitConversions[unit_normalized];
|
conversionFactor = unitConversions[unit_normalized];
|
||||||
|
matchedUnit = unit_normalized;
|
||||||
} else {
|
} else {
|
||||||
// If direct lookup fails, try a fuzzy match
|
// Fuzzy matching for non-exact matches
|
||||||
for (const knownUnit in unitConversions) {
|
for (const knownUnit in unitConversions) {
|
||||||
if (unit_normalized.includes(knownUnit) || knownUnit.includes(unit_normalized)) {
|
if (unit_normalized.includes(knownUnit) || knownUnit.includes(unit_normalized)) {
|
||||||
// Log the unit correction for debugging
|
|
||||||
console.log(`Fuzzy matching unit: "${unit}" → interpreted as "${knownUnit}" (conversion: ${unitConversions[knownUnit]})`);
|
|
||||||
conversionFactor = unitConversions[knownUnit];
|
conversionFactor = unitConversions[knownUnit];
|
||||||
|
matchedUnit = knownUnit;
|
||||||
|
|
||||||
|
if (debug) {
|
||||||
|
console.log(`Fuzzy matching unit: "${unit}" → interpreted as "${knownUnit}" (conversion: ${unitConversions[knownUnit]})`);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If no conversion factor found, assume TH/s but log a warning
|
// Handle unknown units
|
||||||
if (conversionFactor === null) {
|
if (conversionFactor === null) {
|
||||||
console.warn(`Unrecognized hashrate unit: "${unit}", assuming TH/s. Value: ${value}`);
|
console.warn(`Unrecognized hashrate unit: "${unit}", assuming TH/s. Value: ${value}`);
|
||||||
// Add additional info to help diagnose incorrect units
|
|
||||||
|
// Automatically detect and suggest the appropriate unit based on magnitude
|
||||||
if (value > 1000) {
|
if (value > 1000) {
|
||||||
console.warn(`NOTE: Value ${value} is quite large for TH/s. Could it be PH/s?`);
|
console.warn(`NOTE: Value ${value} is quite large for TH/s. Could it be PH/s?`);
|
||||||
|
} else if (value > 1000000) {
|
||||||
|
console.warn(`NOTE: Value ${value} is extremely large for TH/s. Could it be EH/s?`);
|
||||||
} else if (value < 0.001) {
|
} else if (value < 0.001) {
|
||||||
console.warn(`NOTE: Value ${value} is quite small for TH/s. Could it be GH/s or MH/s?`);
|
console.warn(`NOTE: Value ${value} is quite small for TH/s. Could it be GH/s or MH/s?`);
|
||||||
}
|
}
|
||||||
return value; // Assume TH/s
|
|
||||||
|
// Assume TH/s as fallback
|
||||||
|
conversionFactor = 1;
|
||||||
|
matchedUnit = 'th/s';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply conversion and return normalized value
|
// Calculate normalized value
|
||||||
return value * conversionFactor;
|
const normalizedValue = value * conversionFactor;
|
||||||
|
|
||||||
|
// Log abnormally large conversions for debugging
|
||||||
|
if ((normalizedValue > 1000000 || normalizedValue < 0.000001) && normalizedValue !== 0) {
|
||||||
|
console.log(`Large scale conversion detected: ${originalValue} ${originalUnit} → ${normalizedValue.toExponential(2)} TH/s`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extra debugging for very large values to help track the Redis storage issue
|
||||||
|
if (debug && originalValue > 900000 && matchedUnit === 'th/s') {
|
||||||
|
console.group('High Hashrate Debug Info');
|
||||||
|
console.log(`Original: ${originalValue} ${originalUnit}`);
|
||||||
|
console.log(`Normalized: ${normalizedValue} TH/s`);
|
||||||
|
console.log(`Should be displayed as: ${(normalizedValue / 1000).toFixed(2)} PH/s`);
|
||||||
|
console.log('Call stack:', new Error().stack);
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
|
||||||
|
return normalizedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to format hashrate values for display
|
// Helper function to format hashrate values for display
|
||||||
@ -1306,183 +1363,294 @@ function updateChartWithNormalizedData(chart, data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process history data with enhanced validation and error handling
|
/**
|
||||||
|
* Process history data with comprehensive validation, unit normalization, and performance optimizations
|
||||||
|
* @param {Object} data - The metrics data containing hashrate history
|
||||||
|
* @param {Object} chart - The Chart.js chart instance to update
|
||||||
|
* @param {boolean} useHashrate3hr - Whether to use 3hr average data instead of 60sec data
|
||||||
|
* @param {number} normalizedAvg - The normalized 24hr average hashrate for reference
|
||||||
|
*/
|
||||||
if (data.arrow_history && data.arrow_history.hashrate_60sec) {
|
if (data.arrow_history && data.arrow_history.hashrate_60sec) {
|
||||||
// Validate history data
|
// Validate history data
|
||||||
try {
|
try {
|
||||||
// Log 60sec data
|
const perfStart = performance.now(); // Performance measurement
|
||||||
console.log("60sec history data received:", data.arrow_history.hashrate_60sec);
|
|
||||||
|
|
||||||
// Also log 3hr data if available
|
// Determine which history data to use (3hr or 60sec) with proper fallback
|
||||||
if (data.arrow_history.hashrate_3hr) {
|
let historyData;
|
||||||
console.log("3hr history data received:", data.arrow_history.hashrate_3hr);
|
let dataSource;
|
||||||
|
|
||||||
|
if (useHashrate3hr && data.arrow_history.hashrate_3hr && data.arrow_history.hashrate_3hr.length > 0) {
|
||||||
|
historyData = data.arrow_history.hashrate_3hr;
|
||||||
|
dataSource = "3hr";
|
||||||
|
chart.data.datasets[0].label = 'Hashrate Trend (3HR AVG)';
|
||||||
} else {
|
} else {
|
||||||
console.log("3hr history data not available in API response");
|
historyData = data.arrow_history.hashrate_60sec;
|
||||||
|
dataSource = "60sec";
|
||||||
|
chart.data.datasets[0].label = 'Hashrate Trend (60SEC AVG)';
|
||||||
|
|
||||||
|
// If we wanted 3hr data but it wasn't available, log a warning
|
||||||
|
if (useHashrate3hr) {
|
||||||
|
console.warn("3hr data requested but not available, falling back to 60sec data");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we're using 3hr average, try to use that history if available
|
console.log(`Using ${dataSource} history data with ${historyData?.length || 0} points`);
|
||||||
const historyData = useHashrate3hr && data.arrow_history.hashrate_3hr ?
|
|
||||||
data.arrow_history.hashrate_3hr : data.arrow_history.hashrate_60sec;
|
|
||||||
|
|
||||||
console.log("Using history data:", useHashrate3hr ? "3hr data" : "60sec data");
|
|
||||||
|
|
||||||
if (historyData && historyData.length > 0) {
|
if (historyData && historyData.length > 0) {
|
||||||
// Format time labels
|
// Pre-process history data to filter out invalid entries
|
||||||
chart.data.labels = historyData.map(item => {
|
const validHistoryData = historyData.filter(item => {
|
||||||
|
return item &&
|
||||||
|
(typeof item.value !== 'undefined') &&
|
||||||
|
!isNaN(parseFloat(item.value)) &&
|
||||||
|
(parseFloat(item.value) >= 0) &&
|
||||||
|
typeof item.time === 'string';
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validHistoryData.length < historyData.length) {
|
||||||
|
console.warn(`Filtered out ${historyData.length - validHistoryData.length} invalid data points`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validHistoryData.length === 0) {
|
||||||
|
console.warn("No valid history data points after filtering");
|
||||||
|
useSingleDataPoint();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format time labels more efficiently (do this once, not in a map callback)
|
||||||
|
const timeZone = dashboardTimezone || 'America/Los_Angeles';
|
||||||
|
const now = new Date();
|
||||||
|
const yearMonthDay = {
|
||||||
|
year: now.getFullYear(),
|
||||||
|
month: now.getMonth(),
|
||||||
|
day: now.getDate()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Create time formatter function with consistent options
|
||||||
|
const timeFormatter = new Intl.DateTimeFormat('en-US', {
|
||||||
|
timeZone: timeZone,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format all time labels at once
|
||||||
|
const formattedLabels = validHistoryData.map(item => {
|
||||||
const timeStr = item.time;
|
const timeStr = item.time;
|
||||||
try {
|
try {
|
||||||
// Parse and format the time (existing code)...
|
// Parse time efficiently
|
||||||
let timeParts;
|
let hours = 0, minutes = 0, seconds = 0;
|
||||||
|
|
||||||
if (timeStr.length === 8 && timeStr.indexOf(':') !== -1) {
|
if (timeStr.length === 8 && timeStr.indexOf(':') !== -1) {
|
||||||
// Format: HH:MM:SS
|
// Format: HH:MM:SS
|
||||||
timeParts = timeStr.split(':');
|
const parts = timeStr.split(':');
|
||||||
|
hours = parseInt(parts[0], 10);
|
||||||
|
minutes = parseInt(parts[1], 10);
|
||||||
|
seconds = parseInt(parts[2], 10);
|
||||||
} else if (timeStr.length === 5 && timeStr.indexOf(':') !== -1) {
|
} else if (timeStr.length === 5 && timeStr.indexOf(':') !== -1) {
|
||||||
// Format: HH:MM
|
// Format: HH:MM
|
||||||
timeParts = timeStr.split(':');
|
const parts = timeStr.split(':');
|
||||||
timeParts.push('00'); // Add seconds
|
hours = parseInt(parts[0], 10);
|
||||||
|
minutes = parseInt(parts[1], 10);
|
||||||
} else {
|
} else {
|
||||||
return timeStr; // Use as-is if format is unexpected
|
return timeStr; // Use original if format is unexpected
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format in 12-hour time with timezone support
|
// Create time date with validation
|
||||||
const now = new Date();
|
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) ||
|
||||||
const timeDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(),
|
hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) {
|
||||||
parseInt(timeParts[0]), parseInt(timeParts[1]), parseInt(timeParts[2] || 0));
|
return timeStr; // Use original for invalid times
|
||||||
|
}
|
||||||
|
|
||||||
let formattedTime = timeDate.toLocaleTimeString('en-US', {
|
const timeDate = new Date(yearMonthDay.year, yearMonthDay.month, yearMonthDay.day,
|
||||||
timeZone: dashboardTimezone || 'America/Los_Angeles',
|
hours, minutes, seconds);
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
});
|
|
||||||
|
|
||||||
// Remove the AM/PM part
|
// Format using the formatter
|
||||||
formattedTime = formattedTime.replace(/\s[AP]M$/i, '');
|
const formatted = timeFormatter.format(timeDate);
|
||||||
return formattedTime;
|
return formatted.replace(/\s[AP]M$/i, ''); // Remove AM/PM
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error formatting time:", e, timeStr);
|
console.error("Time formatting error:", e);
|
||||||
return timeStr; // Use original on error
|
return timeStr; // Use original on error
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Process and normalize hashrate values with validation
|
chart.data.labels = formattedLabels;
|
||||||
|
|
||||||
|
// Process and normalize hashrate values with validation (optimize by avoiding multiple iterations)
|
||||||
const hashrateValues = [];
|
const hashrateValues = [];
|
||||||
const validatedData = historyData.map((item, index) => {
|
const validatedData = new Array(validHistoryData.length);
|
||||||
|
|
||||||
|
// Enhanced unit validation
|
||||||
|
const validUnits = new Set(['th/s', 'ph/s', 'eh/s', 'gh/s', 'mh/s', 'zh/s']);
|
||||||
|
|
||||||
|
// Process all data points with error boundaries around each item
|
||||||
|
for (let i = 0; i < validHistoryData.length; i++) {
|
||||||
try {
|
try {
|
||||||
|
const item = validHistoryData[i];
|
||||||
|
|
||||||
// Safety conversion in case value is a string
|
// Safety conversion in case value is a string
|
||||||
const val = parseFloat(item.value || 0);
|
const val = parseFloat(item.value);
|
||||||
if (isNaN(val)) {
|
|
||||||
console.warn(`Invalid value at index ${index}: ${item.value}`);
|
// Get unit with better validation
|
||||||
return 0;
|
let unit = (item.unit || 'th/s').toLowerCase().trim();
|
||||||
|
|
||||||
|
// Use storeHashrateWithUnit to properly handle unit conversions for large values
|
||||||
|
// This increases chart precision by storing values in appropriate units
|
||||||
|
if (typeof window.storeHashrateWithUnit === 'function') {
|
||||||
|
// Use our specialized function if available
|
||||||
|
const storedFormat = window.storeHashrateWithUnit(val, unit);
|
||||||
|
const normalizedValue = normalizeHashrate(val, unit);
|
||||||
|
|
||||||
|
// Store the properly adjusted values for tooltip display
|
||||||
|
item.storageValue = storedFormat.value;
|
||||||
|
item.storageUnit = storedFormat.unit;
|
||||||
|
item.originalValue = val;
|
||||||
|
item.originalUnit = unit;
|
||||||
|
|
||||||
|
validatedData[i] = normalizedValue;
|
||||||
|
|
||||||
|
// Collect valid values for statistics
|
||||||
|
if (normalizedValue > 0) {
|
||||||
|
hashrateValues.push(normalizedValue);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Original approach if storeHashrateWithUnit isn't available
|
||||||
|
const normalizedValue = normalizeHashrate(val, unit);
|
||||||
|
|
||||||
|
// Store original values for tooltip reference
|
||||||
|
item.originalValue = val;
|
||||||
|
item.originalUnit = unit;
|
||||||
|
|
||||||
|
validatedData[i] = normalizedValue;
|
||||||
|
|
||||||
|
// Collect valid values for statistics
|
||||||
|
if (normalizedValue > 0) {
|
||||||
|
hashrateValues.push(normalizedValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate the unit
|
|
||||||
const unit = item.unit || 'th/s';
|
|
||||||
const normalizedValue = normalizeHashrate(val, unit);
|
|
||||||
|
|
||||||
// Collect valid values for statistics
|
|
||||||
if (normalizedValue > 0) {
|
|
||||||
hashrateValues.push(normalizedValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
return normalizedValue;
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(`Error processing hashrate at index ${index}:`, err);
|
console.error(`Error processing hashrate at index ${i}:`, err);
|
||||||
return 0;
|
validatedData[i] = 0; // Use 0 as a safe fallback
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
chart.data.datasets[0].data = validatedData;
|
|
||||||
|
|
||||||
// Calculate statistics for anomaly detection
|
|
||||||
if (hashrateValues.length > 1) {
|
|
||||||
const mean = hashrateValues.reduce((sum, val) => sum + val, 0) / hashrateValues.length;
|
|
||||||
const max = Math.max(...hashrateValues);
|
|
||||||
const min = Math.min(...hashrateValues);
|
|
||||||
|
|
||||||
// Check for outliers that might indicate incorrect units
|
|
||||||
if (max > mean * 10 || min < mean / 10) {
|
|
||||||
console.warn("WARNING: Wide hashrate variance detected in chart data. Possible unit inconsistency.");
|
|
||||||
console.warn(`Min: ${min.toFixed(2)} TH/s, Max: ${max.toFixed(2)} TH/s, Mean: ${mean.toFixed(2)} TH/s`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update chart dataset label to indicate which average we're displaying
|
// Assign the processed data to chart
|
||||||
chart.data.datasets[0].label = useHashrate3hr ?
|
chart.data.datasets[0].data = validatedData;
|
||||||
'Hashrate Trend (3HR AVG)' : 'Hashrate Trend (60SEC AVG)';
|
chart.originalData = validHistoryData; // Store for tooltip reference
|
||||||
|
|
||||||
// Calculate appropriate y-axis range with safeguards for outliers and ensure 24hr avg line is visible
|
// Update tooltip callback to display proper units
|
||||||
const values = chart.data.datasets[0].data.filter(v => !isNaN(v) && v !== null && v > 0);
|
chart.options.plugins.tooltip.callbacks.label = function (context) {
|
||||||
if (values.length > 0) {
|
const index = context.dataIndex;
|
||||||
const max = Math.max(...values);
|
const originalData = chart.originalData?.[index];
|
||||||
const min = Math.min(...values) || 0;
|
|
||||||
|
|
||||||
// MODIFICATION: When in low hashrate mode, ensure the y-axis includes the 24hr average
|
if (originalData) {
|
||||||
if (useHashrate3hr && normalizedAvg > 0) {
|
if (originalData.storageValue !== undefined && originalData.storageUnit) {
|
||||||
// Ensure the 24-hour average is visible on the chart
|
// Use the optimized storage value/unit if available
|
||||||
const yMin = Math.min(min * 0.8, normalizedAvg * 0.5);
|
return `HASHRATE: ${originalData.storageValue} ${originalData.storageUnit.toUpperCase()}`;
|
||||||
const yMax = Math.max(max * 1.2, normalizedAvg * 1.5);
|
}
|
||||||
|
else if (originalData.originalValue !== undefined && originalData.originalUnit) {
|
||||||
chart.options.scales.y.min = yMin;
|
// Fall back to original values
|
||||||
chart.options.scales.y.max = yMax;
|
return `HASHRATE: ${originalData.originalValue} ${originalData.originalUnit.toUpperCase()}`;
|
||||||
console.log(`Low hashrate mode: Adjusting y-axis to include 24hr avg: [${yMin.toFixed(2)}, ${yMax.toFixed(2)}]`);
|
|
||||||
} else {
|
|
||||||
// Normal mode scaling
|
|
||||||
chart.options.scales.y.min = min * 0.8;
|
|
||||||
chart.options.scales.y.max = max * 1.2;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set appropriate step size based on range
|
|
||||||
const range = chart.options.scales.y.max - chart.options.scales.y.min;
|
|
||||||
|
|
||||||
// Calculate an appropriate stepSize that won't exceed Chart.js tick limits
|
|
||||||
// Aim for approximately 5-10 ticks (Chart.js recommends ~5-6 ticks for readability)
|
|
||||||
let stepSize;
|
|
||||||
const targetTicks = 6; // Target number of ticks we want to display
|
|
||||||
|
|
||||||
if (range <= 0.1) {
|
|
||||||
// For very small ranges (< 0.1 TH/s)
|
|
||||||
stepSize = 0.01;
|
|
||||||
} else if (range <= 1) {
|
|
||||||
// For small ranges (0.1 - 1 TH/s)
|
|
||||||
stepSize = 0.1;
|
|
||||||
} else if (range <= 10) {
|
|
||||||
// For medium ranges (1 - 10 TH/s)
|
|
||||||
stepSize = 1;
|
|
||||||
} else if (range <= 50) {
|
|
||||||
stepSize = 5;
|
|
||||||
} else if (range <= 100) {
|
|
||||||
stepSize = 10;
|
|
||||||
} else if (range <= 500) {
|
|
||||||
stepSize = 50;
|
|
||||||
} else if (range <= 1000) {
|
|
||||||
stepSize = 100;
|
|
||||||
} else if (range <= 5000) {
|
|
||||||
stepSize = 500;
|
|
||||||
} else {
|
|
||||||
// For very large ranges, calculate stepSize that will produce ~targetTicks ticks
|
|
||||||
stepSize = Math.ceil(range / targetTicks);
|
|
||||||
|
|
||||||
// Round to a nice number (nearest power of 10 multiple)
|
|
||||||
const magnitude = Math.pow(10, Math.floor(Math.log10(stepSize)));
|
|
||||||
stepSize = Math.ceil(stepSize / magnitude) * magnitude;
|
|
||||||
|
|
||||||
// Safety check for extremely large ranges
|
|
||||||
if (range / stepSize > 1000) {
|
|
||||||
console.warn(`Y-axis range (${range.toFixed(2)}) requires extremely large stepSize.
|
|
||||||
Adjusting to limit ticks to 1000.`);
|
|
||||||
stepSize = Math.ceil(range / 1000);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Last resort fallback
|
||||||
|
return 'HASHRATE: ' + formatHashrateForDisplay(context.raw).toUpperCase();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate statistics for anomaly detection with optimization
|
||||||
|
if (hashrateValues.length > 1) {
|
||||||
|
// Calculate mean, min, max in a single pass for efficiency
|
||||||
|
let sum = 0, min = Infinity, max = -Infinity;
|
||||||
|
|
||||||
|
for (let i = 0; i < hashrateValues.length; i++) {
|
||||||
|
const val = hashrateValues[i];
|
||||||
|
sum += val;
|
||||||
|
if (val < min) min = val;
|
||||||
|
if (val > max) max = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mean = sum / hashrateValues.length;
|
||||||
|
|
||||||
|
// Enhanced outlier detection
|
||||||
|
const standardDeviation = calculateStandardDeviation(hashrateValues, mean);
|
||||||
|
const outlierThreshold = 3; // Standard deviations
|
||||||
|
|
||||||
|
// Check for outliers using both range and statistical methods
|
||||||
|
const hasOutliersByRange = (max > mean * 10 || min < mean / 10);
|
||||||
|
const hasOutliersByStats = hashrateValues.some(v => Math.abs(v - mean) > outlierThreshold * standardDeviation);
|
||||||
|
|
||||||
|
// Log more helpful diagnostics for outliers
|
||||||
|
if (hasOutliersByRange || hasOutliersByStats) {
|
||||||
|
console.warn("WARNING: Hashrate variance detected in chart data. Possible unit inconsistency.");
|
||||||
|
console.warn(`Stats: Min: ${min.toFixed(2)}, Max: ${max.toFixed(2)}, Mean: ${mean.toFixed(2)}, StdDev: ${standardDeviation.toFixed(2)} TH/s`);
|
||||||
|
|
||||||
|
// Give more specific guidance
|
||||||
|
if (max > 1000 && min < 10) {
|
||||||
|
console.warn("ADVICE: Data contains mixed units (likely TH/s and PH/s). Check API response consistency.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log performance timing for large datasets
|
||||||
|
if (hashrateValues.length > 100) {
|
||||||
|
const perfEnd = performance.now();
|
||||||
|
console.log(`Processed ${hashrateValues.length} hashrate points in ${(perfEnd - perfStart).toFixed(1)}ms`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find filtered valid values for y-axis limits (more efficient than creating a new array)
|
||||||
|
let activeValues = 0, yMin = Infinity, yMax = -Infinity;
|
||||||
|
for (let i = 0; i < validatedData.length; i++) {
|
||||||
|
const v = validatedData[i];
|
||||||
|
if (!isNaN(v) && v !== null && v > 0) {
|
||||||
|
activeValues++;
|
||||||
|
if (v < yMin) yMin = v;
|
||||||
|
if (v > yMax) yMax = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (activeValues > 0) {
|
||||||
|
// Optimized y-axis range calculation with padding
|
||||||
|
const padding = useHashrate3hr ? 0.5 : 0.2; // More padding in low hashrate mode
|
||||||
|
|
||||||
|
// When in low hashrate mode, ensure the y-axis includes the 24hr average
|
||||||
|
if (useHashrate3hr && normalizedAvg > 0) {
|
||||||
|
// Ensure the 24-hour average is visible with adequate padding
|
||||||
|
const minPadding = normalizedAvg * padding;
|
||||||
|
const maxPadding = normalizedAvg * padding;
|
||||||
|
|
||||||
|
chart.options.scales.y.min = Math.min(yMin * (1 - padding), normalizedAvg - minPadding);
|
||||||
|
chart.options.scales.y.max = Math.max(yMax * (1 + padding), normalizedAvg + maxPadding);
|
||||||
|
|
||||||
|
console.log(`Low hashrate mode: Y-axis range [${chart.options.scales.y.min.toFixed(2)}, ${chart.options.scales.y.max.toFixed(2)}] TH/s`);
|
||||||
|
} else {
|
||||||
|
// Normal mode scaling with smarter padding (less padding for large ranges)
|
||||||
|
const dynamicPadding = Math.min(0.2, 10 / yMax); // Reduce padding as max increases
|
||||||
|
chart.options.scales.y.min = Math.max(0, yMin * (1 - dynamicPadding)); // Never go below zero
|
||||||
|
chart.options.scales.y.max = yMax * (1 + dynamicPadding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set appropriate step size based on range - improved algorithm
|
||||||
|
const range = chart.options.scales.y.max - chart.options.scales.y.min;
|
||||||
|
|
||||||
|
// Dynamic target ticks based on chart height for better readability
|
||||||
|
const chartHeight = chart.height || 300;
|
||||||
|
const targetTicks = Math.max(4, Math.min(8, Math.floor(chartHeight / 50)));
|
||||||
|
|
||||||
|
// Calculate ideal step size
|
||||||
|
const rawStepSize = range / targetTicks;
|
||||||
|
|
||||||
|
// Find a "nice" step size that's close to the raw step size
|
||||||
|
const stepSize = calculateNiceStepSize(rawStepSize);
|
||||||
|
|
||||||
// Set the calculated stepSize
|
// Set the calculated stepSize
|
||||||
chart.options.scales.y.ticks.stepSize = stepSize;
|
chart.options.scales.y.ticks.stepSize = stepSize;
|
||||||
|
|
||||||
// Log the chosen stepSize for debugging
|
// Log the chosen stepSize
|
||||||
console.log(`Y-axis range: ${range.toFixed(2)}, using stepSize: ${stepSize}`);
|
console.log(`Y-axis range: ${range.toFixed(2)} TH/s, using stepSize: ${stepSize} (target ticks: ${targetTicks})`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.warn("No valid history data items available");
|
console.warn("No history data items available");
|
||||||
|
useSingleDataPoint();
|
||||||
}
|
}
|
||||||
} catch (historyError) {
|
} catch (historyError) {
|
||||||
console.error("Error processing hashrate history data:", historyError);
|
console.error("Error processing hashrate history data:", historyError);
|
||||||
@ -1494,6 +1662,48 @@ function updateChartWithNormalizedData(chart, data) {
|
|||||||
useSingleDataPoint();
|
useSingleDataPoint();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate standard deviation of an array of values
|
||||||
|
* @param {Array<number>} values - Array of numeric values
|
||||||
|
* @param {number} mean - Pre-calculated mean (optional)
|
||||||
|
* @returns {number} - Standard deviation
|
||||||
|
*/
|
||||||
|
function calculateStandardDeviation(values, precalculatedMean = null) {
|
||||||
|
if (!values || values.length <= 1) return 0;
|
||||||
|
|
||||||
|
// Calculate mean if not provided
|
||||||
|
const mean = precalculatedMean !== null ? precalculatedMean :
|
||||||
|
values.reduce((sum, val) => sum + val, 0) / values.length;
|
||||||
|
|
||||||
|
// Calculate sum of squared differences
|
||||||
|
const squaredDiffSum = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0);
|
||||||
|
|
||||||
|
// Calculate standard deviation
|
||||||
|
return Math.sqrt(squaredDiffSum / values.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate a "nice" step size close to the raw step size
|
||||||
|
* @param {number} rawStepSize - The mathematically ideal step size
|
||||||
|
* @returns {number} - A rounded, human-friendly step size
|
||||||
|
*/
|
||||||
|
function calculateNiceStepSize(rawStepSize) {
|
||||||
|
if (rawStepSize <= 0) return 1; // Safety check
|
||||||
|
|
||||||
|
// Get order of magnitude
|
||||||
|
const magnitude = Math.pow(10, Math.floor(Math.log10(rawStepSize)));
|
||||||
|
const normalized = rawStepSize / magnitude;
|
||||||
|
|
||||||
|
// Choose a nice step size
|
||||||
|
let niceStepSize;
|
||||||
|
if (normalized < 1.5) niceStepSize = 1;
|
||||||
|
else if (normalized < 3) niceStepSize = 2;
|
||||||
|
else if (normalized < 7) niceStepSize = 5;
|
||||||
|
else niceStepSize = 10;
|
||||||
|
|
||||||
|
return niceStepSize * magnitude;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle single datapoint display when no history is available
|
// Handle single datapoint display when no history is available
|
||||||
function useSingleDataPoint() {
|
function useSingleDataPoint() {
|
||||||
try {
|
try {
|
||||||
|
@ -227,84 +227,131 @@ function hideLoader() {
|
|||||||
$("#loader").hide();
|
$("#loader").hide();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch worker data from API with pagination, limiting to 10 pages
|
// Fetch worker data with better pagination and progressive loading
|
||||||
function fetchWorkerData(forceRefresh = false) {
|
function fetchWorkerData(forceRefresh = false) {
|
||||||
console.log("Fetching worker data...");
|
console.log("Fetching worker data...");
|
||||||
lastManualRefreshTime = Date.now();
|
lastManualRefreshTime = Date.now();
|
||||||
$('#worker-grid').addClass('loading-fade');
|
$('#worker-grid').addClass('loading-fade');
|
||||||
showLoader();
|
showLoader();
|
||||||
|
|
||||||
const maxPages = 10;
|
// For large datasets, better to use streaming or chunked approach
|
||||||
|
// First fetch just the summary data and first page
|
||||||
|
const initialRequest = $.ajax({
|
||||||
|
url: `/api/workers?summary=true&page=1${forceRefresh ? '&force=true' : ''}`,
|
||||||
|
method: 'GET',
|
||||||
|
dataType: 'json',
|
||||||
|
timeout: 15000
|
||||||
|
});
|
||||||
|
|
||||||
|
initialRequest.then(summaryData => {
|
||||||
|
// Store summary stats immediately to update UI
|
||||||
|
workerData = summaryData || {};
|
||||||
|
workerData.workers = workerData.workers || [];
|
||||||
|
|
||||||
|
// Update summary stats while we load more workers
|
||||||
|
updateSummaryStats();
|
||||||
|
updateMiniChart();
|
||||||
|
updateLastUpdated();
|
||||||
|
|
||||||
|
const totalPages = Math.ceil((workerData.workers_total || 0) / 100); // Assuming 100 workers per page
|
||||||
|
const pagesToFetch = Math.min(totalPages, 20); // Limit to 20 pages max
|
||||||
|
|
||||||
|
if (pagesToFetch <= 1) {
|
||||||
|
// We already have all the data from the first request
|
||||||
|
finishWorkerLoad();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Progress indicator
|
||||||
|
const progressBar = $('<div class="worker-load-progress"><div class="progress-bar"></div><div class="progress-text">Loading workers: 1/' + pagesToFetch + '</div></div>');
|
||||||
|
$('#worker-grid').html(progressBar);
|
||||||
|
|
||||||
|
// Load remaining pages in batches to avoid overwhelming the browser
|
||||||
|
loadWorkerPages(2, pagesToFetch, progressBar);
|
||||||
|
}).catch(error => {
|
||||||
|
console.error("Error fetching initial worker data:", error);
|
||||||
|
$('#worker-grid').html('<div class="text-center p-5"><i class="fas fa-exclamation-circle"></i> Error loading workers. <button class="retry-btn">Retry</button></div>');
|
||||||
|
$('.retry-btn').on('click', () => fetchWorkerData(true));
|
||||||
|
hideLoader();
|
||||||
|
$('#worker-grid').removeClass('loading-fade');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load worker pages in batches
|
||||||
|
function loadWorkerPages(startPage, totalPages, progressBar) {
|
||||||
|
const BATCH_SIZE = 3; // Number of pages to load in parallel
|
||||||
|
const endPage = Math.min(startPage + BATCH_SIZE - 1, totalPages);
|
||||||
const requests = [];
|
const requests = [];
|
||||||
|
|
||||||
// Create requests for pages 1 through maxPages concurrently
|
for (let page = startPage; page <= endPage; page++) {
|
||||||
for (let page = 1; page <= maxPages; page++) {
|
requests.push(
|
||||||
const apiUrl = `/api/workers?page=${page}${forceRefresh ? '&force=true' : ''}`;
|
$.ajax({
|
||||||
requests.push($.ajax({
|
url: `/api/workers?page=${page}`,
|
||||||
url: apiUrl,
|
method: 'GET',
|
||||||
method: 'GET',
|
dataType: 'json',
|
||||||
dataType: 'json',
|
timeout: 15000
|
||||||
timeout: 15000
|
})
|
||||||
}));
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process all requests concurrently
|
|
||||||
Promise.all(requests)
|
Promise.all(requests)
|
||||||
.then(pages => {
|
.then(pages => {
|
||||||
let allWorkers = [];
|
// Process each page
|
||||||
let aggregatedData = null;
|
pages.forEach(pageData => {
|
||||||
|
if (pageData && pageData.workers && pageData.workers.length > 0) {
|
||||||
pages.forEach((data, i) => {
|
// Append new workers to our list efficiently
|
||||||
if (data && data.workers && data.workers.length > 0) {
|
workerData.workers = workerData.workers.concat(pageData.workers);
|
||||||
allWorkers = allWorkers.concat(data.workers);
|
|
||||||
if (i === 0) {
|
|
||||||
aggregatedData = data; // preserve stats from first page
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.warn(`No workers found on page ${i + 1}`);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Deduplicate workers if necessary (using worker.name as unique key)
|
// Update progress
|
||||||
const uniqueWorkers = allWorkers.filter((worker, index, self) =>
|
const progress = Math.min(endPage / totalPages * 100, 100);
|
||||||
index === self.findIndex((w) => w.name === worker.name)
|
progressBar.find('.progress-bar').css('width', progress + '%');
|
||||||
);
|
progressBar.find('.progress-text').text(`Loading workers: ${endPage}/${totalPages}`);
|
||||||
|
|
||||||
workerData = aggregatedData || {};
|
if (endPage < totalPages) {
|
||||||
workerData.workers = uniqueWorkers;
|
// Continue with next batch
|
||||||
|
setTimeout(() => loadWorkerPages(endPage + 1, totalPages, progressBar), 100);
|
||||||
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
|
} else {
|
||||||
BitcoinMinuteRefresh.notifyRefresh();
|
// All pages loaded
|
||||||
|
finishWorkerLoad();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateWorkerGrid();
|
|
||||||
updateSummaryStats();
|
|
||||||
updateMiniChart();
|
|
||||||
updateLastUpdated();
|
|
||||||
|
|
||||||
$('#retry-button').hide();
|
|
||||||
connectionRetryCount = 0;
|
|
||||||
console.log("Worker data updated successfully");
|
|
||||||
$('#worker-grid').removeClass('loading-fade');
|
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.catch(error => {
|
||||||
console.error("Error fetching worker data:", error);
|
console.error(`Error fetching worker pages ${startPage}-${endPage}:`, error);
|
||||||
})
|
// Continue with what we have so far
|
||||||
.finally(() => {
|
finishWorkerLoad();
|
||||||
hideLoader();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh worker data every 60 seconds
|
// Finish loading process with optimized rendering
|
||||||
setInterval(function () {
|
function finishWorkerLoad() {
|
||||||
console.log("Refreshing worker data at " + new Date().toLocaleTimeString());
|
// Deduplicate workers more efficiently with a Map
|
||||||
fetchWorkerData();
|
const uniqueWorkersMap = new Map();
|
||||||
}, 60000);
|
workerData.workers.forEach(worker => {
|
||||||
|
if (worker.name) {
|
||||||
|
uniqueWorkersMap.set(worker.name, worker);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
workerData.workers = Array.from(uniqueWorkersMap.values());
|
||||||
|
|
||||||
// Update the worker grid with data
|
// Notify BitcoinMinuteRefresh
|
||||||
function updateWorkerGrid() {
|
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
|
||||||
console.log("Updating worker grid...");
|
BitcoinMinuteRefresh.notifyRefresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Efficiently render workers with virtualized list approach
|
||||||
|
renderWorkersList();
|
||||||
|
|
||||||
|
$('#retry-button').hide();
|
||||||
|
connectionRetryCount = 0;
|
||||||
|
console.log(`Worker data updated successfully: ${workerData.workers.length} workers`);
|
||||||
|
$('#worker-grid').removeClass('loading-fade');
|
||||||
|
hideLoader();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtualized list rendering for large datasets
|
||||||
|
function renderWorkersList() {
|
||||||
if (!workerData || !workerData.workers) {
|
if (!workerData || !workerData.workers) {
|
||||||
console.error("No worker data available");
|
console.error("No worker data available");
|
||||||
return;
|
return;
|
||||||
@ -325,14 +372,114 @@ function updateWorkerGrid() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
filteredWorkers.forEach(worker => {
|
// Performance optimization for large lists
|
||||||
const card = createWorkerCard(worker);
|
if (filteredWorkers.length > 200) {
|
||||||
workerGrid.append(card);
|
// For very large lists, render in batches
|
||||||
});
|
const workerBatch = 100;
|
||||||
|
const totalBatches = Math.ceil(filteredWorkers.length / workerBatch);
|
||||||
|
|
||||||
|
console.log(`Rendering ${filteredWorkers.length} workers in ${totalBatches} batches`);
|
||||||
|
|
||||||
|
// Render first batch immediately
|
||||||
|
renderWorkerBatch(filteredWorkers.slice(0, workerBatch), workerGrid);
|
||||||
|
|
||||||
|
// Render remaining batches with setTimeout to avoid UI freezing
|
||||||
|
for (let i = 1; i < totalBatches; i++) {
|
||||||
|
const start = i * workerBatch;
|
||||||
|
const end = Math.min(start + workerBatch, filteredWorkers.length);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
renderWorkerBatch(filteredWorkers.slice(start, end), workerGrid);
|
||||||
|
|
||||||
|
// Update "loading more" message with progress
|
||||||
|
const loadingMsg = workerGrid.find('.loading-more-workers');
|
||||||
|
if (loadingMsg.length) {
|
||||||
|
if (i === totalBatches - 1) {
|
||||||
|
loadingMsg.remove();
|
||||||
|
} else {
|
||||||
|
loadingMsg.text(`Loading more workers... ${Math.min((i + 1) * workerBatch, filteredWorkers.length)}/${filteredWorkers.length}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, i * 50); // 50ms delay between batches
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add "loading more" indicator at the bottom
|
||||||
|
if (totalBatches > 1) {
|
||||||
|
workerGrid.append(`<div class="loading-more-workers">Loading more workers... ${workerBatch}/${filteredWorkers.length}</div>`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For smaller lists, render all at once
|
||||||
|
renderWorkerBatch(filteredWorkers, workerGrid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create worker card element
|
// Render a batch of workers efficiently
|
||||||
function createWorkerCard(worker) {
|
function renderWorkerBatch(workers, container) {
|
||||||
|
// Create a document fragment for better performance
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
|
// Calculate max hashrate once for this batch
|
||||||
|
const maxHashrate = calculateMaxHashrate();
|
||||||
|
|
||||||
|
workers.forEach(worker => {
|
||||||
|
const card = createWorkerCard(worker, maxHashrate);
|
||||||
|
fragment.appendChild(card[0]);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.append(fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate max hashrate once to avoid recalculating for each worker
|
||||||
|
function calculateMaxHashrate() {
|
||||||
|
let maxHashrate = 125; // Default fallback
|
||||||
|
|
||||||
|
// First check if global hashrate data is available
|
||||||
|
if (workerData && workerData.hashrate_24hr) {
|
||||||
|
const globalHashrate = normalizeHashrate(workerData.hashrate_24hr, workerData.hashrate_24hr_unit || 'th/s');
|
||||||
|
if (globalHashrate > 0) {
|
||||||
|
return Math.max(5, globalHashrate * 1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no global data, calculate from workers efficiently
|
||||||
|
if (workerData && workerData.workers && workerData.workers.length > 0) {
|
||||||
|
const onlineWorkers = workerData.workers.filter(w => w.status === 'online');
|
||||||
|
|
||||||
|
if (onlineWorkers.length > 0) {
|
||||||
|
let maxWorkerHashrate = 0;
|
||||||
|
|
||||||
|
// Find maximum hashrate without logging every worker
|
||||||
|
onlineWorkers.forEach(w => {
|
||||||
|
const hashrateValue = w.hashrate_24hr || w.hashrate_3hr || 0;
|
||||||
|
const hashrateUnit = w.hashrate_24hr ?
|
||||||
|
(w.hashrate_24hr_unit || 'th/s') :
|
||||||
|
(w.hashrate_3hr_unit || 'th/s');
|
||||||
|
const normalizedRate = normalizeHashrate(hashrateValue, hashrateUnit);
|
||||||
|
|
||||||
|
if (normalizedRate > maxWorkerHashrate) {
|
||||||
|
maxWorkerHashrate = normalizedRate;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (maxWorkerHashrate > 0) {
|
||||||
|
return Math.max(5, maxWorkerHashrate * 1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to total hashrate
|
||||||
|
if (workerData && workerData.total_hashrate) {
|
||||||
|
const totalHashrate = normalizeHashrate(workerData.total_hashrate, workerData.hashrate_unit || 'th/s');
|
||||||
|
if (totalHashrate > 0) {
|
||||||
|
return Math.max(5, totalHashrate * 1.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return maxHashrate;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optimized worker card creation (removed debug logging)
|
||||||
|
function createWorkerCard(worker, maxHashrate) {
|
||||||
const card = $('<div class="worker-card"></div>');
|
const card = $('<div class="worker-card"></div>');
|
||||||
|
|
||||||
card.addClass(worker.status === 'online' ? 'worker-card-online' : 'worker-card-offline');
|
card.addClass(worker.status === 'online' ? 'worker-card-online' : 'worker-card-offline');
|
||||||
@ -340,7 +487,7 @@ function createWorkerCard(worker) {
|
|||||||
card.append(`<div class="worker-name">${worker.name}</div>`);
|
card.append(`<div class="worker-name">${worker.name}</div>`);
|
||||||
card.append(`<div class="status-badge ${worker.status === 'online' ? 'status-badge-online' : 'status-badge-offline'}">${worker.status.toUpperCase()}</div>`);
|
card.append(`<div class="status-badge ${worker.status === 'online' ? 'status-badge-online' : 'status-badge-offline'}">${worker.status.toUpperCase()}</div>`);
|
||||||
|
|
||||||
const maxHashrate = 125; // TH/s - adjust based on your fleet
|
// Use 3hr hashrate for display as in original code
|
||||||
const normalizedHashrate = normalizeHashrate(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
|
const normalizedHashrate = normalizeHashrate(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
|
||||||
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
|
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
|
||||||
const formattedHashrate = formatHashrateForDisplay(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
|
const formattedHashrate = formatHashrateForDisplay(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
|
||||||
@ -358,15 +505,8 @@ function createWorkerCard(worker) {
|
|||||||
// Format the last share using the proper method for timezone conversion
|
// Format the last share using the proper method for timezone conversion
|
||||||
let formattedLastShare = 'N/A';
|
let formattedLastShare = 'N/A';
|
||||||
if (worker.last_share && typeof worker.last_share === 'string') {
|
if (worker.last_share && typeof worker.last_share === 'string') {
|
||||||
// This is a more reliable method for timezone conversion
|
|
||||||
try {
|
try {
|
||||||
// The worker.last_share is likely in format "YYYY-MM-DD HH:MM"
|
const dateWithoutTZ = new Date(worker.last_share + 'Z');
|
||||||
// We need to consider it as UTC and convert to the configured timezone
|
|
||||||
|
|
||||||
// Create a proper date object, ensuring UTC interpretation
|
|
||||||
const dateWithoutTZ = new Date(worker.last_share + 'Z'); // Adding Z to treat as UTC
|
|
||||||
|
|
||||||
// Format it according to the configured timezone
|
|
||||||
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
|
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
|
||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
@ -374,8 +514,7 @@ function createWorkerCard(worker) {
|
|||||||
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error formatting last share time:", e, worker.last_share);
|
formattedLastShare = worker.last_share;
|
||||||
formattedLastShare = worker.last_share; // Fallback to original value
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -395,33 +534,41 @@ function createWorkerCard(worker) {
|
|||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter worker data based on current filter state
|
// Modified filterWorkers function for better search functionality
|
||||||
|
// This will replace the existing filterWorkers function in workers.js
|
||||||
|
function filterWorkers() {
|
||||||
|
if (!workerData || !workerData.workers) return;
|
||||||
|
renderWorkersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the workers grid when filters change
|
||||||
|
function updateWorkerGrid() {
|
||||||
|
renderWorkersList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modified filterWorkersData function to only include 'all', 'online', and 'offline' filters
|
||||||
function filterWorkersData(workers) {
|
function filterWorkersData(workers) {
|
||||||
if (!workers) return [];
|
if (!workers) return [];
|
||||||
|
|
||||||
return workers.filter(worker => {
|
return workers.filter(worker => {
|
||||||
const workerName = (worker.name || '').toLowerCase();
|
const workerName = (worker.name || '').toLowerCase();
|
||||||
const isOnline = worker.status === 'online';
|
const isOnline = worker.status === 'online';
|
||||||
const workerType = (worker.type || '').toLowerCase();
|
|
||||||
|
|
||||||
|
// Modified to only handle 'all', 'online', and 'offline' filters
|
||||||
const matchesFilter = filterState.currentFilter === 'all' ||
|
const matchesFilter = filterState.currentFilter === 'all' ||
|
||||||
(filterState.currentFilter === 'online' && isOnline) ||
|
(filterState.currentFilter === 'online' && isOnline) ||
|
||||||
(filterState.currentFilter === 'offline' && !isOnline) ||
|
(filterState.currentFilter === 'offline' && !isOnline);
|
||||||
(filterState.currentFilter === 'asic' && workerType === 'asic') ||
|
|
||||||
(filterState.currentFilter === 'bitaxe' && workerType === 'bitaxe');
|
|
||||||
|
|
||||||
const matchesSearch = filterState.searchTerm === '' || workerName.includes(filterState.searchTerm);
|
// Improved search matching to check name, model and type
|
||||||
|
const matchesSearch = filterState.searchTerm === '' ||
|
||||||
|
workerName.includes(filterState.searchTerm) ||
|
||||||
|
(worker.model && worker.model.toLowerCase().includes(filterState.searchTerm)) ||
|
||||||
|
(worker.type && worker.type.toLowerCase().includes(filterState.searchTerm));
|
||||||
|
|
||||||
return matchesFilter && matchesSearch;
|
return matchesFilter && matchesSearch;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply filter to rendered worker cards
|
|
||||||
function filterWorkers() {
|
|
||||||
if (!workerData || !workerData.workers) return;
|
|
||||||
updateWorkerGrid();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update summary stats with normalized hashrate display
|
// Update summary stats with normalized hashrate display
|
||||||
function updateSummaryStats() {
|
function updateSummaryStats() {
|
||||||
if (!workerData) return;
|
if (!workerData) return;
|
||||||
|
@ -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 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">
|
||||||
@ -116,6 +116,7 @@
|
|||||||
<div class="navigation-links">
|
<div class="navigation-links">
|
||||||
<a href="/dashboard" class="nav-link {% block dashboard_active %}{% endblock %}">DASHBOARD</a>
|
<a href="/dashboard" class="nav-link {% block dashboard_active %}{% endblock %}">DASHBOARD</a>
|
||||||
<a href="/workers" class="nav-link {% block workers_active %}{% endblock %}">WORKERS</a>
|
<a href="/workers" class="nav-link {% block workers_active %}{% endblock %}">WORKERS</a>
|
||||||
|
<a href="/earnings" class="nav-link {% block earnings_active %}{% endblock %}">EARNINGS</a>
|
||||||
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">BLOCKS</a>
|
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">BLOCKS</a>
|
||||||
<a href="/notifications" class="nav-link {% block notifications_active %}{% endblock %}">
|
<a href="/notifications" class="nav-link {% block notifications_active %}{% endblock %}">
|
||||||
NOTIFICATIONS
|
NOTIFICATIONS
|
||||||
@ -135,7 +136,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>
|
<p>v0.9.5 - Public Beta</p>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD {% endblock %}
|
{% block title %}BLOCKS - BTC-OS Dashboard {% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/blocks.css">
|
<link rel="stylesheet" href="/static/css/blocks.css">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}BTC-OS Mining Dashboard {% endblock %}
|
{% block title %}BTC-OS Dashboard {% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||||
|
149
templates/earnings.html
Normal file
149
templates/earnings.html
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}EARNINGS - BTC-OS Dashboard {% endblock %}
|
||||||
|
|
||||||
|
{% block css %}
|
||||||
|
<link rel="stylesheet" href="{{ url_for('static', filename='css/earnings.css') }}">
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block header %}EARNINGS MONITOR{% endblock %}
|
||||||
|
|
||||||
|
{% block earnings_active %}active{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="dashboard-container">
|
||||||
|
|
||||||
|
<!-- Summary Cards Section -->
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-card">
|
||||||
|
<h2>Unpaid Earnings</h2>
|
||||||
|
<div class="stat-value">
|
||||||
|
<span id="unpaid-sats">{{ earnings.unpaid_earnings_sats|default(0)|int|commafy }}</span>
|
||||||
|
<span class="stat-unit">sats</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-secondary">
|
||||||
|
<span id="unpaid-btc">{{ "%.8f"|format(earnings.unpaid_earnings|default(0)|float) }}</span> BTC
|
||||||
|
</div>
|
||||||
|
<div class="stat-time" id="est-time-payout">{{ earnings.est_time_to_payout|default('Unknown') }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h2>Total Paid</h2>
|
||||||
|
<div class="stat-value">
|
||||||
|
<span id="total-paid-sats">{{ earnings.total_paid_sats|default(0)|int|commafy }}</span>
|
||||||
|
<span class="stat-unit">sats</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-secondary">
|
||||||
|
<span id="total-paid-btc">{{ "%.8f"|format(earnings.total_paid_btc|default(0)|float) }}</span> BTC
|
||||||
|
</div>
|
||||||
|
<div class="stat-secondary" id="total-paid-fiat">
|
||||||
|
<span id="total-paid-currency-symbol">{{ currency_symbols[user_currency] }}</span>{{ "%.2f"|format(earnings.total_paid_fiat|default(0)|float)|commafy }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stat-card">
|
||||||
|
<h2>Total Payments</h2>
|
||||||
|
<div class="stat-value">
|
||||||
|
<span id="payment-count">{{ earnings.total_payments|default(0) }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-secondary" id="latest-payment">
|
||||||
|
Latest: {{ earnings.payments[0].date|format_datetime if earnings.payments else 'None' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Monthly Summaries Section -->
|
||||||
|
<div class="earnings-section">
|
||||||
|
<h2>Monthly Summary</h2>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="earnings-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Month</th>
|
||||||
|
<th>Payments</th>
|
||||||
|
<th>BTC</th>
|
||||||
|
<th>Sats</th>
|
||||||
|
<th>{{ user_currency }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="monthly-summary-table">
|
||||||
|
{% for month in earnings.monthly_summaries %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ month.month_name }}</td>
|
||||||
|
<td>{{ month.payments|length }}</td>
|
||||||
|
<td>{{ "%.8f"|format(month.total_btc|float) }}</td>
|
||||||
|
<td>{{ month.total_sats|int|commafy }}</td>
|
||||||
|
<td><span class="currency-symbol">{{ currency_symbols[user_currency] }}</span>{{ "%.2f"|format(month.total_fiat|float) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">No payment data available</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Payments History Section -->
|
||||||
|
<div class="earnings-section">
|
||||||
|
<h2>Payment History</h2>
|
||||||
|
<div class="table-container">
|
||||||
|
<table class="earnings-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Amount (BTC)</th>
|
||||||
|
<th>Amount (sats)</th>
|
||||||
|
<th>Transaction ID</th>
|
||||||
|
<th>Status</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="payment-history-table">
|
||||||
|
{% for payment in earnings.payments %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ payment.date|format_datetime }}</td>
|
||||||
|
<td>{{ "%.8f"|format(payment.amount_btc|float) }}</td>
|
||||||
|
<td>{{ payment.amount_sats|int|commafy }}</td>
|
||||||
|
<td>
|
||||||
|
{% if payment.txid %}
|
||||||
|
<a href="https://mempool.guide/tx/{{ payment.txid }}" target="_blank" class="tx-link">
|
||||||
|
{{ payment.txid[:8] }}...{{ payment.txid[-8:] }}
|
||||||
|
</a>
|
||||||
|
{% elif payment.truncated_txid %}
|
||||||
|
<span class="tx-link truncated" title="Incomplete transaction ID">{{ payment.truncated_txid }}</span>
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="status-label status-{{ payment.status }}">{{ payment.status }}</span>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% else %}
|
||||||
|
<tr>
|
||||||
|
<td colspan="5">No payment history available</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- User Settings Info -->
|
||||||
|
<div class="settings-info">
|
||||||
|
<span class="setting-item">Currency: <strong id="user-currency">{{ user_currency }}</strong></span>
|
||||||
|
<span class="setting-item">Timezone: <strong id="user-timezone">{{ user_timezone }}</strong></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Pass configuration values to JavaScript
|
||||||
|
const userCurrency = "{{ user_currency }}";
|
||||||
|
const userTimezone = "{{ user_timezone }}";
|
||||||
|
const currencySymbol = "{{ currency_symbols[user_currency] }}";
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascript %}
|
||||||
|
<script src="{{ url_for('static', filename='js/earnings.js') }}"></script>
|
||||||
|
{% endblock %}
|
@ -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>Error - Mining Dashboard</title>
|
<title>Error - BTC-OS Dashboard</title>
|
||||||
<!-- Include both Orbitron and VT323 fonts -->
|
<!-- Include both Orbitron and VT323 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">
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD {% endblock %}
|
{% block title %}NOTIFICATIONS - BTC-OS Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/notifications.css">
|
<link rel="stylesheet" href="/static/css/notifications.css">
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}WORKERS - BTC-OS MINING DASHBOARD {% endblock %}
|
{% block title %}WORKERS - BTC-OS Dashboard{% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/workers.css">
|
<link rel="stylesheet" href="/static/css/workers.css">
|
||||||
@ -80,8 +80,6 @@
|
|||||||
<button class="filter-button active" data-filter="all">ALL WORKERS</button>
|
<button class="filter-button active" data-filter="all">ALL WORKERS</button>
|
||||||
<button class="filter-button" data-filter="online">ONLINE</button>
|
<button class="filter-button" data-filter="online">ONLINE</button>
|
||||||
<button class="filter-button" data-filter="offline">OFFLINE</button>
|
<button class="filter-button" data-filter="offline">OFFLINE</button>
|
||||||
<button class="filter-button" data-filter="asic">ASIC</button>
|
|
||||||
<button class="filter-button" data-filter="bitaxe">BITAXE</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -54,33 +54,33 @@ class WorkerService:
|
|||||||
|
|
||||||
def get_workers_data(self, cached_metrics, force_refresh=False):
|
def get_workers_data(self, cached_metrics, force_refresh=False):
|
||||||
"""
|
"""
|
||||||
Get worker data with caching for better performance.
|
Get worker data with caching for better performance and use consistent hashrate values.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cached_metrics (dict): Cached metrics from the dashboard
|
cached_metrics (dict): Cached metrics from the dashboard
|
||||||
force_refresh (bool): Whether to force a refresh of cached data
|
force_refresh (bool): Whether to force a refresh of cached data
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Worker data
|
dict: Worker data with standardized hashrates
|
||||||
"""
|
"""
|
||||||
current_time = datetime.now().timestamp()
|
current_time = datetime.now().timestamp()
|
||||||
|
|
||||||
# Return cached data if it's still fresh and not forced to refresh
|
# Return cached data if it's still fresh and not forced to refresh
|
||||||
if not force_refresh and self.worker_data_cache and self.last_worker_data_update and \
|
if not force_refresh and self.worker_data_cache and self.last_worker_data_update and \
|
||||||
(current_time - self.last_worker_data_update) < self.WORKER_DATA_CACHE_TIMEOUT:
|
(current_time - self.last_worker_data_update) < self.WORKER_DATA_CACHE_TIMEOUT:
|
||||||
# Even when using cached data, sync worker count with main dashboard
|
# Even when using cached data, sync worker count with main dashboard
|
||||||
if cached_metrics and cached_metrics.get("workers_hashing") is not None:
|
if cached_metrics and cached_metrics.get("workers_hashing") is not None:
|
||||||
self.sync_worker_counts_with_dashboard(self.worker_data_cache, cached_metrics)
|
self.sync_worker_counts_with_dashboard(self.worker_data_cache, cached_metrics)
|
||||||
|
|
||||||
logging.info("Using cached worker data")
|
logging.info("Using cached worker data")
|
||||||
return self.worker_data_cache
|
return self.worker_data_cache
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# First try to get actual worker data from the dashboard service
|
# First try to get actual worker data from the dashboard service
|
||||||
if self.dashboard_service:
|
if self.dashboard_service:
|
||||||
logging.info("Attempting to fetch real worker data from Ocean.xyz")
|
logging.info("Attempting to fetch real worker data from Ocean.xyz")
|
||||||
real_worker_data = self.dashboard_service.get_worker_data()
|
real_worker_data = self.dashboard_service.get_worker_data()
|
||||||
|
|
||||||
if real_worker_data and real_worker_data.get('workers') and len(real_worker_data['workers']) > 0:
|
if real_worker_data and real_worker_data.get('workers') and len(real_worker_data['workers']) > 0:
|
||||||
# Validate that worker names are not just "Online" or "Offline"
|
# Validate that worker names are not just "Online" or "Offline"
|
||||||
valid_names = False
|
valid_names = False
|
||||||
@ -89,22 +89,22 @@ class WorkerService:
|
|||||||
if name and name not in ['online', 'offline', 'total', 'worker', 'status']:
|
if name and name not in ['online', 'offline', 'total', 'worker', 'status']:
|
||||||
valid_names = True
|
valid_names = True
|
||||||
break
|
break
|
||||||
|
|
||||||
if valid_names:
|
if valid_names:
|
||||||
logging.info(f"Successfully retrieved {len(real_worker_data['workers'])} real workers from Ocean.xyz")
|
logging.info(f"Successfully retrieved {len(real_worker_data['workers'])} real workers from Ocean.xyz")
|
||||||
|
|
||||||
# Add hashrate history if available in cached metrics
|
# Add hashrate history if available in cached metrics
|
||||||
if cached_metrics and cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
if cached_metrics and cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
||||||
real_worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
real_worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
||||||
|
|
||||||
# Sync with dashboard metrics to ensure consistency
|
# Sync with dashboard metrics to ensure consistency
|
||||||
if cached_metrics:
|
if cached_metrics:
|
||||||
self.sync_worker_counts_with_dashboard(real_worker_data, cached_metrics)
|
self.sync_worker_counts_with_dashboard(real_worker_data, cached_metrics)
|
||||||
|
|
||||||
# Update cache
|
# Update cache
|
||||||
self.worker_data_cache = real_worker_data
|
self.worker_data_cache = real_worker_data
|
||||||
self.last_worker_data_update = current_time
|
self.last_worker_data_update = current_time
|
||||||
|
|
||||||
return real_worker_data
|
return real_worker_data
|
||||||
else:
|
else:
|
||||||
logging.warning("Real worker data had invalid names (like 'online'/'offline'), falling back to simulated data")
|
logging.warning("Real worker data had invalid names (like 'online'/'offline'), falling back to simulated data")
|
||||||
@ -112,93 +112,97 @@ class WorkerService:
|
|||||||
logging.warning("Real worker data fetch returned no workers, falling back to simulated data")
|
logging.warning("Real worker data fetch returned no workers, falling back to simulated data")
|
||||||
else:
|
else:
|
||||||
logging.warning("Dashboard service not available, cannot fetch real worker data")
|
logging.warning("Dashboard service not available, cannot fetch real worker data")
|
||||||
|
|
||||||
# Fallback to simulated data if real data fetch fails or returns no workers
|
# Fallback to simulated data if real data fetch fails or returns no workers
|
||||||
logging.info("Generating fallback simulated worker data")
|
logging.info("Generating fallback simulated worker data")
|
||||||
worker_data = self.generate_fallback_data(cached_metrics)
|
worker_data = self.generate_fallback_data(cached_metrics)
|
||||||
|
|
||||||
# Add hashrate history if available in cached metrics
|
# Add hashrate history if available in cached metrics
|
||||||
if cached_metrics and cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
if cached_metrics and cached_metrics.get("arrow_history") and cached_metrics["arrow_history"].get("hashrate_3hr"):
|
||||||
worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
||||||
|
|
||||||
# Ensure worker counts match dashboard metrics
|
# Ensure worker counts match dashboard metrics
|
||||||
if cached_metrics:
|
if cached_metrics:
|
||||||
self.sync_worker_counts_with_dashboard(worker_data, cached_metrics)
|
self.sync_worker_counts_with_dashboard(worker_data, cached_metrics)
|
||||||
|
|
||||||
# Update cache
|
# Update cache
|
||||||
self.worker_data_cache = worker_data
|
self.worker_data_cache = worker_data
|
||||||
self.last_worker_data_update = current_time
|
self.last_worker_data_update = current_time
|
||||||
|
|
||||||
logging.info(f"Successfully generated fallback worker data: {worker_data['workers_total']} workers")
|
logging.info(f"Successfully generated fallback worker data: {worker_data['workers_total']} workers")
|
||||||
return worker_data
|
return worker_data
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error getting worker data: {e}")
|
logging.error(f"Error getting worker data: {e}")
|
||||||
fallback_data = self.generate_fallback_data(cached_metrics)
|
fallback_data = self.generate_fallback_data(cached_metrics)
|
||||||
|
|
||||||
# Even on error, try to sync with dashboard metrics
|
# Even on error, try to sync with dashboard metrics
|
||||||
if cached_metrics:
|
if cached_metrics:
|
||||||
self.sync_worker_counts_with_dashboard(fallback_data, cached_metrics)
|
self.sync_worker_counts_with_dashboard(fallback_data, cached_metrics)
|
||||||
|
|
||||||
return fallback_data
|
return fallback_data
|
||||||
|
|
||||||
def sync_worker_counts_with_dashboard(self, worker_data, dashboard_metrics):
|
def sync_worker_counts_with_dashboard(self, worker_data, dashboard_metrics):
|
||||||
"""
|
"""
|
||||||
Synchronize worker counts and other metrics between worker data and dashboard metrics.
|
Synchronize worker counts and other metrics between worker data and dashboard metrics.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
worker_data (dict): Worker data to be updated
|
worker_data (dict): Worker data to be updated
|
||||||
dashboard_metrics (dict): Dashboard metrics with worker count and other data
|
dashboard_metrics (dict): Dashboard metrics with worker count and other data
|
||||||
"""
|
"""
|
||||||
if not worker_data or not dashboard_metrics:
|
if not worker_data or not dashboard_metrics:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Sync worker count
|
# Sync worker count
|
||||||
dashboard_worker_count = dashboard_metrics.get("workers_hashing")
|
dashboard_worker_count = dashboard_metrics.get("workers_hashing")
|
||||||
|
|
||||||
# Only proceed if dashboard has valid worker count
|
# Only proceed if dashboard has valid worker count
|
||||||
if dashboard_worker_count is not None:
|
if dashboard_worker_count is not None:
|
||||||
current_worker_count = worker_data.get("workers_total", 0)
|
current_worker_count = worker_data.get("workers_total", 0)
|
||||||
|
|
||||||
# If counts already match, no need to sync workers count
|
# If counts already match, no need to sync workers count
|
||||||
if current_worker_count != dashboard_worker_count:
|
if current_worker_count != dashboard_worker_count:
|
||||||
logging.info(f"Syncing worker count: worker page({current_worker_count}) → dashboard({dashboard_worker_count})")
|
logging.info(f"Syncing worker count: worker page({current_worker_count}) → dashboard({dashboard_worker_count})")
|
||||||
|
|
||||||
# Update the total count
|
# Update the total count
|
||||||
worker_data["workers_total"] = dashboard_worker_count
|
worker_data["workers_total"] = dashboard_worker_count
|
||||||
|
|
||||||
# Adjust online/offline counts proportionally
|
# Adjust online/offline counts proportionally
|
||||||
current_online = worker_data.get("workers_online", 0)
|
current_online = worker_data.get("workers_online", 0)
|
||||||
current_total = max(1, current_worker_count) # Avoid division by zero
|
current_total = max(1, current_worker_count) # Avoid division by zero
|
||||||
|
|
||||||
# Calculate ratio of online workers
|
# Calculate ratio of online workers
|
||||||
online_ratio = current_online / current_total
|
online_ratio = current_online / current_total
|
||||||
|
|
||||||
# Recalculate online and offline counts
|
# Recalculate online and offline counts
|
||||||
new_online_count = round(dashboard_worker_count * online_ratio)
|
new_online_count = round(dashboard_worker_count * online_ratio)
|
||||||
new_offline_count = dashboard_worker_count - new_online_count
|
new_offline_count = dashboard_worker_count - new_online_count
|
||||||
|
|
||||||
# Update the counts
|
# Update the counts
|
||||||
worker_data["workers_online"] = new_online_count
|
worker_data["workers_online"] = new_online_count
|
||||||
worker_data["workers_offline"] = new_offline_count
|
worker_data["workers_offline"] = new_offline_count
|
||||||
|
|
||||||
logging.info(f"Updated worker counts - Total: {dashboard_worker_count}, Online: {new_online_count}, Offline: {new_offline_count}")
|
logging.info(f"Updated worker counts - Total: {dashboard_worker_count}, Online: {new_online_count}, Offline: {new_offline_count}")
|
||||||
|
|
||||||
# If we have worker instances, try to adjust them as well
|
# If we have worker instances, try to adjust them as well
|
||||||
if "workers" in worker_data and isinstance(worker_data["workers"], list):
|
if "workers" in worker_data and isinstance(worker_data["workers"], list):
|
||||||
self.adjust_worker_instances(worker_data, dashboard_worker_count)
|
self.adjust_worker_instances(worker_data, dashboard_worker_count)
|
||||||
|
|
||||||
|
# IMPORTANT: Use the dashboard's hashrate values for consistency
|
||||||
|
# This ensures the workers page shows the same hashrate as the main dashboard
|
||||||
|
if dashboard_metrics.get("hashrate_3hr") is not None:
|
||||||
|
worker_data["total_hashrate"] = dashboard_metrics.get("hashrate_3hr")
|
||||||
|
worker_data["hashrate_unit"] = dashboard_metrics.get("hashrate_3hr_unit", "TH/s")
|
||||||
|
logging.info(f"Synced total hashrate from dashboard: {worker_data['total_hashrate']} {worker_data['hashrate_unit']}")
|
||||||
|
|
||||||
# Sync daily sats - critical for fixing the daily sats discrepancy
|
# Sync daily sats - critical for fixing the daily sats discrepancy
|
||||||
if dashboard_metrics.get("daily_mined_sats") is not None:
|
if dashboard_metrics.get("daily_mined_sats") is not None:
|
||||||
daily_sats_value = dashboard_metrics.get("daily_mined_sats")
|
daily_sats_value = dashboard_metrics.get("daily_mined_sats")
|
||||||
if daily_sats_value != worker_data.get("daily_sats"):
|
if daily_sats_value != worker_data.get("daily_sats"):
|
||||||
worker_data["daily_sats"] = daily_sats_value
|
worker_data["daily_sats"] = daily_sats_value
|
||||||
logging.info(f"Synced daily sats: {worker_data['daily_sats']}")
|
logging.info(f"Synced daily sats: {worker_data['daily_sats']}")
|
||||||
|
|
||||||
# Sync other important metrics
|
# Sync other important metrics
|
||||||
if dashboard_metrics.get("total_hashrate") is not None:
|
|
||||||
worker_data["total_hashrate"] = dashboard_metrics.get("total_hashrate")
|
|
||||||
|
|
||||||
if dashboard_metrics.get("unpaid_earnings") is not None:
|
if dashboard_metrics.get("unpaid_earnings") is not None:
|
||||||
# Attempt to convert string to float if needed
|
# Attempt to convert string to float if needed
|
||||||
unpaid_value = dashboard_metrics.get("unpaid_earnings")
|
unpaid_value = dashboard_metrics.get("unpaid_earnings")
|
||||||
|
Loading…
Reference in New Issue
Block a user