Compare commits

...

25 Commits

Author SHA1 Message Date
09cf1cd17f
new path 2025-04-28 11:14:27 +02:00
DJObleezy
b92f8074da Enhance README.md with new features and guidance
Updated README.md to include:
- Multi-Currency Support with configuration options and notifications
- Expanded Worker Management section with new API endpoints
- Added preferred fiat currency to environment variables in `docker-compose.yml`
- New API endpoints for managing notifications and fetching exchange rates
- Additional user guidance for currency and timezone settings
2025-04-27 17:10:11 -07:00
DJObleezy
923a4826a9 Update footer version to v0.9.2
Updated the version number in the footer of the `base.html` file from "v0.9.1 - Public Beta" to "v0.9.2 - Public Beta".
2025-04-27 16:59:23 -07:00
DJObleezy
9337dd49d3 Add read_only parameter to notification clearing API
Updated the `api_clear_notifications` function to include a `read_only` parameter, allowing conditional clearing of notifications based on their read status.

Modified the filtering logic in the `NotificationService` to retain notifications that match the specified category, are newer than a cutoff date, or are unread when `read_only` is true. This enhances the granularity of the notification clearing process.
2025-04-27 16:58:48 -07:00
DJObleezy
f7b2e550f6 Enhance notification clearing and update UI elements
- Added `read_only` parameter to `clear_notifications` method in `NotificationService` to filter unread notifications.
- Updated method docstring to reflect the new parameter.
- Changed page title and header from "BTC-OS MINING DASHBOARD" to "BTC-OS DASHBOARD".
- Incremented version number in footer from "v0.8.8" to "v0.9.1".
2025-04-27 16:46:51 -07:00
DJObleezy
39f983563a
Update README.md 2025-04-27 15:10:14 -07:00
DJObleezy
a50bd552f7 Add currency support and config management enhancements
This commit introduces significant updates to the application, focusing on currency support and improved configuration management. Key changes include:

- Added a `config_reset` flag in `update_metrics_job` in `App.py` to manage configuration resets.
- Modified `update_config` to handle currency changes and update notifications accordingly.
- Updated `MiningDashboardService` to fetch and include exchange rates and configured currency in metrics.
- Introduced utility functions for currency formatting and symbol retrieval in `notification_service.py`.
- Enhanced UI components in `main.js` and `boot.html` to support currency selection and display.
- Adjusted Docker Compose file to remove hardcoded wallet and power settings for better flexibility.

These changes enhance usability by allowing users to view financial data in their preferred currency and manage configurations more effectively.
2025-04-27 14:43:45 -07:00
DJObleezy
2f1cbd3143 Enhance hashrate detection and UI updates
Updated `NotificationService` to distinguish between low and normal hashrate modes, utilizing 3-hour and 10-minute averages for detection. Improved `updateChartWithNormalizedData` in `main.js` to support localStorage persistence for hashrate state and refined mode-switching logic. Introduced `showHashrateNormalizeNotice` for user notifications regarding hashrate normalization. Updated HTML files for UI consistency, including version number and structured display of pool fees and unpaid earnings. Ensured proper chart updates and annotations for 24-hour averages.
2025-04-27 11:18:42 -07:00
DJObleezy
f45ce8a1e8 Refactor footer styles and adjust container padding
- Removed margin-top from `.footer` in `common.css`, affecting spacing above the footer.
- Deleted `<style>` tag for theme preloading, which may impact theme loading behavior.
- Increased `padding-bottom` in `.container-fluid` from 60px to 100px in `dashboard.css` to accommodate a minimized system monitor.
2025-04-26 20:50:50 -07:00
DJObleezy
dec0045244 Update footer in base.html to include version info
Added a new paragraph to the footer displaying the version number "v0.8.7 - Public Beta".
2025-04-26 20:46:37 -07:00
DJObleezy
744ed27279 Add Alt+W shortcut to reset wallet address and chart
Implemented a keyboard event listener for Alt+W to reset the wallet address, clear chart data, and redirect to the configuration page with a confirmation dialog. Added a fallback function for wallet reset if chart data clearing fails. Updated the chart's Y-axis label to 'HASHRATE (TH/S)' and commented out the theme toggle button in the HTML for cleaner implementation.
2025-04-26 20:44:36 -07:00
DJObleezy
b4465e5a5c Revert "Update UI and functionality for wallet and fees"
This reverts commit 9ef1260347.
2025-04-26 20:39:05 -07:00
DJObleezy
9ef1260347 Update UI and functionality for wallet and fees
- Adjust footer CSS and increase padding in dashboard.
- Update chart title for clarity on hashrate units.
- Implement Alt+W keyboard shortcut to reset wallet address with confirmation and error handling.
- Add version number to footer in base.html.
- Change "Network Fee (%)" label to "Firmware Fee (%)" with updated tooltip in boot.html.
2025-04-26 20:35:27 -07:00
DJObleezy
57d8a9ab45 Refactor pool fee calculation to use last block earnings
Updated `calculatePoolFeeInSats` to accept `lastBlockEarnings` instead of `estimatedEarningsPerDay`. Added debugging logs for pool fee percentage and last block earnings. Modified `updateUI` to check for `last_block_earnings` and parse its value accordingly.
2025-04-26 17:36:51 -07:00
DJObleezy
b253e5aa7c Update .datum-label color to cyan
Changed the color of the `.datum-label` class from white (`#ffffff`) to cyan. Updated the corresponding comment to reflect the new color.
2025-04-26 16:14:29 -07:00
DJObleezy
0ab96cb7c1 Enhance pool fee display in SATS
- Added CSS rule to style pool fees in SATS as red and bold.
- Implemented `calculatePoolFeeInSats` function to compute fees based on percentage and earnings.
- Updated `updateUI` to display calculated pool fees, ensuring proper formatting and element creation.
2025-04-26 16:03:56 -07:00
DJObleezy
06f5c646e2 Enhance chart y-axis for low hashrate visibility
Updated `updateChartWithNormalizedData` to ensure the 24-hour average line is visible in low hashrate mode. Adjusted y-axis range calculations for both multi-point and single-point scenarios. Added console log statements for better debugging and feedback.
2025-04-26 15:31:31 -07:00
DJObleezy
367ba3788f Improve theme handling in BitcoinProgressBar.js
- Introduced fallback colors and utilized CSS variables for styling.
- Replaced hardcoded color values with dynamic CSS variable values.
- Enhanced theme change listener to respond to storage changes and class mutations for real-time UI updates.
2025-04-26 15:23:25 -07:00
DJObleezy
574d5637bc Refactor hashrate handling in main.js
Enhanced `updateChartWithNormalizedData` to manage low and high hashrate modes with a new state management system. Implemented debounce for mode switching and added hysteresis to prevent rapid transitions. Improved tracking of hashrate spikes for better handling of fluctuations.
2025-04-26 08:30:48 -07:00
DJObleezy
e9825c9006 Refactor hashrate normalization and improve error handling
- Refactored `normalizeHashrate` function to use a lookup table for unit conversion, enhancing error handling and logging for unrecognized units.
- Updated `ArrowIndicator` class to utilize the new global `normalizeHashrate` function for consistency.
- Enhanced `updateChartWithNormalizedData` with better error handling for 24-hour averages and current hashrate data, including checks for unreasonable values.
- Improved historical data handling with validation and anomaly detection.
- Encapsulated single data point display logic in `useSingleDataPoint` for better error management.
- Refined low hashrate indicator display logic to show only when necessary and updated its appearance based on the current theme.
- Overall improvements enhance robustness, maintainability, and user experience.
2025-04-26 06:39:55 -07:00
DJObleezy
24c46f058b Add last block earnings display to UI
Updated `updateUI()` in `main.js` to show last block earnings with a "+" prefix and "SATS" suffix. Modified `dashboard.html` to include a new span element that conditionally displays the earnings or defaults to "+0 SATS".
2025-04-26 06:13:58 -07:00
DJObleezy
e0c5f085cc Enhance data handling and UI responsiveness
- Updated `save_graph_state` in `state_manager.py` to include unit preservation for `arrow_history` and `metrics_log`, with improved data size logging and exception handling.
- Modified `notifications.css` for better mobile responsiveness, including a grid layout for `.filter-buttons` and adjusted button sizes.
- Changed value display in `initializeChart` to use 'PH' for petahashes, aligning with new unit standards.
- Ensured consistent unit formatting in `updateChartWithNormalizedData` for the 24-hour average line.
- Enhanced precision in `updateUI` for unpaid earnings display.
- Simplified y-axis label in chart configuration and updated value formatting to reflect new unit standards.
2025-04-25 22:13:01 -07:00
DJObleezy
312031dcae Add theme styles for Bitcoin and update DeepSea theme
Introduce new theme-specific styles for Bitcoin, including primary color and RGB values. Update DeepSea theme styles and implement a CSS variable for the graph container's shadow. Revise focus styles for theme toggle buttons to align with the new themes.
2025-04-25 17:41:12 -07:00
DJObleezy
0d5ddda2f8 Add theme-specific styles for graph container
Implemented CSS rules for Bitcoin and DeepSea themes,
adding distinct box shadows for the graph container based
on the active theme. Retained existing styling for the
theme toggle button in the Bitcoin theme.
2025-04-25 17:20:33 -07:00
DJObleezy
b9d2c39b85
Update README.md 2025-04-25 05:50:56 -07:00
16 changed files with 1482 additions and 445 deletions

