mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 11:10:44 +02:00
Enhance earnings features and improve data handling
- Updated `App.py` to add a datetime formatting filter and enhance the earnings route with better error handling and currency conversion. - Revised `README.md` to document new features, including earnings breakdown and API endpoints. - Added default currency setting in `config.json` and a function to retrieve it in `config.py`. - Improved `data_service.py` with a new method for fetching payment history and enhanced error handling. - Updated `setup.py` to include new CSS and JS files for the earnings page. - Enhanced styling in `common.css`, `dashboard.css`, and `earnings.css` for better responsiveness. - Optimized `workers.js` for improved performance with a progressive loading approach. - Updated multiple HTML files to reflect new dashboard structure and features. - Enhanced `worker_service.py` for consistent hashrate values and improved logging.
This commit is contained in:
parent
b92f8074da
commit
5a6331d032
194
App.py
194
App.py
@ -1,4 +1,4 @@
|
||||
"""
|
||||
"""
|
||||
Main application module for the Bitcoin Mining Dashboard.
|
||||
"""
|
||||
import os
|
||||
@ -533,7 +533,19 @@ def create_scheduler():
|
||||
def commafy(value):
|
||||
"""Add commas to numbers for better readability."""
|
||||
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:
|
||||
return value
|
||||
|
||||
@ -1227,6 +1239,184 @@ def reset_chart_data():
|
||||
logging.error(f"Error resetting chart data: {e}")
|
||||
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
|
||||
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
|
||||
- **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
|
||||
- **Connection Recovery**: Automatic reconnection after network interruptions
|
||||
- **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
|
||||
- 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
|
||||
|
||||
- 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
|
||||
- Block details modal with comprehensive data
|
||||
|
||||
### Notifications
|
||||
- Real-time alerts for important events
|
||||
- Notification history with read/unread status
|
||||
|
||||
### System Monitor
|
||||
|
||||
- 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/notifications`: Manages notifications for the user.
|
||||
- `/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
|
||||
|
||||
@ -206,6 +232,7 @@ DeepSea-Dashboard/
|
||||
├── requirements.txt # Python dependencies
|
||||
├── Dockerfile # Docker configuration
|
||||
├── docker-compose.yml # Docker Compose configuration
|
||||
DeepSea-Dashboard/
|
||||
│
|
||||
├── templates/ # HTML templates
|
||||
│ ├── base.html # Base template with common elements
|
||||
@ -214,6 +241,7 @@ DeepSea-Dashboard/
|
||||
│ ├── workers.html # Workers dashboard template
|
||||
│ ├── blocks.html # Bitcoin blocks template
|
||||
│ ├── notifications.html # Notifications template
|
||||
│ ├── earnings.html # Earnings page template
|
||||
│ └── error.html # Error page template
|
||||
│
|
||||
├── static/ # Static assets
|
||||
@ -224,6 +252,7 @@ DeepSea-Dashboard/
|
||||
│ │ ├── boot.css # Boot sequence styles
|
||||
│ │ ├── blocks.css # Blocks page styles
|
||||
│ │ ├── notifications.css # Notifications page styles
|
||||
│ │ ├── earnings.css # Earnings page styles
|
||||
│ │ ├── error.css # Error page styles
|
||||
│ │ ├── retro-refresh.css # Floating refresh bar styles
|
||||
│ │ └── theme-toggle.css # Theme toggle styles
|
||||
@ -233,6 +262,7 @@ DeepSea-Dashboard/
|
||||
│ ├── workers.js # Workers page functionality
|
||||
│ ├── blocks.js # Blocks page functionality
|
||||
│ ├── notifications.js # Notifications functionality
|
||||
│ ├── earnings.js # Earnings page functionality
|
||||
│ ├── block-animation.js # Block mining animation
|
||||
│ ├── BitcoinProgressBar.js # System monitor functionality
|
||||
│ └── theme.js # Theme toggle functionality
|
||||
|
@ -3,5 +3,6 @@
|
||||
"power_usage": 0.0,
|
||||
"wallet": "yourwallethere",
|
||||
"timezone": "America/Los_Angeles",
|
||||
"network_fee": 0.0
|
||||
"network_fee": 0.0,
|
||||
"currency": "USD"
|
||||
}
|
||||
|
25
config.py
25
config.py
@ -18,7 +18,8 @@ def load_config():
|
||||
"power_usage": 0.0,
|
||||
"wallet": "yourwallethere",
|
||||
"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):
|
||||
@ -94,3 +95,25 @@ def get_value(key, default=None):
|
||||
"""
|
||||
config = load_config()
|
||||
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.
|
||||
"""
|
||||
import logging
|
||||
@ -489,6 +489,291 @@ class MiningDashboardService:
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching exchange rates: {e}")
|
||||
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):
|
||||
"""
|
||||
|
6
setup.py
6
setup.py
@ -61,6 +61,7 @@ FILE_MAPPINGS = {
|
||||
'blocks.css': 'static/css/blocks.css',
|
||||
'notifications.css': 'static/css/notifications.css',
|
||||
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
|
||||
'earnings.css': 'static/css/earnings.css', # Added earnings.css
|
||||
|
||||
# JS files
|
||||
'main.js': 'static/js/main.js',
|
||||
@ -69,6 +70,7 @@ FILE_MAPPINGS = {
|
||||
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
|
||||
'notifications.js': 'static/js/notifications.js',
|
||||
'theme.js': 'static/js/theme.js', # Added theme.js
|
||||
'earnings.js': 'static/js/earnings.js', # Added earnings.js
|
||||
|
||||
# Template files
|
||||
'base.html': 'templates/base.html',
|
||||
@ -78,6 +80,7 @@ FILE_MAPPINGS = {
|
||||
'error.html': 'templates/error.html',
|
||||
'blocks.html': 'templates/blocks.html',
|
||||
'notifications.html': 'templates/notifications.html',
|
||||
'earnings.html': 'templates/earnings.html', # Added earnings.html
|
||||
}
|
||||
|
||||
# Default configuration
|
||||
@ -86,7 +89,8 @@ DEFAULT_CONFIG = {
|
||||
"power_usage": 0.0,
|
||||
"wallet": "yourwallethere",
|
||||
"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():
|
||||
|
@ -280,11 +280,12 @@ h1 {
|
||||
|
||||
/* Container */
|
||||
.container-fluid {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
position: relative;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
position: relative;
|
||||
padding-bottom: 6rem;
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
.nav-badge {
|
||||
background-color: var(--primary-color);
|
||||
@ -495,3 +504,4 @@ h1 {
|
||||
margin-left: 5px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
|
@ -182,10 +182,6 @@
|
||||
box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);
|
||||
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 */
|
||||
@keyframes pulse-block-marker {
|
||||
|
560
static/css/earnings.css
Normal file
560
static/css/earnings.css
Normal file
@ -0,0 +1,560 @@
|
||||
/* earnings.css - Enhanced with dashboard styling */
|
||||
|
||||
/* Main section styling */
|
||||
.earnings-section {
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
/* Dashboard title styling */
|
||||
.dashboard-title {
|
||||
color: var(--primary-color);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 1.5rem;
|
||||
font-family: var(--header-font);
|
||||
letter-spacing: 2px;
|
||||
text-shadow: 0 0 10px var(--primary-color, rgba(0, 136, 204, 0.5));
|
||||
}
|
||||
|
||||
/* Stats grid styling for summary cards */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: #000;
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 10px var(--primary-color, rgba(247, 147, 26, 0.2));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stat-card h2 {
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.5rem;
|
||||
text-transform: uppercase;
|
||||
font-family: var(--header-font);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.stat-unit {
|
||||
font-size: 1rem;
|
||||
color: var(--text-color);
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-secondary {
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.stat-time {
|
||||
margin-top: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #00dfff; /* Blue color for time values */
|
||||
}
|
||||
|
||||
/* Card and container styling similar to dashboard */
|
||||
.row.equal-height {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.row.equal-height > [class*="col-"] {
|
||||
display: flex;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.row.equal-height > [class*="col-"] .card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Section headers like dashboard blue headers */
|
||||
.earnings-section h2 {
|
||||
background-color: var(--primary-color, #0088cc);
|
||||
color: #000;
|
||||
padding: 0.5rem;
|
||||
font-weight: bold;
|
||||
font-family: var(--header-font);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 5px var(--primary-color, rgba(0, 136, 204, 0.5));
|
||||
}
|
||||
|
||||
/* Table styling */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
background-color: #000;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 10px var(--primary-color, rgba(247, 147, 26, 0.2));
|
||||
position: relative;
|
||||
}
|
||||
|
||||
/* Add scanline effect to 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;
|
||||
}
|
||||
|
||||
/* Transaction link styling */
|
||||
.tx-link {
|
||||
color: #00dfff; /* Blue color from dashboard */
|
||||
text-decoration: none;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.tx-link:hover {
|
||||
text-decoration: underline;
|
||||
text-shadow: 0 0 5px var(--primary-color, rgba(0, 223, 255, 0.7));
|
||||
}
|
||||
|
||||
/* Status indicators with color families similar to dashboard */
|
||||
.status-indicator {
|
||||
display: inline-block;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-confirmed {
|
||||
background-color: rgba(75, 181, 67, 0.15);
|
||||
color: #32CD32; /* Matched with green color family from dashboard */
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
background-color: rgba(247, 147, 26, 0.15);
|
||||
color: #ffd700; /* Matched with yellow color family from dashboard */
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background-color: rgba(52, 152, 219, 0.15);
|
||||
color: #00dfff; /* Matched with blue metrics from dashboard */
|
||||
}
|
||||
|
||||
/* Metric styling by category - matching dashboard */
|
||||
.metric-value {
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Yellow color family - for BTC/sats values */
|
||||
#unpaid-sats,
|
||||
#total-paid-sats,
|
||||
.earnings-table td:nth-child(4) {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
#unpaid-btc,
|
||||
#total-paid-btc,
|
||||
.earnings-table td:nth-child(3) {
|
||||
color: #ffd700;
|
||||
}
|
||||
|
||||
/* Green color family - for earnings/profits */
|
||||
#total-paid-usd {
|
||||
color: #32CD32;
|
||||
}
|
||||
|
||||
/* Green color for fiat currency amount */
|
||||
#total-paid-fiat {
|
||||
color: #32CD32; /* Using the same green already in your theme */
|
||||
}
|
||||
|
||||
.earnings-table td:nth-child(5) {
|
||||
color: #32CD32; /* USD values */
|
||||
}
|
||||
|
||||
/* Red color family - for fees/costs */
|
||||
.earnings-fee,
|
||||
.earnings-cost {
|
||||
color: #ff5555 !important;
|
||||
}
|
||||
|
||||
/* Styling for date fields */
|
||||
.earnings-table td:nth-child(1) {
|
||||
color: #00dfff; /* Blue date indicators */
|
||||
}
|
||||
|
||||
/* Add bounce animations from dashboard */
|
||||
@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;
|
||||
}
|
||||
|
||||
.arrow {
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
/* Add pulse animation for new payments */
|
||||
@keyframes pulse-highlight {
|
||||
0% {
|
||||
background-color: color-mix(in srgb, var(--primary-color, rgba(247, 147, 26, 0.1)) 10%, transparent);
|
||||
}
|
||||
|
||||
50% {
|
||||
background-color: color-mix(in srgb, var(--primary-color, rgba(247, 147, 26, 0.3)) 30%, transparent);
|
||||
}
|
||||
|
||||
100% {
|
||||
background-color: color-mix(in srgb, var(--primary-color, rgba(247, 147, 26, 0.1)) 10%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
.new-payment {
|
||||
animation: pulse-highlight 2s infinite;
|
||||
}
|
||||
|
||||
/* Card body styling consistency */
|
||||
.card-body strong {
|
||||
color: var(--primary-color);
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.card-body p {
|
||||
margin: 0.25rem 0;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
/* Monospace styling for numerical values */
|
||||
#payment-count {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Font styling for Latest payment */
|
||||
#latest-payment {
|
||||
color: #00dfff;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Status label specific styling with uppercase text */
|
||||
.status-confirmed,
|
||||
.status-pending,
|
||||
.status-processing {
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Empty state styling */
|
||||
.earnings-table tr td[colspan] {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: var(--text-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Pool luck indicators - reused from dashboard */
|
||||
.very-lucky {
|
||||
color: #32CD32 !important;
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
.lucky {
|
||||
color: #90EE90 !important;
|
||||
}
|
||||
|
||||
.normal-luck {
|
||||
color: #ffd700 !important;
|
||||
}
|
||||
|
||||
.unlucky {
|
||||
color: #ff5555 !important;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.earnings-table th, .earnings-table td {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.3rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Chart container styling for future charts */
|
||||
.chart-container {
|
||||
background-color: #000;
|
||||
padding: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
height: 230px;
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 10px var(--primary-color, rgba(247, 147, 26, 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;
|
||||
}
|
||||
|
||||
.settings-info {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 15px;
|
||||
font-size: 0.9em;
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.setting-item strong {
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.currency-symbol {
|
||||
display: inline-block;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* Table styling - Enhanced for mobile */
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin: 1rem 0;
|
||||
background-color: #000;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid var(--primary-color);
|
||||
box-shadow: 0 0 10px var(--primary-color, rgba(247, 147, 26, 0.2));
|
||||
position: relative;
|
||||
/* Add momentum-based scrolling for touch devices */
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Responsive adjustments - Expanded */
|
||||
@media (max-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.earnings-table th, .earnings-table td {
|
||||
padding: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
font-size: 0.7rem;
|
||||
padding: 0.15rem 0.3rem;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
/* Additional mobile optimizations */
|
||||
.tx-link {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.settings-info {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
margin-left: 0;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Small phone screens */
|
||||
@media (max-width: 480px) {
|
||||
/* Convert payment history table to card-like layout for very small screens */
|
||||
.earnings-table thead {
|
||||
display: none; /* Hide table headers */
|
||||
}
|
||||
|
||||
.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, #333);
|
||||
}
|
||||
|
||||
.earnings-table td:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Add labels for each cell */
|
||||
.earnings-table td::before {
|
||||
content: attr(data-label);
|
||||
font-weight: bold;
|
||||
width: 40%;
|
||||
color: var(--primary-color);
|
||||
margin-right: 5%;
|
||||
}
|
||||
|
||||
/* Specific adjustments for payment history */
|
||||
#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 */
|
||||
#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: ";
|
||||
}
|
||||
|
||||
/* Adjust empty state */
|
||||
.earnings-table tr td[colspan] {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* Fix truncated transaction links */
|
||||
.tx-link {
|
||||
word-break: break-all;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
@ -330,7 +330,7 @@
|
||||
}
|
||||
/* Add extra padding at bottom of worker grid to avoid overlap */
|
||||
.worker-grid {
|
||||
margin-bottom: 120px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* 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
|
||||
// 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);
|
||||
|
||||
// Standardize unit handling with a lookup table
|
||||
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)
|
||||
const unitConversions = {
|
||||
'ph/s': 1000,
|
||||
'p/s': 1000,
|
||||
'p': 1000,
|
||||
'petahash': 1000,
|
||||
'petahash/s': 1000,
|
||||
'peta': 1000,
|
||||
// Zettahash (ZH/s) - 1 ZH/s = 1,000,000,000 TH/s
|
||||
'zh/s': 1000000000,
|
||||
'z/s': 1000000000,
|
||||
'z': 1000000000,
|
||||
'zettahash': 1000000000,
|
||||
'zettahash/s': 1000000000,
|
||||
'zetta': 1000000000,
|
||||
|
||||
// Exahash (EH/s) - 1 EH/s = 1,000,000 TH/s
|
||||
'eh/s': 1000000,
|
||||
'e/s': 1000000,
|
||||
'e': 1000000,
|
||||
@ -370,6 +387,15 @@ function normalizeHashrate(value, unit) {
|
||||
'exahash/s': 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,
|
||||
't/s': 1,
|
||||
't': 1,
|
||||
@ -377,6 +403,7 @@ function normalizeHashrate(value, unit) {
|
||||
'terahash/s': 1,
|
||||
'tera': 1,
|
||||
|
||||
// Gigahash (GH/s) - 1 TH/s = 1,000 GH/s
|
||||
'gh/s': 1 / 1000,
|
||||
'g/s': 1 / 1000,
|
||||
'g': 1 / 1000,
|
||||
@ -384,6 +411,7 @@ function normalizeHashrate(value, unit) {
|
||||
'gigahash/s': 1 / 1000,
|
||||
'giga': 1 / 1000,
|
||||
|
||||
// Megahash (MH/s) - 1 TH/s = 1,000,000 MH/s
|
||||
'mh/s': 1 / 1000000,
|
||||
'm/s': 1 / 1000000,
|
||||
'm': 1 / 1000000,
|
||||
@ -391,6 +419,7 @@ function normalizeHashrate(value, unit) {
|
||||
'megahash/s': 1 / 1000000,
|
||||
'mega': 1 / 1000000,
|
||||
|
||||
// Kilohash (KH/s) - 1 TH/s = 1,000,000,000 KH/s
|
||||
'kh/s': 1 / 1000000000,
|
||||
'k/s': 1 / 1000000000,
|
||||
'k': 1 / 1000000000,
|
||||
@ -398,44 +427,72 @@ function normalizeHashrate(value, unit) {
|
||||
'kilohash/s': 1 / 1000000000,
|
||||
'kilo': 1 / 1000000000,
|
||||
|
||||
// Hash (H/s) - 1 TH/s = 1,000,000,000,000 H/s
|
||||
'h/s': 1 / 1000000000000,
|
||||
'h': 1 / 1000000000000,
|
||||
'hash': 1 / 1000000000000,
|
||||
'hash/s': 1 / 1000000000000
|
||||
};
|
||||
|
||||
// Try to find the conversion factor
|
||||
let conversionFactor = null;
|
||||
let matchedUnit = null;
|
||||
|
||||
// First try direct lookup
|
||||
// Direct lookup for exact matches
|
||||
if (unitConversions.hasOwnProperty(unit_normalized)) {
|
||||
conversionFactor = unitConversions[unit_normalized];
|
||||
matchedUnit = unit_normalized;
|
||||
} else {
|
||||
// If direct lookup fails, try a fuzzy match
|
||||
// Fuzzy matching for non-exact matches
|
||||
for (const knownUnit in unitConversions) {
|
||||
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];
|
||||
matchedUnit = knownUnit;
|
||||
|
||||
if (debug) {
|
||||
console.log(`Fuzzy matching unit: "${unit}" → interpreted as "${knownUnit}" (conversion: ${unitConversions[knownUnit]})`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no conversion factor found, assume TH/s but log a warning
|
||||
// Handle unknown units
|
||||
if (conversionFactor === null) {
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
return value * conversionFactor;
|
||||
// Calculate normalized value
|
||||
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
|
||||
@ -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) {
|
||||
// Validate history data
|
||||
try {
|
||||
// Log 60sec data
|
||||
console.log("60sec history data received:", data.arrow_history.hashrate_60sec);
|
||||
const perfStart = performance.now(); // Performance measurement
|
||||
|
||||
// Also log 3hr data if available
|
||||
if (data.arrow_history.hashrate_3hr) {
|
||||
console.log("3hr history data received:", data.arrow_history.hashrate_3hr);
|
||||
// Determine which history data to use (3hr or 60sec) with proper fallback
|
||||
let historyData;
|
||||
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 {
|
||||
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
|
||||
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");
|
||||
console.log(`Using ${dataSource} history data with ${historyData?.length || 0} points`);
|
||||
|
||||
if (historyData && historyData.length > 0) {
|
||||
// Format time labels
|
||||
chart.data.labels = historyData.map(item => {
|
||||
// Pre-process history data to filter out invalid entries
|
||||
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;
|
||||
try {
|
||||
// Parse and format the time (existing code)...
|
||||
let timeParts;
|
||||
// Parse time efficiently
|
||||
let hours = 0, minutes = 0, seconds = 0;
|
||||
|
||||
if (timeStr.length === 8 && timeStr.indexOf(':') !== -1) {
|
||||
// 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) {
|
||||
// Format: HH:MM
|
||||
timeParts = timeStr.split(':');
|
||||
timeParts.push('00'); // Add seconds
|
||||
const parts = timeStr.split(':');
|
||||
hours = parseInt(parts[0], 10);
|
||||
minutes = parseInt(parts[1], 10);
|
||||
} 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
|
||||
const now = new Date();
|
||||
const timeDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(),
|
||||
parseInt(timeParts[0]), parseInt(timeParts[1]), parseInt(timeParts[2] || 0));
|
||||
// Create time date with validation
|
||||
if (isNaN(hours) || isNaN(minutes) || isNaN(seconds) ||
|
||||
hours < 0 || hours > 23 || minutes < 0 || minutes > 59 || seconds < 0 || seconds > 59) {
|
||||
return timeStr; // Use original for invalid times
|
||||
}
|
||||
|
||||
let formattedTime = timeDate.toLocaleTimeString('en-US', {
|
||||
timeZone: dashboardTimezone || 'America/Los_Angeles',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
const timeDate = new Date(yearMonthDay.year, yearMonthDay.month, yearMonthDay.day,
|
||||
hours, minutes, seconds);
|
||||
|
||||
// Remove the AM/PM part
|
||||
formattedTime = formattedTime.replace(/\s[AP]M$/i, '');
|
||||
return formattedTime;
|
||||
// Format using the formatter
|
||||
const formatted = timeFormatter.format(timeDate);
|
||||
return formatted.replace(/\s[AP]M$/i, ''); // Remove AM/PM
|
||||
} catch (e) {
|
||||
console.error("Error formatting time:", e, timeStr);
|
||||
console.error("Time formatting error:", e);
|
||||
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 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 {
|
||||
const item = validHistoryData[i];
|
||||
|
||||
// Safety conversion in case value is a string
|
||||
const val = parseFloat(item.value || 0);
|
||||
if (isNaN(val)) {
|
||||
console.warn(`Invalid value at index ${index}: ${item.value}`);
|
||||
return 0;
|
||||
const val = parseFloat(item.value);
|
||||
|
||||
// Get unit with better validation
|
||||
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) {
|
||||
console.error(`Error processing hashrate at index ${index}:`, err);
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
|
||||
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`);
|
||||
console.error(`Error processing hashrate at index ${i}:`, err);
|
||||
validatedData[i] = 0; // Use 0 as a safe fallback
|
||||
}
|
||||
}
|
||||
|
||||
// Update chart dataset label to indicate which average we're displaying
|
||||
chart.data.datasets[0].label = useHashrate3hr ?
|
||||
'Hashrate Trend (3HR AVG)' : 'Hashrate Trend (60SEC AVG)';
|
||||
// Assign the processed data to chart
|
||||
chart.data.datasets[0].data = validatedData;
|
||||
chart.originalData = validHistoryData; // Store for tooltip reference
|
||||
|
||||
// Calculate appropriate y-axis range with safeguards for outliers and ensure 24hr avg line is visible
|
||||
const values = chart.data.datasets[0].data.filter(v => !isNaN(v) && v !== null && v > 0);
|
||||
if (values.length > 0) {
|
||||
const max = Math.max(...values);
|
||||
const min = Math.min(...values) || 0;
|
||||
// Update tooltip callback to display proper units
|
||||
chart.options.plugins.tooltip.callbacks.label = function (context) {
|
||||
const index = context.dataIndex;
|
||||
const originalData = chart.originalData?.[index];
|
||||
|
||||
// MODIFICATION: When in low hashrate mode, ensure the y-axis includes the 24hr average
|
||||
if (useHashrate3hr && normalizedAvg > 0) {
|
||||
// Ensure the 24-hour average is visible on the chart
|
||||
const yMin = Math.min(min * 0.8, normalizedAvg * 0.5);
|
||||
const yMax = Math.max(max * 1.2, normalizedAvg * 1.5);
|
||||
|
||||
chart.options.scales.y.min = yMin;
|
||||
chart.options.scales.y.max = yMax;
|
||||
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);
|
||||
if (originalData) {
|
||||
if (originalData.storageValue !== undefined && originalData.storageUnit) {
|
||||
// Use the optimized storage value/unit if available
|
||||
return `HASHRATE: ${originalData.storageValue} ${originalData.storageUnit.toUpperCase()}`;
|
||||
}
|
||||
else if (originalData.originalValue !== undefined && originalData.originalUnit) {
|
||||
// Fall back to original values
|
||||
return `HASHRATE: ${originalData.originalValue} ${originalData.originalUnit.toUpperCase()}`;
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
chart.options.scales.y.ticks.stepSize = stepSize;
|
||||
|
||||
// Log the chosen stepSize for debugging
|
||||
console.log(`Y-axis range: ${range.toFixed(2)}, using stepSize: ${stepSize}`);
|
||||
// Log the chosen stepSize
|
||||
console.log(`Y-axis range: ${range.toFixed(2)} TH/s, using stepSize: ${stepSize} (target ticks: ${targetTicks})`);
|
||||
}
|
||||
} else {
|
||||
console.warn("No valid history data items available");
|
||||
console.warn("No history data items available");
|
||||
useSingleDataPoint();
|
||||
}
|
||||
} catch (historyError) {
|
||||
console.error("Error processing hashrate history data:", historyError);
|
||||
@ -1494,6 +1662,48 @@ function updateChartWithNormalizedData(chart, data) {
|
||||
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
|
||||
function useSingleDataPoint() {
|
||||
try {
|
||||
|
@ -227,84 +227,131 @@ function hideLoader() {
|
||||
$("#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) {
|
||||
console.log("Fetching worker data...");
|
||||
lastManualRefreshTime = Date.now();
|
||||
$('#worker-grid').addClass('loading-fade');
|
||||
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 = [];
|
||||
|
||||
// Create requests for pages 1 through maxPages concurrently
|
||||
for (let page = 1; page <= maxPages; page++) {
|
||||
const apiUrl = `/api/workers?page=${page}${forceRefresh ? '&force=true' : ''}`;
|
||||
requests.push($.ajax({
|
||||
url: apiUrl,
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
timeout: 15000
|
||||
}));
|
||||
for (let page = startPage; page <= endPage; page++) {
|
||||
requests.push(
|
||||
$.ajax({
|
||||
url: `/api/workers?page=${page}`,
|
||||
method: 'GET',
|
||||
dataType: 'json',
|
||||
timeout: 15000
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Process all requests concurrently
|
||||
Promise.all(requests)
|
||||
.then(pages => {
|
||||
let allWorkers = [];
|
||||
let aggregatedData = null;
|
||||
|
||||
pages.forEach((data, i) => {
|
||||
if (data && data.workers && data.workers.length > 0) {
|
||||
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}`);
|
||||
// Process each page
|
||||
pages.forEach(pageData => {
|
||||
if (pageData && pageData.workers && pageData.workers.length > 0) {
|
||||
// Append new workers to our list efficiently
|
||||
workerData.workers = workerData.workers.concat(pageData.workers);
|
||||
}
|
||||
});
|
||||
|
||||
// Deduplicate workers if necessary (using worker.name as unique key)
|
||||
const uniqueWorkers = allWorkers.filter((worker, index, self) =>
|
||||
index === self.findIndex((w) => w.name === worker.name)
|
||||
);
|
||||
// Update progress
|
||||
const progress = Math.min(endPage / totalPages * 100, 100);
|
||||
progressBar.find('.progress-bar').css('width', progress + '%');
|
||||
progressBar.find('.progress-text').text(`Loading workers: ${endPage}/${totalPages}`);
|
||||
|
||||
workerData = aggregatedData || {};
|
||||
workerData.workers = uniqueWorkers;
|
||||
|
||||
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
|
||||
BitcoinMinuteRefresh.notifyRefresh();
|
||||
if (endPage < totalPages) {
|
||||
// Continue with next batch
|
||||
setTimeout(() => loadWorkerPages(endPage + 1, totalPages, progressBar), 100);
|
||||
} else {
|
||||
// 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 => {
|
||||
console.error("Error fetching worker data:", error);
|
||||
})
|
||||
.finally(() => {
|
||||
hideLoader();
|
||||
console.error(`Error fetching worker pages ${startPage}-${endPage}:`, error);
|
||||
// Continue with what we have so far
|
||||
finishWorkerLoad();
|
||||
});
|
||||
}
|
||||
|
||||
// Refresh worker data every 60 seconds
|
||||
setInterval(function () {
|
||||
console.log("Refreshing worker data at " + new Date().toLocaleTimeString());
|
||||
fetchWorkerData();
|
||||
}, 60000);
|
||||
// Finish loading process with optimized rendering
|
||||
function finishWorkerLoad() {
|
||||
// Deduplicate workers more efficiently with a Map
|
||||
const uniqueWorkersMap = new Map();
|
||||
workerData.workers.forEach(worker => {
|
||||
if (worker.name) {
|
||||
uniqueWorkersMap.set(worker.name, worker);
|
||||
}
|
||||
});
|
||||
workerData.workers = Array.from(uniqueWorkersMap.values());
|
||||
|
||||
// Update the worker grid with data
|
||||
function updateWorkerGrid() {
|
||||
console.log("Updating worker grid...");
|
||||
// Notify BitcoinMinuteRefresh
|
||||
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
|
||||
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) {
|
||||
console.error("No worker data available");
|
||||
return;
|
||||
@ -325,14 +372,114 @@ function updateWorkerGrid() {
|
||||
return;
|
||||
}
|
||||
|
||||
filteredWorkers.forEach(worker => {
|
||||
const card = createWorkerCard(worker);
|
||||
workerGrid.append(card);
|
||||
});
|
||||
// Performance optimization for large lists
|
||||
if (filteredWorkers.length > 200) {
|
||||
// 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
|
||||
function createWorkerCard(worker) {
|
||||
// Render a batch of workers efficiently
|
||||
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>');
|
||||
|
||||
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="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 hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
|
||||
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
|
||||
let formattedLastShare = 'N/A';
|
||||
if (worker.last_share && typeof worker.last_share === 'string') {
|
||||
// This is a more reliable method for timezone conversion
|
||||
try {
|
||||
// The worker.last_share is likely in format "YYYY-MM-DD HH:MM"
|
||||
// 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
|
||||
const dateWithoutTZ = new Date(worker.last_share + 'Z');
|
||||
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
@ -374,8 +514,7 @@ function createWorkerCard(worker) {
|
||||
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
||||
});
|
||||
} catch (e) {
|
||||
console.error("Error formatting last share time:", e, worker.last_share);
|
||||
formattedLastShare = worker.last_share; // Fallback to original value
|
||||
formattedLastShare = worker.last_share;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3,7 +3,7 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}BTC-OS DASHBOARD {% endblock %}</title>
|
||||
<title>{% block title %}BTC-OS Dashboard{% endblock %}</title>
|
||||
|
||||
<!-- Common fonts -->
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
|
||||
@ -116,6 +116,7 @@
|
||||
<div class="navigation-links">
|
||||
<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="/earnings" class="nav-link {% block earnings_active %}{% endblock %}">EARNINGS</a>
|
||||
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">BLOCKS</a>
|
||||
<a href="/notifications" class="nav-link {% block notifications_active %}{% endblock %}">
|
||||
NOTIFICATIONS
|
||||
@ -135,7 +136,7 @@
|
||||
<!-- Footer -->
|
||||
<footer class="footer text-center">
|
||||
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
|
||||
<p>v0.9.2 - Public Beta</p>
|
||||
<p>v0.9.3 - Public Beta</p>
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD {% endblock %}
|
||||
{% block title %}BLOCKS - BTC-OS Dashboard {% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/static/css/blocks.css">
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}BTC-OS Mining Dashboard {% endblock %}
|
||||
{% block title %}BTC-OS Dashboard {% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||
|
150
templates/earnings.html
Normal file
150
templates/earnings.html
Normal file
@ -0,0 +1,150 @@
|
||||
{% 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">
|
||||
|
||||
<!-- 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>
|
||||
|
||||
<!-- 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-indicator status-{{ payment.status }}">{{ payment.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="5">No payment history available</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</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>
|
||||
<meta charset="utf-8">
|
||||
<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 -->
|
||||
<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">
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD {% endblock %}
|
||||
{% block title %}NOTIFICATIONS - BTC-OS Dashboard{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/static/css/notifications.css">
|
||||
|
@ -1,6 +1,6 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}WORKERS - BTC-OS MINING DASHBOARD {% endblock %}
|
||||
{% block title %}WORKERS - BTC-OS Dashboard{% endblock %}
|
||||
|
||||
{% block css %}
|
||||
<link rel="stylesheet" href="/static/css/workers.css">
|
||||
|
@ -54,33 +54,33 @@ class WorkerService:
|
||||
|
||||
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:
|
||||
cached_metrics (dict): Cached metrics from the dashboard
|
||||
force_refresh (bool): Whether to force a refresh of cached data
|
||||
|
||||
Returns:
|
||||
dict: Worker data
|
||||
dict: Worker data with standardized hashrates
|
||||
"""
|
||||
current_time = datetime.now().timestamp()
|
||||
|
||||
|
||||
# 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 \
|
||||
(current_time - self.last_worker_data_update) < self.WORKER_DATA_CACHE_TIMEOUT:
|
||||
# Even when using cached data, sync worker count with main dashboard
|
||||
if cached_metrics and cached_metrics.get("workers_hashing") is not None:
|
||||
self.sync_worker_counts_with_dashboard(self.worker_data_cache, cached_metrics)
|
||||
|
||||
|
||||
logging.info("Using cached worker data")
|
||||
return self.worker_data_cache
|
||||
|
||||
|
||||
try:
|
||||
# First try to get actual worker data from the dashboard service
|
||||
if self.dashboard_service:
|
||||
logging.info("Attempting to fetch real worker data from Ocean.xyz")
|
||||
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:
|
||||
# Validate that worker names are not just "Online" or "Offline"
|
||||
valid_names = False
|
||||
@ -89,22 +89,22 @@ class WorkerService:
|
||||
if name and name not in ['online', 'offline', 'total', 'worker', 'status']:
|
||||
valid_names = True
|
||||
break
|
||||
|
||||
|
||||
if valid_names:
|
||||
logging.info(f"Successfully retrieved {len(real_worker_data['workers'])} real workers from Ocean.xyz")
|
||||
|
||||
|
||||
# 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"):
|
||||
real_worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
||||
|
||||
|
||||
# Sync with dashboard metrics to ensure consistency
|
||||
if cached_metrics:
|
||||
self.sync_worker_counts_with_dashboard(real_worker_data, cached_metrics)
|
||||
|
||||
|
||||
# Update cache
|
||||
self.worker_data_cache = real_worker_data
|
||||
self.last_worker_data_update = current_time
|
||||
|
||||
|
||||
return real_worker_data
|
||||
else:
|
||||
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")
|
||||
else:
|
||||
logging.warning("Dashboard service not available, cannot fetch real worker data")
|
||||
|
||||
|
||||
# Fallback to simulated data if real data fetch fails or returns no workers
|
||||
logging.info("Generating fallback simulated worker data")
|
||||
worker_data = self.generate_fallback_data(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"):
|
||||
worker_data["hashrate_history"] = cached_metrics["arrow_history"]["hashrate_3hr"]
|
||||
|
||||
|
||||
# Ensure worker counts match dashboard metrics
|
||||
if cached_metrics:
|
||||
self.sync_worker_counts_with_dashboard(worker_data, cached_metrics)
|
||||
|
||||
|
||||
# Update cache
|
||||
self.worker_data_cache = worker_data
|
||||
self.last_worker_data_update = current_time
|
||||
|
||||
|
||||
logging.info(f"Successfully generated fallback worker data: {worker_data['workers_total']} workers")
|
||||
return worker_data
|
||||
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting worker data: {e}")
|
||||
fallback_data = self.generate_fallback_data(cached_metrics)
|
||||
|
||||
|
||||
# Even on error, try to sync with dashboard metrics
|
||||
if cached_metrics:
|
||||
self.sync_worker_counts_with_dashboard(fallback_data, cached_metrics)
|
||||
|
||||
|
||||
return fallback_data
|
||||
|
||||
def sync_worker_counts_with_dashboard(self, worker_data, dashboard_metrics):
|
||||
"""
|
||||
Synchronize worker counts and other metrics between worker data and dashboard metrics.
|
||||
|
||||
|
||||
Args:
|
||||
worker_data (dict): Worker data to be updated
|
||||
dashboard_metrics (dict): Dashboard metrics with worker count and other data
|
||||
"""
|
||||
if not worker_data or not dashboard_metrics:
|
||||
return
|
||||
|
||||
|
||||
# Sync worker count
|
||||
dashboard_worker_count = dashboard_metrics.get("workers_hashing")
|
||||
|
||||
|
||||
# Only proceed if dashboard has valid worker count
|
||||
if dashboard_worker_count is not None:
|
||||
current_worker_count = worker_data.get("workers_total", 0)
|
||||
|
||||
|
||||
# If counts already match, no need to sync workers count
|
||||
if current_worker_count != dashboard_worker_count:
|
||||
logging.info(f"Syncing worker count: worker page({current_worker_count}) → dashboard({dashboard_worker_count})")
|
||||
|
||||
|
||||
# Update the total count
|
||||
worker_data["workers_total"] = dashboard_worker_count
|
||||
|
||||
|
||||
# Adjust online/offline counts proportionally
|
||||
current_online = worker_data.get("workers_online", 0)
|
||||
current_total = max(1, current_worker_count) # Avoid division by zero
|
||||
|
||||
|
||||
# Calculate ratio of online workers
|
||||
online_ratio = current_online / current_total
|
||||
|
||||
|
||||
# Recalculate online and offline counts
|
||||
new_online_count = round(dashboard_worker_count * online_ratio)
|
||||
new_offline_count = dashboard_worker_count - new_online_count
|
||||
|
||||
|
||||
# Update the counts
|
||||
worker_data["workers_online"] = new_online_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}")
|
||||
|
||||
|
||||
# If we have worker instances, try to adjust them as well
|
||||
if "workers" in worker_data and isinstance(worker_data["workers"], list):
|
||||
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
|
||||
if dashboard_metrics.get("daily_mined_sats") is not None:
|
||||
daily_sats_value = dashboard_metrics.get("daily_mined_sats")
|
||||
if daily_sats_value != worker_data.get("daily_sats"):
|
||||
worker_data["daily_sats"] = daily_sats_value
|
||||
logging.info(f"Synced daily sats: {worker_data['daily_sats']}")
|
||||
|
||||
|
||||
# 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:
|
||||
# Attempt to convert string to float if needed
|
||||
unpaid_value = dashboard_metrics.get("unpaid_earnings")
|
||||
|
Loading…
Reference in New Issue
Block a user