41
App.py
View File

@ -367,12 +367,25 @@ def update_metrics_job(force=False):
metrics = dashboard_service.fetch_metrics() metrics = dashboard_service.fetch_metrics()
if metrics: if metrics:
logging.info("Fetched metrics successfully") logging.info("Fetched metrics successfully")
# Add config_reset flag to metrics from the config
config = load_config()
metrics["config_reset"] = config.get("config_reset", False)
logging.info(f"Added config_reset flag to metrics: {metrics.get('config_reset')}")
# First check for notifications by comparing new metrics with old cached metrics # First check for notifications by comparing new metrics with old cached metrics
notification_service.check_and_generate_notifications(metrics, cached_metrics) notification_service.check_and_generate_notifications(metrics, cached_metrics)
# Then update cached metrics after comparison # Then update cached metrics after comparison
cached_metrics = metrics cached_metrics = metrics
# Clear the config_reset flag after it's been used
if metrics.get("config_reset"):
config = load_config()
if "config_reset" in config:
del config["config_reset"]
save_config(config)
logging.info("Cleared config_reset flag from configuration after use")
# Update state history (only once) # Update state history (only once)
state_manager.update_metrics_history(metrics) state_manager.update_metrics_history(metrics)
@ -771,7 +784,7 @@ def get_config():
@app.route("/api/config", methods=["POST"]) @app.route("/api/config", methods=["POST"])
def update_config(): def update_config():
"""API endpoint to update configuration.""" """API endpoint to update configuration."""
global dashboard_service, worker_service # Add this to access the global dashboard_service global dashboard_service, worker_service
try: try:
# Get the request data # Get the request data
@ -783,11 +796,19 @@ def update_config():
logging.error("Invalid configuration format") logging.error("Invalid configuration format")
return jsonify({"error": "Invalid configuration format"}), 400 return jsonify({"error": "Invalid configuration format"}), 400
# Get current config to check if currency is changing
current_config = load_config()
currency_changed = (
"currency" in new_config and
new_config.get("currency") != current_config.get("currency", "USD")
)
# Required fields and default values # Required fields and default values
defaults = { defaults = {
"wallet": "yourwallethere", "wallet": "yourwallethere",
"power_cost": 0.0, "power_cost": 0.0,
"power_usage": 0.0 "power_usage": 0.0,
"currency": "USD" # Add default currency
} }
# Merge new config with defaults for any missing fields # Merge new config with defaults for any missing fields
@ -802,7 +823,8 @@ def update_config():
dashboard_service = MiningDashboardService( dashboard_service = MiningDashboardService(
new_config.get("power_cost", 0.0), new_config.get("power_cost", 0.0),
new_config.get("power_usage", 0.0), new_config.get("power_usage", 0.0),
new_config.get("wallet") new_config.get("wallet"),
network_fee=new_config.get("network_fee", 0.0) # Include network fee
) )
logging.info(f"Dashboard service reinitialized with new wallet: {new_config.get('wallet')}") logging.info(f"Dashboard service reinitialized with new wallet: {new_config.get('wallet')}")
@ -810,6 +832,15 @@ def update_config():
worker_service.set_dashboard_service(dashboard_service) worker_service.set_dashboard_service(dashboard_service)
logging.info(f"Worker service updated with the new dashboard service") logging.info(f"Worker service updated with the new dashboard service")
# If currency changed, update notifications to use the new currency
if currency_changed:
try:
logging.info(f"Currency changed from {current_config.get('currency', 'USD')} to {new_config['currency']}")
updated_count = notification_service.update_notification_currency()
logging.info(f"Updated {updated_count} notifications to use {new_config['currency']} currency")
except Exception as e:
logging.error(f"Error updating notification currency: {e}")
# Force a metrics update to reflect the new configuration # Force a metrics update to reflect the new configuration
update_metrics_job(force=True) update_metrics_job(force=True)
logging.info("Forced metrics update after configuration change") logging.info("Forced metrics update after configuration change")
@ -1127,10 +1158,12 @@ def api_clear_notifications():
"""API endpoint to clear notifications.""" """API endpoint to clear notifications."""
category = request.json.get('category') category = request.json.get('category')
older_than_days = request.json.get('older_than_days') older_than_days = request.json.get('older_than_days')
read_only = request.json.get('read_only', False) # Get the read_only parameter with default False
cleared_count = notification_service.clear_notifications( cleared_count = notification_service.clear_notifications(
category=category, category=category,
older_than_days=older_than_days older_than_days=older_than_days,
read_only=read_only # Pass the parameter to the method
) )
return jsonify({ return jsonify({

View File

@ -8,9 +8,9 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
## Gallery: ## Gallery:
![DeepSea Boot](https://github.com/user-attachments/assets/77222f13-1e95-48ee-a418-afd0e6b7a920) ![DeepSea Boot](https://github.com/user-attachments/assets/77222f13-1e95-48ee-a418-afd0e6b7a920)
![DeepSea Config](https://github.com/user-attachments/assets/48fcc2a6-f56e-48b9-ac61-b27e9b4a6e41) ![DeepSea Config](https://github.com/user-attachments/assets/e23859bd-76f3-4239-aa6b-060b5bf13f1b)
![DeepSea Dashboard](https://github.com/user-attachments/assets/8a96fd5e-5ba2-4e0e-be83-965ecb046671) ![DeepSea Dashboard](https://github.com/user-attachments/assets/8a96fd5e-5ba2-4e0e-be83-965ecb046671)
![DeepSea Workers](https://github.com/user-attachments/assets/a89b70f2-529e-46d6-b8dc-a140d618fb65) ![DeepSea Workers](https://github.com/user-attachments/assets/075d3f25-bbfb-4e0d-a4d1-1e7f23b96715)
![DeepSea Blocks](https://github.com/user-attachments/assets/078fc533-62c7-4375-bdb4-5f33e4a07925) ![DeepSea Blocks](https://github.com/user-attachments/assets/078fc533-62c7-4375-bdb4-5f33e4a07925)
![DeepSea Notifications](https://github.com/user-attachments/assets/881ffac0-e447-4455-8b6e-39e2aac1b94e) ![DeepSea Notifications](https://github.com/user-attachments/assets/881ffac0-e447-4455-8b6e-39e2aac1b94e)
@ -28,6 +28,13 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **Payout Monitoring**: View unpaid balance and estimated time to next payout - **Payout Monitoring**: View unpaid balance and estimated time to next payout
- **Pool Fee Analysis**: Monitor pool fee percentages with visual indicator when optimal rates (0.9-1.3%) are detected - **Pool Fee Analysis**: Monitor pool fee percentages with visual indicator when optimal rates (0.9-1.3%) are detected
### Multi-Currency Support
- **Flexible Currency Configuration: Set your preferred fiat currency for displaying Bitcoin value and earnings
- **Wide Currency Selection: Choose from USD, EUR, GBP, JPY, CAD, AUD, CNY, KRW, BRL, CHF and more
- **Real-Time Exchange Rates: Automatically fetches up-to-date exchange rates from public APIs
- **Persistent Configuration: Currency preferences saved and restored between sessions
- **Adaptive Notifications: Financial notifications display in your selected currency
### Worker Management ### Worker Management
- **Fleet Overview**: Comprehensive view of all mining devices in one interface - **Fleet Overview**: Comprehensive view of all mining devices in one interface
- **Status Monitoring**: Real-time status indicators for online and offline devices - **Status Monitoring**: Real-time status indicators for online and offline devices
@ -114,6 +121,7 @@ You can modify the following environment variables in the `docker-compose.yml` f
- `POWER_USAGE`: Power usage in watts. - `POWER_USAGE`: Power usage in watts.
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees). - `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
- `TIMEZONE`: Local timezone for displaying time information. - `TIMEZONE`: Local timezone for displaying time information.
- `CURRENCY`: Preferred fiat currency for earnings display.
Redis data is stored in a persistent volume (`redis_data`), and application logs are saved in the `./logs` directory. Redis data is stored in a persistent volume (`redis_data`), and application logs are saved in the `./logs` directory.
@ -174,6 +182,9 @@ Built with a modern stack for reliability and performance:
- `/api/available_timezones`: Returns a list of supported timezones. - `/api/available_timezones`: Returns a list of supported timezones.
- `/api/config`: Fetches or updates the mining configuration. - `/api/config`: Fetches or updates the mining configuration.
- `/api/health`: Returns the health status of the application. - `/api/health`: Returns the health status of the application.
- `/api/notifications`: Manages notifications for the user.
- `/api/workers`: Manages worker data and status.
- `/api/exchange_rates`: Fetches real-time exchange rates for supported currencies.
## Project Structure ## Project Structure
@ -244,6 +255,9 @@ For optimal performance:
4. Access the health endpoint at `/api/health` for diagnostics 4. Access the health endpoint at `/api/health` for diagnostics
5. For stale data issues, use the Force Refresh function 5. For stale data issues, use the Force Refresh function
6. Use hotkey Shift+R to clear chart and Redis data (as needed, not required) 6. Use hotkey Shift+R to clear chart and Redis data (as needed, not required)
7. Check the currency settings if financial calculations appear incorrect
8. Verify timezone settings for accurate time displays
9. Alt + W on Dashboard resets wallet configuration and redirects to Boot sequence
## License ## License

View File

@ -7,7 +7,7 @@ import json
import logging import logging
# Default configuration file path # Default configuration file path
CONFIG_FILE = "config.json" CONFIG_FILE = "data/config.json"
def load_config(): def load_config():
""" """

View File

@ -164,6 +164,18 @@ class MiningDashboardService:
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat() metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
metrics["server_start_time"] = datetime.now(ZoneInfo(get_timezone())).isoformat() metrics["server_start_time"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
# Get the configured currency
from config import load_config
config = load_config()
selected_currency = config.get("currency", "USD")
# Fetch exchange rates
exchange_rates = self.fetch_exchange_rates()
# Add to metrics
metrics["currency"] = selected_currency
metrics["exchange_rates"] = exchange_rates
# Log execution time # Log execution time
execution_time = time.time() - start_time execution_time = time.time() - start_time
metrics["execution_time"] = execution_time metrics["execution_time"] = execution_time
@ -452,6 +464,32 @@ class MiningDashboardService:
logging.error(f"Error fetching {url}: {e}") logging.error(f"Error fetching {url}: {e}")
return None return None
# Add the fetch_exchange_rates method after the fetch_url method
def fetch_exchange_rates(self, base_currency="USD"):
"""
Fetch currency exchange rates from a public API.
Args:
base_currency (str): Base currency for rates (default: USD)
Returns:
dict: Exchange rates for supported currencies
"""
try:
# Use exchangerate-api for currency rates
url = f"https://api.exchangerate-api.com/v4/latest/{base_currency}"
response = self.session.get(url, timeout=5)
if response.ok:
data = response.json()
return data.get('rates', {})
else:
logging.error(f"Failed to fetch exchange rates: {response.status_code}")
return {}
except Exception as e:
logging.error(f"Error fetching exchange rates: {e}")
return {}
def get_bitcoin_stats(self): def get_bitcoin_stats(self):
""" """
Fetch Bitcoin network statistics with improved error handling and caching. Fetch Bitcoin network statistics with improved error handling and caching.

View File

@ -16,11 +16,6 @@ services:
- "5000:5000" - "5000:5000"
environment: environment:
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS
- POWER_COST=0
- POWER_USAGE=0
- NETWORK_FEE=0
- TIMEZONE=America/Los_Angeles
- LOG_LEVEL=INFO - LOG_LEVEL=INFO
volumes: volumes:
- ./logs:/app/logs - ./logs:/app/logs

View File

@ -1,14 +1,17 @@
# notification_service.py # notification_service.py
import logging import logging
import json import json
import time import time
import uuid import uuid
import pytz import pytz
import re
from datetime import datetime, timedelta from datetime import datetime, timedelta
from enum import Enum from enum import Enum
from collections import deque from collections import deque
from typing import List, Dict, Any, Optional, Union from typing import List, Dict, Any, Optional, Union
from config import get_timezone from config import get_timezone, load_config
from data_service import MiningDashboardService
# Constants to replace magic values # Constants to replace magic values
ONE_DAY_SECONDS = 86400 ONE_DAY_SECONDS = 86400
@ -29,6 +32,52 @@ class NotificationCategory(Enum):
EARNINGS = "earnings" EARNINGS = "earnings"
SYSTEM = "system" SYSTEM = "system"
# Currency utility functions
def get_currency_symbol(currency):
"""Return symbol for the specified currency"""
symbols = {
'USD': '$',
'EUR': '',
'GBP': '£',
'JPY': '¥',
'CAD': 'CA$',
'AUD': 'A$',
'CNY': '¥',
'KRW': '',
'BRL': 'R$',
'CHF': 'Fr'
}
return symbols.get(currency, '$')
def format_currency_value(value, currency, exchange_rates):
"""Format a USD value in the selected currency"""
if value is None or value == "N/A":
return "N/A"
# Get exchange rate (default to 1.0 if not found)
exchange_rate = exchange_rates.get(currency, 1.0)
converted_value = value * exchange_rate
# Get currency symbol
symbol = get_currency_symbol(currency)
# Format with or without decimals based on currency
if currency in ['JPY', 'KRW']:
return f"{symbol}{int(converted_value):,}"
else:
return f"{symbol}{converted_value:.2f}"
def get_exchange_rates():
"""Get exchange rates with caching"""
try:
# Create a dashboard service instance for fetching exchange rates
dashboard_service = MiningDashboardService(0, 0, "", 0)
exchange_rates = dashboard_service.fetch_exchange_rates()
return exchange_rates
except Exception as e:
logging.error(f"Error fetching exchange rates for notifications: {e}")
return {} # Return empty dict if failed
class NotificationService: class NotificationService:
"""Service for managing mining dashboard notifications.""" """Service for managing mining dashboard notifications."""
@ -263,35 +312,37 @@ class NotificationService:
return deleted > 0 return deleted > 0
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None) -> int: def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None, read_only: bool = False) -> int:
""" """
Clear notifications with optimized filtering. Clear notifications with optimized filtering.
Args: Args:
category (str, optional): Only clear specific category category (str, optional): Only clear specific category
older_than_days (int, optional): Only clear notifications older than this older_than_days (int, optional): Only clear notifications older than this
read_only (bool, optional): Only clear notifications that have been read
Returns: Returns:
int: Number of notifications cleared int: Number of notifications cleared
""" """
original_count = len(self.notifications) original_count = len(self.notifications)
cutoff_date = None cutoff_date = None
if older_than_days: if older_than_days:
cutoff_date = self._get_current_time() - timedelta(days=older_than_days) cutoff_date = self._get_current_time() - timedelta(days=older_than_days)
# Apply filters in a single pass # Apply filters to KEEP notifications that should NOT be cleared
self.notifications = [ self.notifications = [
n for n in self.notifications n for n in self.notifications
if (not category or n.get("category") != category) and if (category and n.get("category") != category) or # Keep if we're filtering by category and this isn't that category
(not cutoff_date or self._parse_timestamp(n.get("timestamp", self._get_current_time().isoformat())) >= cutoff_date) (cutoff_date and self._parse_timestamp(n.get("timestamp", self._get_current_time().isoformat())) >= cutoff_date) or # Keep if newer than cutoff
(read_only and not n.get("read", False)) # Keep if we're only clearing read notifications and this is unread
] ]
cleared_count = original_count - len(self.notifications) cleared_count = original_count - len(self.notifications)
if cleared_count > 0: if cleared_count > 0:
logging.info(f"[NotificationService] Cleared {cleared_count} notifications") logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
self._save_notifications() self._save_notifications()
return cleared_count return cleared_count
def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]: def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
@ -312,6 +363,11 @@ class NotificationService:
if not current_metrics: if not current_metrics:
logging.warning("[NotificationService] No current metrics available, skipping notification checks") logging.warning("[NotificationService] No current metrics available, skipping notification checks")
return new_notifications return new_notifications
# Skip notification generation after configuration reset
if current_metrics.get("wallet") == "yourwallethere" or current_metrics.get("config_reset", False):
logging.info("[NotificationService] Configuration reset detected, skipping all notifications")
return new_notifications
# Check for block updates (using persistent storage) # Check for block updates (using persistent storage)
last_block_height = current_metrics.get("last_block_height") last_block_height = current_metrics.get("last_block_height")
@ -384,12 +440,22 @@ class NotificationService:
hashrate_24hr = metrics.get("hashrate_24hr", 0) hashrate_24hr = metrics.get("hashrate_24hr", 0)
hashrate_unit = metrics.get("hashrate_24hr_unit", "TH/s") hashrate_unit = metrics.get("hashrate_24hr_unit", "TH/s")
# Format daily earnings # Format daily earnings with user's currency
daily_mined_sats = metrics.get("daily_mined_sats", 0) daily_mined_sats = metrics.get("daily_mined_sats", 0)
daily_profit_usd = metrics.get("daily_profit_usd", 0) daily_profit_usd = metrics.get("daily_profit_usd", 0)
# Get user's currency preference
config = load_config()
user_currency = config.get("currency", "USD")
# Get exchange rates
exchange_rates = get_exchange_rates()
# Format with the user's currency
formatted_profit = format_currency_value(daily_profit_usd, user_currency, exchange_rates)
# Build message # Build message
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined (${daily_profit_usd:.2f})" message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined ({formatted_profit})"
# Add notification # Add notification
logging.info(f"[NotificationService] Generating daily stats notification: {message}") logging.info(f"[NotificationService] Generating daily stats notification: {message}")
@ -401,7 +467,8 @@ class NotificationService:
"hashrate": hashrate_24hr, "hashrate": hashrate_24hr,
"unit": hashrate_unit, "unit": hashrate_unit,
"daily_sats": daily_mined_sats, "daily_sats": daily_mined_sats,
"daily_profit": daily_profit_usd "daily_profit": daily_profit_usd,
"currency": user_currency
} }
) )
except Exception as e: except Exception as e:
@ -447,38 +514,71 @@ class NotificationService:
return 0.0 return 0.0
def _check_hashrate_change(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]: def _check_hashrate_change(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Check for significant hashrate changes using 10-minute average.""" """Check for significant hashrate changes using appropriate time window based on mode."""
try: try:
# Get 10min hashrate values # Check if we're in low hashrate mode
current_10min = current.get("hashrate_10min", 0) # A simple threshold approach: if hashrate_3hr is below 1 TH/s, consider it low hashrate mode
previous_10min = previous.get("hashrate_10min", 0) is_low_hashrate_mode = False
# Log what we're comparing if "hashrate_3hr" in current:
logging.debug(f"[NotificationService] Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}") current_3hr = self._parse_numeric_value(current.get("hashrate_3hr", 0))
current_3hr_unit = current.get("hashrate_3hr_unit", "TH/s").lower()
# Skip if values are missing
if not current_10min or not previous_10min:
logging.debug("[NotificationService] Skipping hashrate check - missing values")
return None
# Parse values consistently
current_value = self._parse_numeric_value(current_10min)
previous_value = self._parse_numeric_value(previous_10min)
logging.debug(f"[NotificationService] Converted 10min hashrates - current: {current_value}, previous: {previous_value}") # Normalize to TH/s for comparison
if "ph/s" in current_3hr_unit:
current_3hr *= 1000
elif "gh/s" in current_3hr_unit:
current_3hr /= 1000
elif "mh/s" in current_3hr_unit:
current_3hr /= 1000000
# If hashrate is less than 3 TH/s, consider it low hashrate mode
is_low_hashrate_mode = current_3hr < 3.0
logging.debug(f"[NotificationService] Low hashrate mode: {is_low_hashrate_mode}")
# Choose the appropriate hashrate metric based on mode
if is_low_hashrate_mode:
# In low hashrate mode, use 3hr averages for more stability
current_hashrate_key = "hashrate_3hr"
previous_hashrate_key = "hashrate_3hr"
timeframe = "3hr"
else:
# In normal mode, use 10min averages for faster response
current_hashrate_key = "hashrate_10min"
previous_hashrate_key = "hashrate_10min"
timeframe = "10min"
# Get hashrate values
current_hashrate = current.get(current_hashrate_key, 0)
previous_hashrate = previous.get(previous_hashrate_key, 0)
# Log what we're comparing
logging.debug(f"[NotificationService] Comparing {timeframe} hashrates - current: {current_hashrate}, previous: {previous_hashrate}")
# Skip if values are missing
if not current_hashrate or not previous_hashrate:
logging.debug(f"[NotificationService] Skipping hashrate check - missing {timeframe} values")
return None
# Parse values consistently
current_value = self._parse_numeric_value(current_hashrate)
previous_value = self._parse_numeric_value(previous_hashrate)
logging.debug(f"[NotificationService] Converted {timeframe} hashrates - current: {current_value}, previous: {previous_value}")
# Skip if previous was zero (prevents division by zero) # Skip if previous was zero (prevents division by zero)
if previous_value == 0: if previous_value == 0:
logging.debug("[NotificationService] Skipping hashrate check - previous was zero") logging.debug(f"[NotificationService] Skipping hashrate check - previous {timeframe} was zero")
return None return None
# Calculate percentage change # Calculate percentage change
percent_change = ((current_value - previous_value) / previous_value) * 100 percent_change = ((current_value - previous_value) / previous_value) * 100
logging.debug(f"[NotificationService] 10min hashrate change: {percent_change:.1f}%") logging.debug(f"[NotificationService] {timeframe} hashrate change: {percent_change:.1f}%")
# Significant decrease # Significant decrease
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT: if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease" message = f"Significant {timeframe} hashrate drop detected: {abs(percent_change):.1f}% decrease"
logging.info(f"[NotificationService] Generating hashrate notification: {message}") logging.info(f"[NotificationService] Generating hashrate notification: {message}")
return self.add_notification( return self.add_notification(
message, message,
@ -488,13 +588,14 @@ class NotificationService:
"previous": previous_value, "previous": previous_value,
"current": current_value, "current": current_value,
"change": percent_change, "change": percent_change,
"timeframe": "10min" "timeframe": timeframe,
"is_low_hashrate_mode": is_low_hashrate_mode
} }
) )
# Significant increase # Significant increase
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT: elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
message = f"10min hashrate increase detected: {percent_change:.1f}% increase" message = f"{timeframe} hashrate increase detected: {percent_change:.1f}% increase"
logging.info(f"[NotificationService] Generating hashrate notification: {message}") logging.info(f"[NotificationService] Generating hashrate notification: {message}")
return self.add_notification( return self.add_notification(
message, message,
@ -504,10 +605,11 @@ class NotificationService:
"previous": previous_value, "previous": previous_value,
"current": current_value, "current": current_value,
"change": percent_change, "change": percent_change,
"timeframe": "10min" "timeframe": timeframe,
"is_low_hashrate_mode": is_low_hashrate_mode
} }
) )
return None return None
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error checking hashrate change: {e}") logging.error(f"[NotificationService] Error checking hashrate change: {e}")
@ -516,8 +618,20 @@ class NotificationService:
def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]: def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Check for significant earnings progress or payout approach.""" """Check for significant earnings progress or payout approach."""
try: try:
# First check for configuration reset via Alt+W (this is a more robust check)
# This specifically looks for the default "yourwallethere" wallet which indicates Alt+W was used
current_wallet = str(current.get("wallet", ""))
if current_wallet == "yourwallethere":
logging.info("[NotificationService] Detected wallet reset to default (likely Alt+W) - skipping payout notification")
return None
# Check if ANY config value changed that might affect balance
if self._is_configuration_change_affecting_balance(current, previous):
logging.info("[NotificationService] Configuration change detected - skipping payout notification")
return None
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0")) current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0"))
# Check if approaching payout # Check if approaching payout
if current.get("est_time_to_payout"): if current.get("est_time_to_payout"):
est_time = current.get("est_time_to_payout") est_time = current.get("est_time_to_payout")
@ -570,9 +684,27 @@ class NotificationService:
logging.error(f"[NotificationService] Error checking earnings progress: {e}") logging.error(f"[NotificationService] Error checking earnings progress: {e}")
return None return None
def _is_configuration_change_affecting_balance(self, current: Dict[str, Any], previous: Dict[str, Any]) -> bool:
"""Check if any configuration changed that would affect balance calculations."""
# Check wallet
if "wallet" in current and "wallet" in previous:
if current.get("wallet") != previous.get("wallet"):
return True
# Check currency
if "currency" in current and "currency" in previous:
if current.get("currency") != previous.get("currency"):
return True
# Check for emergency reset flag
if current.get("config_reset", False):
return True
return False
def _should_send_payout_notification(self) -> bool: def _should_send_payout_notification(self) -> bool:
"""Check if enough time has passed since the last payout notification.""" """Check if enough time has passed since the last payout notification."""
if self.last_payout_notification_time is None: if self.last_payout_notification_time is None:
return True return True
time_since_last_notification = self._get_current_time() - self.last_payout_notification_time time_since_last_notification = self._get_current_time() - self.last_payout_notification_time
return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS

View File

@ -125,87 +125,63 @@ class StateManager:
if not self.redis_client: if not self.redis_client:
logging.info("Redis not available, skipping state save.") logging.info("Redis not available, skipping state save.")
return return
# Check if we've saved recently to avoid too frequent saves
# Only save at most once every 5 minutes
current_time = time.time() current_time = time.time()
if hasattr(self, 'last_save_time') and current_time - self.last_save_time < 300: # 300 seconds = 5 minutes if hasattr(self, 'last_save_time') and current_time - self.last_save_time < 300: # 5 minutes
logging.debug("Skipping Redis save - last save was less than 5 minutes ago") logging.debug("Skipping Redis save - last save was less than 5 minutes ago")
return return
# Update the last save time
self.last_save_time = current_time self.last_save_time = current_time
# Prune data first to reduce volume
self.prune_old_data() self.prune_old_data()
# Create compact versions of the data structures for Redis storage
try: try:
# 1. Create compact arrow_history with minimal data # Compact arrow_history with unit preservation
compact_arrow_history = {} compact_arrow_history = {}
for key, values in arrow_history.items(): for key, values in arrow_history.items():
if isinstance(values, list) and values: if isinstance(values, list) and values:
# Only store recent history (last 2 hours)
recent_values = values[-180:] if len(values) > 180 else values recent_values = values[-180:] if len(values) > 180 else values
# Use shorter field names and preserve arrow directions
compact_arrow_history[key] = [ compact_arrow_history[key] = [
{"t": entry["time"], "v": entry["value"], "a": entry["arrow"]} {"t": entry["time"], "v": entry["value"], "a": entry["arrow"], "u": entry.get("unit", "th/s")}
for entry in recent_values for entry in recent_values
] ]
# 2. Only keep essential hashrate_history # Compact hashrate_history
compact_hashrate_history = hashrate_history[-60:] if len(hashrate_history) > 60 else hashrate_history compact_hashrate_history = hashrate_history[-60:] if len(hashrate_history) > 60 else hashrate_history
# 3. Only keep recent metrics_log entries (last 30 minutes) # Compact metrics_log with unit preservation
# This is typically the largest data structure
compact_metrics_log = [] compact_metrics_log = []
if metrics_log: if metrics_log:
# Keep only last 30 entries (30 minutes assuming 1-minute updates) recent_logs = metrics_log[-30:]
recent_logs = metrics_log[-30:]
for entry in recent_logs: for entry in recent_logs:
# Only keep necessary fields from each metrics entry metrics_copy = {}
if "metrics" in entry and "timestamp" in entry: original_metrics = entry["metrics"]
metrics_copy = {} essential_keys = [
original_metrics = entry["metrics"] "hashrate_60sec", "hashrate_24hr", "btc_price",
"workers_hashing", "unpaid_earnings", "difficulty",
# Only copy the most important metrics for historical tracking "network_hashrate", "daily_profit_usd"
essential_keys = [ ]
"hashrate_60sec", "hashrate_24hr", "btc_price", for key in essential_keys:
"workers_hashing", "unpaid_earnings", "difficulty", if key in original_metrics:
"network_hashrate", "daily_profit_usd" metrics_copy[key] = {
] "value": original_metrics[key],
"unit": original_metrics.get(f"{key}_unit", "th/s")
for key in essential_keys: }
if key in original_metrics: compact_metrics_log.append({
metrics_copy[key] = original_metrics[key] "ts": entry["timestamp"],
"m": metrics_copy
# Skip arrow_history within metrics as we already stored it separately })
compact_metrics_log.append({
"ts": entry["timestamp"],
"m": metrics_copy
})
# Create the final state object
state = { state = {
"arrow_history": compact_arrow_history, "arrow_history": compact_arrow_history,
"hashrate_history": compact_hashrate_history, "hashrate_history": compact_hashrate_history,
"metrics_log": compact_metrics_log "metrics_log": compact_metrics_log
} }
# Convert to JSON once to reuse and measure size
state_json = json.dumps(state) state_json = json.dumps(state)
data_size_kb = len(state_json) / 1024 data_size_kb = len(state_json) / 1024
# Log data size for monitoring
logging.info(f"Saving graph state to Redis: {data_size_kb:.2f} KB (optimized format)") logging.info(f"Saving graph state to Redis: {data_size_kb:.2f} KB (optimized format)")
# Only save if data size is reasonable (adjust threshold as needed) self.redis_client.set(f"{self.STATE_KEY}_version", "2.0")
if data_size_kb > 2000: # 2MB warning threshold (reduced from 5MB)
logging.warning(f"Redis save data size is still large: {data_size_kb:.2f} KB")
# Store version info to handle future format changes
self.redis_client.set(f"{self.STATE_KEY}_version", "2.0")
self.redis_client.set(self.STATE_KEY, state_json) self.redis_client.set(self.STATE_KEY, state_json)
logging.info(f"Successfully saved graph state to Redis ({data_size_kb:.2f} KB)") logging.info(f"Successfully saved graph state to Redis ({data_size_kb:.2f} KB)")
except Exception as e: except Exception as e:

View File

@ -1,14 +1,10 @@
.footer { .footer {
margin-top: 30px;
padding: 10px 0; padding: 10px 0;
color: grey; color: grey;
font-size: 0.9rem; font-size: 0.9rem;
border-top: 1px solid rgba(128, 128, 128, 0.2); border-top: 1px solid rgba(128, 128, 128, 0.2);
} }
</style >
<!-- Preload theme to prevent flicker -->
<style id="theme-preload" >
/* Theme-aware loading state */ /* Theme-aware loading state */
html.bitcoin-theme { html.bitcoin-theme {
background-color: #111111; background-color: #111111;

View File

@ -184,7 +184,7 @@
} }
/* Add bottom padding to accommodate minimized system monitor */ /* Add bottom padding to accommodate minimized system monitor */
.container-fluid { .container-fluid {
padding-bottom: 60px !important; /* Enough space for minimized monitor */ padding-bottom: 100px !important; /* Enough space for minimized monitor */
} }
/* Add these styles to dashboard.css */ /* Add these styles to dashboard.css */
@ -218,7 +218,7 @@
} }
.datum-label { .datum-label {
color: #ffffff; /* White color */ color: cyan; /* cyan color */
font-size: 0.95em; font-size: 0.95em;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
@ -245,3 +245,10 @@
.unlucky { .unlucky {
color: #ff5555 !important; color: #ff5555 !important;
} }
/* Ensure the pool fee in SATS is always red regardless of theme */
#pool_fees_sats {
color: #ff5555 !important;
font-weight: bold !important;
margin-left: 6px;
}

View File

@ -283,12 +283,13 @@
.notification-actions { .notification-actions {
flex-direction: column; flex-direction: column;
gap: 8px; gap: 8px;
margin-top: 10px;
} }
.action-button { .action-button {
width: 100%; /* Full width on small screens */ width: 100%; /* Full width on small screens */
padding: 8px 12px; padding: 8px 12px;
font-size: 1rem; font-size: 0.95rem;
} }
.notification-controls { .notification-controls {
@ -297,16 +298,27 @@
} }
.filter-buttons { .filter-buttons {
overflow-x: auto; display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 5px; margin-bottom: 10px;
white-space: nowrap; width: 100%;
}
.filter-button {
text-align: center;
white-space: normal;
font-size: 0.9rem;
padding: 8px 5px;
display: flex; display: flex;
flex-wrap: nowrap; align-items: center;
justify-content: center;
min-height: 38px;
} }
.notification-actions { .notification-actions {
justify-content: flex-end; justify-content: stretch;
} }
.notification-item { .notification-item {
@ -324,4 +336,11 @@
.notification-actions { .notification-actions {
flex: 0 0 60px; flex: 0 0 60px;
} }
/* For very small screens, reduce to 2 columns */
@media (max-width: 375px) {
.filter-buttons {
grid-template-columns: repeat(2, 1fr);
}
}
} }

View File

@ -91,6 +91,22 @@
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3); box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
} }
/* Add RGB values for primary colors */
html.deepsea-theme {
--primary-color: #0088cc;
--primary-color-rgb: 0, 136, 204;
}
html.bitcoin-theme {
--primary-color: #f2a900;
--primary-color-rgb: 242, 169, 0;
}
/* Theme-specific graph container shadow using CSS variable */
#graphContainer {
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.2) !important;
}
/* Bitcoin theme specific styling (orange) */ /* Bitcoin theme specific styling (orange) */
body:not(.deepsea-theme) #themeToggle, body:not(.deepsea-theme) #themeToggle,
body:not(.deepsea-theme) .theme-toggle-btn { body:not(.deepsea-theme) .theme-toggle-btn {
@ -142,15 +158,6 @@ body.deepsea-theme .theme-toggle-btn:focus {
box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3); box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3);
} }
/* Add to your common.css or theme-toggle.css */
html.deepsea-theme {
--primary-color: #0088cc;
}
html.bitcoin-theme {
--primary-color: #f2a900;
}
/* Add these theme-specific loading styles */ /* Add these theme-specific loading styles */
#theme-loader { #theme-loader {
position: fixed; position: fixed;

View File

@ -8,8 +8,10 @@
const BitcoinMinuteRefresh = (function () { const BitcoinMinuteRefresh = (function () {
// Constants // Constants
const STORAGE_KEY = 'bitcoin_last_refresh_time'; const STORAGE_KEY = 'bitcoin_last_refresh_time';
const BITCOIN_COLOR = '#f7931a'; // Default fallback colors if CSS vars aren't available
const DEEPSEA_COLOR = '#0088cc'; const FALLBACK_BITCOIN_COLOR = '#f2a900';
const FALLBACK_DEEPSEA_COLOR = '#0088cc';
const DOM_IDS = { const DOM_IDS = {
TERMINAL: 'bitcoin-terminal', TERMINAL: 'bitcoin-terminal',
STYLES: 'bitcoin-terminal-styles', STYLES: 'bitcoin-terminal-styles',
@ -45,9 +47,32 @@ const BitcoinMinuteRefresh = (function () {
let uptimeInterval = null; let uptimeInterval = null;
let isInitialized = false; let isInitialized = false;
let refreshCallback = null; let refreshCallback = null;
let currentThemeColor = BITCOIN_COLOR; // Default Bitcoin color let currentThemeColor = '';
let currentThemeRGB = '';
let dragListenersAdded = false; let dragListenersAdded = false;
/**
* Get theme colors from CSS variables
*/
function getThemeColors() {
// Try to get CSS variables from document root
const rootStyles = getComputedStyle(document.documentElement);
let primaryColor = rootStyles.getPropertyValue('--primary-color').trim();
let primaryColorRGB = rootStyles.getPropertyValue('--primary-color-rgb').trim();
// If CSS vars not available, use theme toggle state
if (!primaryColor) {
const isDeepSea = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
primaryColor = isDeepSea ? FALLBACK_DEEPSEA_COLOR : FALLBACK_BITCOIN_COLOR;
primaryColorRGB = isDeepSea ? '0, 136, 204' : '242, 169, 0';
}
return {
color: primaryColor,
rgb: primaryColorRGB
};
}
/** /**
* Logging helper function * Logging helper function
* @param {string} message - Message to log * @param {string} message - Message to log
@ -79,24 +104,22 @@ const BitcoinMinuteRefresh = (function () {
* Apply the current theme color * Apply the current theme color
*/ */
function applyThemeColor() { function applyThemeColor() {
// Check if theme toggle is set to DeepSea // Get current theme colors
const isDeepSeaTheme = localStorage.getItem(STORAGE_KEYS.THEME) === 'true'; const theme = getThemeColors();
currentThemeColor = isDeepSeaTheme ? DEEPSEA_COLOR : BITCOIN_COLOR; currentThemeColor = theme.color;
currentThemeRGB = theme.rgb;
// Don't try to update DOM elements if they don't exist yet // Don't try to update DOM elements if they don't exist yet
if (!terminalElement) return; if (!terminalElement) return;
// Define color values based on theme
const rgbValues = isDeepSeaTheme ? '0, 136, 204' : '247, 147, 26';
// Create theme config // Create theme config
const themeConfig = { const themeConfig = {
color: currentThemeColor, color: currentThemeColor,
borderColor: currentThemeColor, borderColor: currentThemeColor,
boxShadow: `0 0 5px rgba(${rgbValues}, 0.3)`, boxShadow: `0 0 5px rgba(${currentThemeRGB}, 0.3)`,
textShadow: `0 0 5px rgba(${rgbValues}, 0.8)`, textShadow: `0 0 5px rgba(${currentThemeRGB}, 0.8)`,
borderColorRGBA: `rgba(${rgbValues}, 0.5)`, borderColorRGBA: `rgba(${currentThemeRGB}, 0.5)`,
textShadowStrong: `0 0 8px rgba(${rgbValues}, 0.8)` textShadowStrong: `0 0 8px rgba(${currentThemeRGB}, 0.8)`
}; };
// Apply styles to terminal // Apply styles to terminal
@ -144,6 +167,13 @@ const BitcoinMinuteRefresh = (function () {
if (miniLabel) { if (miniLabel) {
miniLabel.style.color = themeConfig.color; miniLabel.style.color = themeConfig.color;
} }
// Update show button if it exists
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON);
if (showButton) {
showButton.style.backgroundColor = themeConfig.color;
showButton.style.boxShadow = `0 0 10px rgba(${currentThemeRGB}, 0.5)`;
}
} }
/** /**
@ -156,6 +186,25 @@ const BitcoinMinuteRefresh = (function () {
applyThemeColor(); applyThemeColor();
} }
}); });
// Listen for custom theme change events
document.addEventListener('themeChanged', function () {
applyThemeColor();
});
// Watch for class changes on HTML element that might indicate theme changes
const observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
if (mutation.attributeName === 'class') {
applyThemeColor();
}
});
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class']
});
} }
/** /**
@ -348,8 +397,12 @@ const BitcoinMinuteRefresh = (function () {
<div class="terminal-header"> <div class="terminal-header">
<div class="terminal-title">SYSTEM MONITOR v.3</div> <div class="terminal-title">SYSTEM MONITOR v.3</div>
<div class="terminal-controls"> <div class="terminal-controls">
<div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()"></div> <div class="terminal-dot minimize" title="Minimize" onclick="BitcoinMinuteRefresh.toggleTerminal()">
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()"></div> <span class="control-symbol">-</span>
</div>
<div class="terminal-dot close" title="Close" onclick="BitcoinMinuteRefresh.hideTerminal()">
<span class="control-symbol">x</span>
</div>
</div> </div>
</div> </div>
<div class="terminal-content"> <div class="terminal-content">
@ -413,13 +466,12 @@ const BitcoinMinuteRefresh = (function () {
* Add CSS styles for the terminal * Add CSS styles for the terminal
*/ */
function addStyles() { function addStyles() {
// Use the currentThemeColor variable instead of hardcoded colors // Get current theme colors for initial styling
const theme = getThemeColors();
const styleElement = document.createElement('style'); const styleElement = document.createElement('style');
styleElement.id = DOM_IDS.STYLES; styleElement.id = DOM_IDS.STYLES;
// Generate RGB values for dynamic colors
const rgbValues = currentThemeColor === DEEPSEA_COLOR ? '0, 136, 204' : '247, 147, 26';
styleElement.textContent = ` styleElement.textContent = `
/* Terminal Container */ /* Terminal Container */
.bitcoin-terminal { .bitcoin-terminal {
@ -428,14 +480,14 @@ const BitcoinMinuteRefresh = (function () {
right: 20px; right: 20px;
width: 230px; width: 230px;
background-color: #000000; background-color: #000000;
border: 1px solid ${currentThemeColor}; border: 1px solid var(--primary-color, ${theme.color});
color: ${currentThemeColor}; color: var(--primary-color, ${theme.color});
font-family: 'VT323', monospace; font-family: 'VT323', monospace;
z-index: 9999; z-index: 9999;
overflow: hidden; overflow: hidden;
padding: 8px; padding: 8px;
transition: all 0.3s ease; transition: all 0.3s ease;
box-shadow: 0 0 5px rgba(${rgbValues}, 0.3); box-shadow: 0 0 5px rgba(var(--primary-color-rgb, ${theme.rgb}), 0.3);
} }
/* Terminal Header */ /* Terminal Header */
@ -443,20 +495,19 @@ const BitcoinMinuteRefresh = (function () {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
border-bottom: 1px solid ${currentThemeColor}; border-bottom: 1px solid var(--primary-color, ${theme.color});
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 8px; margin-bottom: 8px;
cursor: grab; /* Add grab cursor on hover */ cursor: grab;
} }
/* Apply grabbing cursor during active drag */
.terminal-header:active, .terminal-header:active,
.bitcoin-terminal.dragging .terminal-header { .bitcoin-terminal.dragging .terminal-header {
cursor: grabbing; cursor: grabbing;
} }
.terminal-title { .terminal-title {
color: ${currentThemeColor}; color: var(--primary-color, ${theme.color});
font-weight: bold; font-weight: bold;
font-size: 1.1rem; font-size: 1.1rem;
animation: terminal-flicker 4s infinite; animation: terminal-flicker 4s infinite;
@ -470,22 +521,45 @@ const BitcoinMinuteRefresh = (function () {
} }
.terminal-dot { .terminal-dot {
width: 8px; width: 12px;
height: 8px; height: 12px;
border-radius: 50%; border-radius: 50%;
background-color: #555; background-color: #555;
cursor: pointer; cursor: pointer;
transition: background-color 0.3s; transition: background-color 0.3s;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.control-symbol {
color: #333;
font-size: 9px;
font-weight: bold;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
line-height: 1;
} }
.terminal-dot.minimize:hover { .terminal-dot.minimize:hover {
background-color: #ffcc00; background-color: #ffcc00;
} }
.terminal-dot.minimize:hover .control-symbol {
color: #664e00;
}
.terminal-dot.close:hover { .terminal-dot.close:hover {
background-color: #ff3b30; background-color: #ff3b30;
} }
.terminal-dot.close:hover .control-symbol {
color: #7a0200;
}
/* Terminal Content */ /* Terminal Content */
.terminal-content { .terminal-content {
position: relative; position: relative;
@ -524,14 +598,14 @@ const BitcoinMinuteRefresh = (function () {
font-weight: bold; font-weight: bold;
} }
/* Uptime Display - Modern Digital Clock Style (Horizontal) */ /* Uptime Display */
.uptime-timer { .uptime-timer {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
padding: 5px; padding: 5px;
background-color: #111; background-color: #111;
border: 1px solid rgba(${rgbValues}, 0.5); border: 1px solid rgba(var(--primary-color-rgb, ${theme.rgb}), 0.5);
margin-top: 5px; margin-top: 5px;
} }
@ -586,7 +660,7 @@ const BitcoinMinuteRefresh = (function () {
position: fixed; position: fixed;
bottom: 10px; bottom: 10px;
right: 10px; right: 10px;
background-color: ${currentThemeColor}; background-color: var(--primary-color, ${theme.color});
color: #000; color: #000;
border: none; border: none;
padding: 8px 12px; padding: 8px 12px;
@ -594,7 +668,7 @@ const BitcoinMinuteRefresh = (function () {
cursor: pointer; cursor: pointer;
z-index: 9999; z-index: 9999;
display: none; display: none;
box-shadow: 0 0 10px rgba(${rgbValues}, 0.5); box-shadow: 0 0 10px rgba(var(--primary-color-rgb, ${theme.rgb}), 0.5);
} }
/* CRT scanline effect */ /* CRT scanline effect */
@ -660,7 +734,7 @@ const BitcoinMinuteRefresh = (function () {
letter-spacing: 1px; letter-spacing: 1px;
opacity: 0.7; opacity: 0.7;
margin-left: 45px; margin-left: 45px;
color: ${currentThemeColor}; color: var(--primary-color, ${theme.color});
} }
#${DOM_IDS.MINIMIZED_UPTIME} { #${DOM_IDS.MINIMIZED_UPTIME} {
@ -855,9 +929,6 @@ const BitcoinMinuteRefresh = (function () {
// Store the refresh callback // Store the refresh callback
refreshCallback = refreshFunc; refreshCallback = refreshFunc;
// Get current theme status
applyThemeColor();
// Create the terminal element if it doesn't exist // Create the terminal element if it doesn't exist
if (!document.getElementById(DOM_IDS.TERMINAL)) { if (!document.getElementById(DOM_IDS.TERMINAL)) {
createTerminalElement(); createTerminalElement();
@ -865,11 +936,11 @@ const BitcoinMinuteRefresh = (function () {
// Get references to existing elements // Get references to existing elements
terminalElement = document.getElementById(DOM_IDS.TERMINAL); terminalElement = document.getElementById(DOM_IDS.TERMINAL);
uptimeElement = document.getElementById('uptime-timer'); uptimeElement = document.getElementById('uptime-timer');
// Apply theme to existing element
applyThemeColor();
} }
// Apply theme colors
applyThemeColor();
// Set up listener for theme changes // Set up listener for theme changes
setupThemeChangeListener(); setupThemeChangeListener();
@ -923,6 +994,9 @@ const BitcoinMinuteRefresh = (function () {
} catch (e) { } catch (e) {
log("Error reading updated server time: " + e.message, 'error'); log("Error reading updated server time: " + e.message, 'error');
} }
} else if (event.key === STORAGE_KEYS.THEME) {
// Update theme when theme preference changes
applyThemeColor();
} }
} }
@ -933,6 +1007,9 @@ const BitcoinMinuteRefresh = (function () {
if (!document.hidden) { if (!document.hidden) {
log("Page became visible, updating"); log("Page became visible, updating");
// Apply current theme when page becomes visible
applyThemeColor();
// Update immediately when page becomes visible // Update immediately when page becomes visible
updateClock(); updateClock();
updateUptime(); updateUptime();
@ -989,6 +1066,11 @@ const BitcoinMinuteRefresh = (function () {
showButton.textContent = 'Show Monitor'; showButton.textContent = 'Show Monitor';
showButton.onclick = showTerminal; showButton.onclick = showTerminal;
document.body.appendChild(showButton); document.body.appendChild(showButton);
// Apply current theme to the button
const theme = getThemeColors();
showButton.style.backgroundColor = theme.color;
showButton.style.boxShadow = `0 0 10px rgba(${theme.rgb}, 0.5)`;
} }
document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block'; document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block';

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}BTC-OS MINING DASHBOARD {% endblock %}</title> <title>{% block title %}BTC-OS DASHBOARD {% endblock %}</title>
<!-- Common fonts --> <!-- Common fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
@ -96,7 +96,7 @@
<h1 class="text-center"> <h1 class="text-center">
<a href="/" style="text-decoration:none; color:inherit;"> <a href="/" style="text-decoration:none; color:inherit;">
{% block header %}BTC-OS MINING DASHBOARD{% endblock %} {% block header %}BTC-OS DASHBOARD{% endblock %}
</a> </a>
</h1> </h1>
@ -135,6 +135,7 @@
<!-- Footer --> <!-- Footer -->
<footer class="footer text-center"> <footer class="footer text-center">
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p> <p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
<p>v0.9.2 - Public Beta</p>
</footer> </footer>
</div> </div>

View File

@ -68,9 +68,9 @@
}); });
</script> </script>
<!-- Theme toggle button (new) --> <!-- Theme toggle button (new) -->
<button id="themeToggle" class="theme-toggle-btn"> <!--<button id="themeToggle" class="theme-toggle-btn">
<span>Toggle Theme</span> <span>Toggle Theme</span>
</button> </button>-->
<button id="skip-button">SKIP</button> <button id="skip-button">SKIP</button>
<div id="debug-info"></div> <div id="debug-info"></div>
<div id="loading-message">Loading mining data...</div> <div id="loading-message">Loading mining data...</div>
@ -109,6 +109,27 @@ v.21
</label> </label>
<input type="text" id="wallet-address" placeholder="bc1..." value=""> <input type="text" id="wallet-address" placeholder="bc1..." value="">
</div> </div>
<div class="form-group">
<label for="currency">
Preferred Currency
<span class="tooltip">
?
<span class="tooltip-text">Currency for displaying BTC value and earnings</span>
</span>
</label>
<select id="currency" class="form-control">
<option value="USD">USD ($)</option>
<option value="EUR">EUR (€)</option>
<option value="GBP">GBP (£)</option>
<option value="JPY">JPY (¥)</option>
<option value="CAD">CAD ($)</option>
<option value="AUD">AUD ($)</option>
<option value="CNY">CNY (¥)</option>
<option value="KRW">KRW (₩)</option>
<option value="BRL">BRL (R$)</option>
<option value="CHF">CHF (Fr)</option>
</select>
</div>
<div class="form-group"> <div class="form-group">
<label for="power-cost"> <label for="power-cost">
Power Cost ($/kWh) Power Cost ($/kWh)
@ -131,7 +152,7 @@ v.21
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="network-fee"> <label for="network-fee">
Network Fee (%) Firmware/Other Fees (%)
<span class="tooltip"> <span class="tooltip">
? ?
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span> <span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
@ -281,20 +302,22 @@ v.21
loadTimezoneFromConfig(); loadTimezoneFromConfig();
}); });
// Update saveConfig to include network fee // Update saveConfig to include currency
function saveConfig() { function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim(); const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0; const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0; const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const timezone = document.getElementById('timezone').value; const timezone = document.getElementById('timezone').value;
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0; const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
const currency = document.getElementById('currency').value;
const updatedConfig = { const updatedConfig = {
wallet: wallet || (currentConfig ? currentConfig.wallet : ""), wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
power_cost: powerCost, power_cost: powerCost,
power_usage: powerUsage, power_usage: powerUsage,
timezone: timezone, timezone: timezone,
network_fee: networkFee network_fee: networkFee,
currency: currency
}; };
return fetch('/api/config', { return fetch('/api/config', {
@ -345,7 +368,7 @@ v.21
power_usage: 0.0 power_usage: 0.0
}; };
// Update loadConfig function to include network fee // Update loadConfig function to handle currency
function loadConfig() { function loadConfig() {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch('/api/config?nocache=' + new Date().getTime()) fetch('/api/config?nocache=' + new Date().getTime())
@ -364,6 +387,12 @@ v.21
document.getElementById('power-cost').value = currentConfig.power_cost || ""; document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || ""; document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || ""; document.getElementById('network-fee').value = currentConfig.network_fee || "";
// Set currency dropdown value if available
if (currentConfig.currency) {
document.getElementById('currency').value = currentConfig.currency;
}
configLoaded = true; configLoaded = true;
resolve(currentConfig); resolve(currentConfig);
}) })
@ -374,13 +403,16 @@ v.21
wallet: "yourwallethere", wallet: "yourwallethere",
power_cost: 0.0, power_cost: 0.0,
power_usage: 0.0, power_usage: 0.0,
network_fee: 0.0 network_fee: 0.0,
currency: "USD"
}; };
document.getElementById('wallet-address').value = currentConfig.wallet || ""; document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || ""; document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || ""; document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || ""; document.getElementById('network-fee').value = currentConfig.network_fee || "";
document.getElementById('currency').value = currentConfig.currency || "USD";
resolve(currentConfig); resolve(currentConfig);
}); });
}); });

View File

@ -55,43 +55,6 @@
<div class="card" id="payoutMiscCard"> <div class="card" id="payoutMiscCard">
<div class="card-header">Payout Info</div> <div class="card-header">Payout Info</div>
<div class="card-body"> <div class="card-body">
<p>
<strong>Unpaid Earnings:</strong>
<span id="unpaid_earnings" class="metric-value green">
{% if metrics and metrics.unpaid_earnings %}
{{ metrics.unpaid_earnings }} BTC
{% else %}
0 BTC
{% endif %}
</span>
<span id="indicator_unpaid_earnings"></span>
</p>
<p>
<strong>Last Block:</strong>
<span id="last_block_height" class="metric-value white">
{{ metrics.last_block_height|commafy if metrics and metrics.last_block_height else "N/A" }}
</span>
<span id="last_block_time" class="metric-value blue">
{{ metrics.last_block_time if metrics and metrics.last_block_time else "N/A" }}
</span>
<span class="green">
{% if metrics and metrics.last_block_earnings %}
+{{ metrics.last_block_earnings|int|commafy }} SATS
{% else %}
+0 SATS
{% endif %}
</span>
<span id="indicator_last_block"></span>
</p>
<p>
<strong>Est. Time to Payout:</strong>
<span id="est_time_to_payout" class="metric-value yellow">
{{ metrics.est_time_to_payout if metrics and metrics.est_time_to_payout else "N/A" }}
</span>
<span id="indicator_est_time_to_payout"></span>
</p>
<p> <p>
<strong>Pool Fees:</strong> <strong>Pool Fees:</strong>
<span id="pool_fees_percentage" class="metric-value"> <span id="pool_fees_percentage" class="metric-value">
@ -106,6 +69,43 @@
</span> </span>
<span id="indicator_pool_fees_percentage"></span> <span id="indicator_pool_fees_percentage"></span>
</p> </p>
<p>
<strong>Last Block:</strong>
<span id="last_block_height" class="metric-value white">
{{ metrics.last_block_height|commafy if metrics and metrics.last_block_height else "N/A" }}
</span>
<span id="last_block_time" class="metric-value blue">
{{ metrics.last_block_time if metrics and metrics.last_block_time else "N/A" }}
</span>
<span id="last_block_earnings" class="metric-value green">
{% if metrics and metrics.last_block_earnings %}
+{{ metrics.last_block_earnings|int|commafy }} SATS
{% else %}
+0 SATS
{% endif %}
</span>
<span id="indicator_last_block"></span>
</p>
<p>
<strong>Unpaid Earnings:</strong>
<span id="unpaid_earnings" class="metric-value green">
{% if metrics and metrics.unpaid_earnings %}
{{ metrics.unpaid_earnings }} BTC
{% else %}
0 BTC
{% endif %}
</span>
<span id="indicator_unpaid_earnings"></span>
</p>
<p>
<strong>Est. Time to Payout:</strong>
<span id="est_time_to_payout" class="metric-value yellow">
{{ metrics.est_time_to_payout if metrics and metrics.est_time_to_payout else "N/A" }}
</span>
<span id="indicator_est_time_to_payout"></span>
</p>
</div> </div>
</div> </div>
</div> </div>