Compare commits

..

No commits in common. "main" and "v0.4" have entirely different histories.
main ... v0.4

35 changed files with 1178 additions and 4226 deletions

70
App.py
View File

@ -22,7 +22,6 @@ from config import load_config, save_config
from data_service import MiningDashboardService from data_service import MiningDashboardService
from worker_service import WorkerService from worker_service import WorkerService
from state_manager import StateManager, arrow_history, metrics_log from state_manager import StateManager, arrow_history, metrics_log
from config import get_timezone
# Initialize Flask app # Initialize Flask app
app = Flask(__name__) app = Flask(__name__)
@ -46,7 +45,7 @@ scheduler_recreate_lock = threading.Lock()
scheduler = None scheduler = None
# Global start time # Global start time
SERVER_START_TIME = datetime.now(ZoneInfo(get_timezone())) SERVER_START_TIME = datetime.now(ZoneInfo("America/Los_Angeles"))
# Configure logging # Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@ -418,8 +417,8 @@ def dashboard():
# If still None after our attempt, create default metrics # If still None after our attempt, create default metrics
if cached_metrics is None: if cached_metrics is None:
default_metrics = { default_metrics = {
"server_timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat(), "server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo(get_timezone())).isoformat(), "server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat(),
"hashrate_24hr": None, "hashrate_24hr": None,
"hashrate_24hr_unit": "TH/s", "hashrate_24hr_unit": "TH/s",
"hashrate_3hr": None, "hashrate_3hr": None,
@ -454,12 +453,12 @@ def dashboard():
"arrow_history": {} "arrow_history": {}
} }
logging.warning("Rendering dashboard with default metrics - no data available yet") logging.warning("Rendering dashboard with default metrics - no data available yet")
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%Y-%m-%d %H:%M:%S %p") current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %H:%M:%S %p")
return render_template("dashboard.html", metrics=default_metrics, current_time=current_time) return render_template("dashboard.html", metrics=default_metrics, current_time=current_time)
# If we have metrics, use them # If we have metrics, use them
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%Y-%m-%d %H:%M:%S %p") current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %H:%M:%S %p")
return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time) return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time)# api/time endpoint
@app.route("/api/metrics") @app.route("/api/metrics")
def api_metrics(): def api_metrics():
@ -468,30 +467,18 @@ def api_metrics():
update_metrics_job() update_metrics_job()
return jsonify(cached_metrics) return jsonify(cached_metrics)
@app.route("/api/available_timezones")
def available_timezones():
"""Return a list of available timezones."""
from zoneinfo import available_timezones
return jsonify({"timezones": sorted(available_timezones())})
@app.route('/api/timezone', methods=['GET'])
def get_timezone_config():
from flask import jsonify
from config import get_timezone
return jsonify({"timezone": get_timezone()})
# Add this new route to App.py # Add this new route to App.py
@app.route("/blocks") @app.route("/blocks")
def blocks_page(): def blocks_page():
"""Serve the blocks overview page.""" """Serve the blocks overview page."""
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p") current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%b %d, %Y, %I:%M:%S %p")
return render_template("blocks.html", current_time=current_time) return render_template("blocks.html", current_time=current_time)
# --- Workers Dashboard Route and API --- # --- Workers Dashboard Route and API ---
@app.route("/workers") @app.route("/workers")
def workers_dashboard(): def workers_dashboard():
"""Serve the workers overview dashboard page.""" """Serve the workers overview dashboard page."""
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%Y-%m-%d %I:%M:%S %p") current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %I:%M:%S %p")
# Only get minimal worker stats for initial page load # Only get minimal worker stats for initial page load
# Client-side JS will fetch the full data via API # Client-side JS will fetch the full data via API
@ -520,8 +507,8 @@ def api_workers():
def api_time(): def api_time():
"""API endpoint for server time.""" """API endpoint for server time."""
return jsonify({ # correct time return jsonify({ # correct time
"server_timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat(), "server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo(get_timezone())).isoformat() "server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat()
}) })
# --- New Config Endpoints --- # --- New Config Endpoints ---
@ -599,7 +586,7 @@ def update_config():
def health_check(): def health_check():
"""Health check endpoint with enhanced system diagnostics.""" """Health check endpoint with enhanced system diagnostics."""
# Calculate uptime # Calculate uptime
uptime_seconds = (datetime.now(ZoneInfo(get_timezone())) - SERVER_START_TIME).total_seconds() uptime_seconds = (datetime.now(ZoneInfo("America/Los_Angeles")) - SERVER_START_TIME).total_seconds()
# Get process memory usage # Get process memory usage
try: try:
@ -617,7 +604,7 @@ def health_check():
if cached_metrics and cached_metrics.get("server_timestamp"): if cached_metrics and cached_metrics.get("server_timestamp"):
try: try:
last_update = datetime.fromisoformat(cached_metrics["server_timestamp"]) last_update = datetime.fromisoformat(cached_metrics["server_timestamp"])
data_age = (datetime.now(ZoneInfo(get_timezone())) - last_update).total_seconds() data_age = (datetime.now(ZoneInfo("America/Los_Angeles")) - last_update).total_seconds()
except Exception as e: except Exception as e:
logging.error(f"Error calculating data age: {e}") logging.error(f"Error calculating data age: {e}")
@ -650,7 +637,7 @@ def health_check():
"redis": { "redis": {
"connected": state_manager.redis_client is not None "connected": state_manager.redis_client is not None
}, },
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat() "timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
} }
# Log health check if status is not healthy # Log health check if status is not healthy
@ -794,7 +781,7 @@ def api_clear_notifications():
@app.route("/notifications") @app.route("/notifications")
def notifications_page(): def notifications_page():
"""Serve the notifications page.""" """Serve the notifications page."""
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p") current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%b %d, %Y, %I:%M:%S %p")
return render_template("notifications.html", current_time=current_time) return render_template("notifications.html", current_time=current_time)
@app.errorhandler(404) @app.errorhandler(404)
@ -821,42 +808,17 @@ class RobustMiddleware:
start_response("500 Internal Server Error", [("Content-Type", "text/html")]) start_response("500 Internal Server Error", [("Content-Type", "text/html")])
return [b"<h1>Internal Server Error</h1>"] return [b"<h1>Internal Server Error</h1>"]
@app.route("/api/reset-chart-data", methods=["POST"])
def reset_chart_data():
"""API endpoint to reset chart data history."""
try:
global arrow_history, state_manager
# Clear hashrate data from in-memory dictionary
hashrate_keys = ["hashrate_60sec", "hashrate_3hr", "hashrate_10min", "hashrate_24hr"]
for key in hashrate_keys:
if key in arrow_history:
arrow_history[key] = []
# Force an immediate save to Redis if available
if state_manager and hasattr(state_manager, 'redis_client') and state_manager.redis_client:
# Force save by overriding the time check
state_manager.last_save_time = 0
state_manager.save_graph_state()
logging.info("Chart data reset saved to Redis immediately")
return jsonify({"status": "success", "message": "Chart data reset successfully"})
except Exception as e:
logging.error(f"Error resetting chart data: {e}")
return jsonify({"status": "error", "message": str(e)}), 500
# Add the middleware # Add the middleware
app.wsgi_app = RobustMiddleware(app.wsgi_app) app.wsgi_app = RobustMiddleware(app.wsgi_app)
# Update this section in App.py to properly initialize services # Update this section in App.py to properly initialize services
# Initialize the dashboard service with network fee parameter # Initialize the dashboard service and worker service
config = load_config() config = load_config()
dashboard_service = MiningDashboardService( dashboard_service = MiningDashboardService(
config.get("power_cost", 0.0), config.get("power_cost", 0.0),
config.get("power_usage", 0.0), config.get("power_usage", 0.0),
config.get("wallet"), config.get("wallet")
network_fee=config.get("network_fee", 0.0) # Add network fee parameter
) )
worker_service = WorkerService() worker_service = WorkerService()
# Connect the services # Connect the services

View File

@ -1,4 +1,4 @@
# DeepSea Dashboard # Ocean.xyz Bitcoin Mining Dashboard
## A Retro Mining Monitoring Solution ## A Retro Mining Monitoring Solution
@ -6,10 +6,11 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
--- ---
## Gallery: ## Gallery:
![boot](https://github.com/user-attachments/assets/52d787ab-10d9-4c36-9cba-3ed8878dfa2b)
![DeepSea Boot](https://github.com/user-attachments/assets/77222f13-1e95-48ee-a418-afd0e6b7a920) ![dashboard](https://github.com/user-attachments/assets/1042b586-7f02-4514-83f6-1fee50c38b18)
![DeepSea Config](https://github.com/user-attachments/assets/48fcc2a6-f56e-48b9-ac61-b27e9b4a6e41) ![workers](https://github.com/user-attachments/assets/2d26dbd0-64b7-4f77-921c-c48ad2cb6122)
![DeepSea Dashboard](https://github.com/user-attachments/assets/f8f3671e-907a-456a-b8c6-5d9ecd07946c) ![blocks](https://github.com/user-attachments/assets/e38d6f17-5e89-4560-aeec-69a349fa12ba)
![notifications](https://github.com/user-attachments/assets/cb191fc5-fa85-49a6-a155-459c68008b8f)
--- ---
@ -24,6 +25,7 @@ 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
### 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
@ -40,7 +42,6 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **Backup Polling**: Fallback to traditional polling if real-time connection fails - **Backup Polling**: Fallback to traditional polling if real-time connection fails
- **Cross-Tab Synchronization**: Data consistency across multiple browser tabs - **Cross-Tab Synchronization**: Data consistency across multiple browser tabs
- **Server Health Monitoring**: Built-in watchdog processes ensure reliability - **Server Health Monitoring**: Built-in watchdog processes ensure reliability
- **Error Handling**: Displays a user-friendly error page (`error.html`) for unexpected issues.
### Distinctive Design Elements ### Distinctive Design Elements
- **Retro Terminal Aesthetic**: Nostalgic interface with modern functionality - **Retro Terminal Aesthetic**: Nostalgic interface with modern functionality
@ -48,19 +49,14 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **System Monitor**: Floating status display with uptime and refresh information - **System Monitor**: Floating status display with uptime and refresh information
- **Responsive Interface**: Adapts to desktop and mobile devices - **Responsive Interface**: Adapts to desktop and mobile devices
### DeepSea Theme
- **Underwater Effects**: Light rays and digital noise create an immersive experience.
- **Retro Glitch Effects**: Subtle animations for a nostalgic feel.
- **Theme Toggle**: Switch between Bitcoin and DeepSea themes with a single click.
## Quick Start ## Quick Start
### Installation ### Installation
1. Clone the repository 1. Clone the repository
``` ```
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git git clone https://github.com/Djobleezy/Custom-Ocean.xyz-Dashboard.git
cd DeepSea-Dashboard cd Custom-Ocean.xyz-Dashboard
``` ```
2. Install dependencies: 2. Install dependencies:
@ -73,12 +69,21 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
python setup.py python setup.py
``` ```
4. Start the application: 4. Configure your mining settings in [config.json](https://github.com/Djobleezy/Custom-Ocean.xyz-Dashboard/blob/main/config.json):
```json
{
"power_cost": 0.12,
"power_usage": 3450,
"wallet": "yourwallethere" <--- make sure to replace this value in all project files (boot.html, app.py, config.py, config.json, & setup.py)
}
```
5. Start the application:
``` ```
python App.py python App.py
``` ```
5. Open your browser at `http://localhost:5000` 6. Open your browser at `http://localhost:5000`
For detailed deployment instructions with Redis persistence and Gunicorn configuration, see [deployment_steps.md](deployment_steps.md). For detailed deployment instructions with Redis persistence and Gunicorn configuration, see [deployment_steps.md](deployment_steps.md).
@ -108,8 +113,6 @@ You can modify the following environment variables in the `docker-compose.yml` f
- `WALLET`: Your Bitcoin wallet address. - `WALLET`: Your Bitcoin wallet address.
- `POWER_COST`: Cost of power per kWh. - `POWER_COST`: Cost of power per kWh.
- `POWER_USAGE`: Power usage in watts. - `POWER_USAGE`: Power usage in watts.
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
- `TIMEZONE`: Local timezone for displaying time information.
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.
@ -165,18 +168,12 @@ Built with a modern stack for reliability and performance:
- **Resilience**: Automatic recovery mechanisms and state persistence - **Resilience**: Automatic recovery mechanisms and state persistence
- **Configuration**: Environment variables and JSON-based settings - **Configuration**: Environment variables and JSON-based settings
## API Endpoints
- `/api/metrics`: Provides real-time mining metrics.
- `/api/available_timezones`: Returns a list of supported timezones.
- `/api/config`: Fetches or updates the mining configuration.
- `/api/health`: Returns the health status of the application.
## Project Structure ## Project Structure
The project follows a modular architecture with clear separation of concerns: The project follows a modular architecture with clear separation of concerns:
``` ```
DeepSea-Dashboard/ bitcoin-mining-dashboard/
├── App.py # Main application entry point ├── App.py # Main application entry point
├── config.py # Configuration management ├── config.py # Configuration management
@ -185,12 +182,9 @@ DeepSea-Dashboard/
├── models.py # Data models ├── models.py # Data models
├── state_manager.py # Manager for persistent state ├── state_manager.py # Manager for persistent state
├── worker_service.py # Service for worker data management ├── worker_service.py # Service for worker data management
├── notification_service.py # Service for notifications
├── minify.py # Script for minifying assets
├── setup.py # Setup script for organizing files ├── setup.py # Setup script for organizing files
├── requirements.txt # Python dependencies ├── requirements.txt # Python dependencies
├── Dockerfile # Docker configuration ├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
├── templates/ # HTML templates ├── templates/ # HTML templates
│ ├── base.html # Base template with common elements │ ├── base.html # Base template with common elements
@ -198,7 +192,6 @@ DeepSea-Dashboard/
│ ├── dashboard.html # Main dashboard template │ ├── dashboard.html # Main dashboard template
│ ├── workers.html # Workers dashboard template │ ├── workers.html # Workers dashboard template
│ ├── blocks.html # Bitcoin blocks template │ ├── blocks.html # Bitcoin blocks template
│ ├── notifications.html # Notifications template
│ └── error.html # Error page template │ └── error.html # Error page template
├── static/ # Static assets ├── static/ # Static assets
@ -208,24 +201,18 @@ DeepSea-Dashboard/
│ │ ├── workers.css # Workers page styles │ │ ├── workers.css # Workers page styles
│ │ ├── boot.css # Boot sequence styles │ │ ├── boot.css # Boot sequence styles
│ │ ├── blocks.css # Blocks page styles │ │ ├── blocks.css # Blocks page styles
│ │ ├── notifications.css # Notifications page styles
│ │ ├── error.css # Error page styles │ │ ├── error.css # Error page styles
│ │ ├── retro-refresh.css # Floating refresh bar styles │ │ └── retro-refresh.css # Floating refresh bar styles
│ │ └── theme-toggle.css # Theme toggle styles
│ │ │ │
│ └── js/ # JavaScript files │ └── js/ # JavaScript files
│ ├── main.js # Main dashboard functionality │ ├── main.js # Main dashboard functionality
│ ├── workers.js # Workers page functionality │ ├── workers.js # Workers page functionality
│ ├── blocks.js # Blocks page functionality │ ├── blocks.js # Blocks page functionality
│ ├── notifications.js # Notifications functionality
│ ├── block-animation.js # Block mining animation │ ├── block-animation.js # Block mining animation
│ ├── BitcoinProgressBar.js # System monitor functionality │ └── BitcoinProgressBar.js # System monitor functionality
│ └── theme.js # Theme toggle functionality
├── deployment_steps.md # Deployment guide ├── deployment_steps.md # Deployment guide
├── project_structure.md # Additional structure documentation └── project_structure.md # Additional structure documentation
├── LICENSE.md # License information
└── logs/ # Application logs (generated at runtime)
``` ```
For more detailed information on the architecture and component interactions, see [project_structure.md](project_structure.md). For more detailed information on the architecture and component interactions, see [project_structure.md](project_structure.md).
@ -239,7 +226,6 @@ For optimal performance:
3. Use the system monitor to verify connection status 3. Use the system monitor to verify connection status
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)
## License ## License
@ -248,6 +234,5 @@ Available under the MIT License. This is an independent project not affiliated w
## Acknowledgments ## Acknowledgments
- Ocean.xyz mining pool for their service - Ocean.xyz mining pool for their service
- mempool.guide
- The open-source community for their contributions - The open-source community for their contributions
- Bitcoin protocol developers - Bitcoin protocol developers

View File

@ -1,7 +1,5 @@
{ {
"power_cost": 0.0, "power_cost": 0.0,
"power_usage": 0.0, "power_usage": 0.0,
"wallet": "yourwallethere", "wallet": "yourwallethere"
"timezone": "America/Los_Angeles",
"network_fee": 0.0
} }

View File

@ -12,13 +12,14 @@ CONFIG_FILE = "config.json"
def load_config(): def load_config():
""" """
Load configuration from file or return defaults if file doesn't exist. Load configuration from file or return defaults if file doesn't exist.
Returns:
dict: Configuration dictionary with settings
""" """
default_config = { default_config = {
"power_cost": 0.0, "power_cost": 0.0,
"power_usage": 0.0, "power_usage": 0.0,
"wallet": "yourwallethere", "wallet": "yourwallethere"
"timezone": "America/Los_Angeles",
"network_fee": 0.0 # Add default network fee
} }
if os.path.exists(CONFIG_FILE): if os.path.exists(CONFIG_FILE):
@ -26,12 +27,6 @@ def load_config():
with open(CONFIG_FILE, "r") as f: with open(CONFIG_FILE, "r") as f:
config = json.load(f) config = json.load(f)
logging.info(f"Configuration loaded from {CONFIG_FILE}") logging.info(f"Configuration loaded from {CONFIG_FILE}")
# Ensure network_fee is present even in existing config files
if "network_fee" not in config:
config["network_fee"] = default_config["network_fee"]
logging.info("Added missing network_fee to config with default value")
return config return config
except Exception as e: except Exception as e:
logging.error(f"Error loading config: {e}") logging.error(f"Error loading config: {e}")
@ -40,28 +35,6 @@ def load_config():
return default_config return default_config
def get_timezone():
"""
Get the configured timezone with fallback to default.
Returns:
str: Timezone identifier
"""
# First check environment variable (for Docker)
import os
env_timezone = os.environ.get("TIMEZONE")
if env_timezone:
return env_timezone
# Then check config file
config = load_config()
timezone = config.get("timezone")
if timezone:
return timezone
# Default to Los Angeles
return "America/Los_Angeles"
def save_config(config): def save_config(config):
""" """
Save configuration to file. Save configuration to file.

View File

@ -12,12 +12,11 @@ import requests
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from models import OceanData, WorkerData, convert_to_ths from models import OceanData, WorkerData, convert_to_ths
from config import get_timezone
class MiningDashboardService: class MiningDashboardService:
"""Service for fetching and processing mining dashboard data.""" """Service for fetching and processing mining dashboard data."""
def __init__(self, power_cost, power_usage, wallet, network_fee=0.0): def __init__(self, power_cost, power_usage, wallet):
""" """
Initialize the mining dashboard service. Initialize the mining dashboard service.
@ -25,12 +24,10 @@ class MiningDashboardService:
power_cost (float): Cost of power in $ per kWh power_cost (float): Cost of power in $ per kWh
power_usage (float): Power usage in watts power_usage (float): Power usage in watts
wallet (str): Bitcoin wallet address for Ocean.xyz wallet (str): Bitcoin wallet address for Ocean.xyz
network_fee (float): Additional network fee percentage
""" """
self.power_cost = power_cost self.power_cost = power_cost
self.power_usage = power_usage self.power_usage = power_usage
self.wallet = wallet self.wallet = wallet
self.network_fee = network_fee
self.cache = {} self.cache = {}
self.sats_per_btc = 100_000_000 self.sats_per_btc = 100_000_000
self.previous_values = {} self.previous_values = {}
@ -82,25 +79,7 @@ class MiningDashboardService:
block_reward = 3.125 block_reward = 3.125
blocks_per_day = 86400 / 600 blocks_per_day = 86400 / 600
daily_btc_gross = hash_proportion * block_reward * blocks_per_day daily_btc_gross = hash_proportion * block_reward * blocks_per_day
daily_btc_net = daily_btc_gross * (1 - 0.02 - 0.028)
# Use actual pool fees instead of hardcoded values
# Get the pool fee percentage from ocean_data, default to 2.0% if not available
pool_fee_percent = ocean_data.pool_fees_percentage if ocean_data.pool_fees_percentage is not None else 2.0
# Get the network fee from the configuration (default to 0.0% if not set)
from config import load_config
config = load_config()
network_fee_percent = config.get("network_fee", 0.0)
# Calculate total fee percentage (converting from percentage to decimal)
total_fee_rate = (pool_fee_percent + network_fee_percent) / 100.0
# Calculate net BTC accounting for actual fees
daily_btc_net = daily_btc_gross * (1 - total_fee_rate)
# Log the fee calculations for transparency
logging.info(f"Earnings calculation using pool fee: {pool_fee_percent}% + network fee: {network_fee_percent}%")
logging.info(f"Total fee rate: {total_fee_rate}, Daily BTC gross: {daily_btc_gross}, Daily BTC net: {daily_btc_net}")
daily_revenue = round(daily_btc_net * btc_price, 2) if btc_price is not None else None daily_revenue = round(daily_btc_net * btc_price, 2) if btc_price is not None else None
daily_power_cost = round((self.power_usage / 1000) * self.power_cost * 24, 2) daily_power_cost = round((self.power_usage / 1000) * self.power_cost * 24, 2)
@ -133,11 +112,7 @@ class MiningDashboardService:
'block_number': block_count, 'block_number': block_count,
'network_hashrate': (network_hashrate / 1e18) if network_hashrate else None, 'network_hashrate': (network_hashrate / 1e18) if network_hashrate else None,
'difficulty': difficulty, 'difficulty': difficulty,
'daily_btc_gross': daily_btc_gross,
'daily_btc_net': daily_btc_net, 'daily_btc_net': daily_btc_net,
'pool_fee_percent': pool_fee_percent,
'network_fee_percent': network_fee_percent,
'total_fee_rate': total_fee_rate,
'estimated_earnings_per_day': estimated_earnings_per_day, 'estimated_earnings_per_day': estimated_earnings_per_day,
'daily_revenue': daily_revenue, 'daily_revenue': daily_revenue,
'daily_power_cost': daily_power_cost, 'daily_power_cost': daily_power_cost,
@ -161,8 +136,8 @@ class MiningDashboardService:
metrics['estimated_rewards_in_window_sats'] = int(round(estimated_rewards_in_window * self.sats_per_btc)) metrics['estimated_rewards_in_window_sats'] = int(round(estimated_rewards_in_window * self.sats_per_btc))
# --- Add server timestamps to the response in Los Angeles Time --- # --- Add server timestamps to the response in Los Angeles Time ---
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat() metrics["server_timestamp"] = datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
metrics["server_start_time"] = datetime.now(ZoneInfo(get_timezone())).isoformat() metrics["server_start_time"] = datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
# Log execution time # Log execution time
execution_time = time.time() - start_time execution_time = time.time() - start_time
@ -385,7 +360,7 @@ class MiningDashboardService:
try: try:
naive_dt = datetime.strptime(last_share_str, "%Y-%m-%d %H:%M") naive_dt = datetime.strptime(last_share_str, "%Y-%m-%d %H:%M")
utc_dt = naive_dt.replace(tzinfo=ZoneInfo("UTC")) utc_dt = naive_dt.replace(tzinfo=ZoneInfo("UTC"))
la_dt = utc_dt.astimezone(ZoneInfo(get_timezone())) la_dt = utc_dt.astimezone(ZoneInfo("America/Los_Angeles"))
data.total_last_share = la_dt.strftime("%Y-%m-%d %I:%M %p") data.total_last_share = la_dt.strftime("%Y-%m-%d %I:%M %p")
except Exception as e: except Exception as e:
logging.error(f"Error converting last share time '{last_share_str}': {e}") logging.error(f"Error converting last share time '{last_share_str}': {e}")
@ -663,6 +638,7 @@ class MiningDashboardService:
# Find total worker counts # Find total worker counts
workers_online = 0 workers_online = 0
workers_offline = 0 workers_offline = 0
avg_acceptance_rate = 95.0 # Default value
# Iterate through worker rows in the table # Iterate through worker rows in the table
for row in workers_table.find_all('tr', class_='table-row'): for row in workers_table.find_all('tr', class_='table-row'):
@ -698,6 +674,7 @@ class MiningDashboardService:
"efficiency": 90.0, # Default efficiency "efficiency": 90.0, # Default efficiency
"last_share": "N/A", "last_share": "N/A",
"earnings": 0, "earnings": 0,
"acceptance_rate": 95.0, # Default acceptance rate
"power_consumption": 0, "power_consumption": 0,
"temperature": 0 "temperature": 0
} }
@ -837,8 +814,9 @@ class MiningDashboardService:
'workers_online': workers_online, 'workers_online': workers_online,
'workers_offline': workers_offline, 'workers_offline': workers_offline,
'total_earnings': total_earnings, 'total_earnings': total_earnings,
'avg_acceptance_rate': avg_acceptance_rate,
'daily_sats': daily_sats, 'daily_sats': daily_sats,
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat() 'timestamp': datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
} }
logging.info(f"Successfully retrieved worker data: {len(workers)} workers") logging.info(f"Successfully retrieved worker data: {len(workers)} workers")
@ -897,6 +875,7 @@ class MiningDashboardService:
"efficiency": 90.0, "efficiency": 90.0,
"last_share": "N/A", "last_share": "N/A",
"earnings": 0, "earnings": 0,
"acceptance_rate": 95.0,
"power_consumption": 0, "power_consumption": 0,
"temperature": 0 "temperature": 0
} }
@ -983,7 +962,8 @@ class MiningDashboardService:
'workers_online': workers_online, 'workers_online': workers_online,
'workers_offline': workers_offline, 'workers_offline': workers_offline,
'total_earnings': total_earnings, 'total_earnings': total_earnings,
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat() 'avg_acceptance_rate': 99.0,
'timestamp': datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
} }
logging.info(f"Successfully retrieved {len(workers)} workers across multiple pages") logging.info(f"Successfully retrieved {len(workers)} workers across multiple pages")
return result return result

View File

@ -16,8 +16,8 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
1. Clone the repository: 1. Clone the repository:
```bash ```bash
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git git clone https://github.com/yourusername/bitcoin-mining-dashboard.git
cd DeepSea-Dashboard cd bitcoin-mining-dashboard
``` ```
2. Create a virtual environment (recommended): 2. Create a virtual environment (recommended):
@ -36,12 +36,21 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
python setup.py python setup.py
``` ```
5. Start the application: 5. Configure your mining parameters in `config.json`:
```json
{
"power_cost": 0.12, // Cost of electricity per kWh
"power_usage": 3450, // Power consumption in watts
"wallet": "your-wallet-address" // Your Ocean.xyz wallet
}
```
6. Start the application:
```bash ```bash
python App.py python App.py
``` ```
6. Access the dashboard at `http://localhost:5000` 7. Access the dashboard at `http://localhost:5000`
### Option 2: Production Deployment with Gunicorn ### Option 2: Production Deployment with Gunicorn

View File

@ -8,11 +8,6 @@ services:
- redis_data:/data - redis_data:/data
ports: ports:
- "6379:6379" - "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
dashboard: dashboard:
build: . build: .
@ -21,22 +16,14 @@ services:
- "5000:5000" - "5000:5000"
environment: environment:
- REDIS_URL=redis://redis:6379 - REDIS_URL=redis://redis:6379
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS - WALLET=bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9
- POWER_COST=0 - POWER_COST=0
- POWER_USAGE=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
depends_on: depends_on:
redis: - redis
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes: volumes:
redis_data: redis_data:

View File

@ -1,4 +1,4 @@
FROM python:3.9.18-slim FROM python:3.9-slim
WORKDIR /app WORKDIR /app
@ -17,8 +17,8 @@ COPY *.py .
COPY config.json . COPY config.json .
COPY setup.py . COPY setup.py .
# Create all necessary directories in one command # Create necessary directories
RUN mkdir -p static/css static/js templates logs /app/logs RUN mkdir -p static/css static/js templates logs
# Copy static files and templates # Copy static files and templates
COPY static/css/*.css static/css/ COPY static/css/*.css static/css/
@ -29,7 +29,7 @@ COPY templates/*.html templates/
RUN python setup.py RUN python setup.py
# Run the minifier to process HTML templates # Run the minifier to process HTML templates
RUN python minify.py RUN python minify.py
# Create a non-root user for better security # Create a non-root user for better security
RUN adduser --disabled-password --gecos '' appuser RUN adduser --disabled-password --gecos '' appuser
@ -37,6 +37,9 @@ RUN adduser --disabled-password --gecos '' appuser
# Change ownership of the /app directory so appuser can write files # Change ownership of the /app directory so appuser can write files
RUN chown -R appuser:appuser /app RUN chown -R appuser:appuser /app
# Create a directory for logs with proper permissions
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
# Switch to non-root user # Switch to non-root user
USER appuser USER appuser
@ -46,6 +49,7 @@ EXPOSE 5000
# Set environment variables # Set environment variables
ENV FLASK_ENV=production ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV PYTHON_UNBUFFERED=1
# Add healthcheck # Add healthcheck
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \ HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \

223
minify.py
View File

@ -1,14 +1,6 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os import os
import jsmin import jsmin
import htmlmin
import logging
from pathlib import Path
# Set up logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def minify_js_files(): def minify_js_files():
"""Minify JavaScript files.""" """Minify JavaScript files."""
@ -17,230 +9,25 @@ def minify_js_files():
os.makedirs(min_dir, exist_ok=True) os.makedirs(min_dir, exist_ok=True)
minified_count = 0 minified_count = 0
skipped_count = 0
for js_file in os.listdir(js_dir): for js_file in os.listdir(js_dir):
if js_file.endswith('.js') and not js_file.endswith('.min.js'): if js_file.endswith('.js') and not js_file.endswith('.min.js'):
try:
input_path = os.path.join(js_dir, js_file) input_path = os.path.join(js_dir, js_file)
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js')) output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
# Skip already minified files if they're newer than source with open(input_path, 'r') as f:
if os.path.exists(output_path) and \
os.path.getmtime(output_path) > os.path.getmtime(input_path):
logger.info(f"Skipping {js_file} (already up to date)")
skipped_count += 1
continue
with open(input_path, 'r', encoding='utf-8') as f:
js_content = f.read() js_content = f.read()
# Minify the content # Minify the content
minified = jsmin.jsmin(js_content) minified = jsmin.jsmin(js_content)
# Write minified content # Write minified content
with open(output_path, 'w', encoding='utf-8') as f: with open(output_path, 'w') as f:
f.write(minified) f.write(minified)
size_original = len(js_content)
size_minified = len(minified)
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
logger.info(f"Minified {js_file} - Reduced by {reduction:.1f}%")
minified_count += 1 minified_count += 1
except Exception as e: print(f"Minified {js_file}")
logger.error(f"Error processing {js_file}: {e}")
logger.info(f"JavaScript minification: {minified_count} files minified, {skipped_count} files skipped") print(f"Total files minified: {minified_count}")
return minified_count
def minify_css_files():
"""Minify CSS files using simple compression techniques."""
css_dir = 'static/css'
min_dir = os.path.join(css_dir, 'min')
os.makedirs(min_dir, exist_ok=True)
minified_count = 0
skipped_count = 0
for css_file in os.listdir(css_dir):
if css_file.endswith('.css') and not css_file.endswith('.min.css'):
try:
input_path = os.path.join(css_dir, css_file)
output_path = os.path.join(min_dir, css_file.replace('.css', '.min.css'))
# Skip already minified files if they're newer than source
if os.path.exists(output_path) and \
os.path.getmtime(output_path) > os.path.getmtime(input_path):
logger.info(f"Skipping {css_file} (already up to date)")
skipped_count += 1
continue
with open(input_path, 'r', encoding='utf-8') as f:
css_content = f.read()
# Simple CSS minification using string replacements
# Remove comments
import re
css_minified = re.sub(r'/\*[\s\S]*?\*/', '', css_content)
# Remove whitespace
css_minified = re.sub(r'\s+', ' ', css_minified)
# Remove spaces around selectors
css_minified = re.sub(r'\s*{\s*', '{', css_minified)
css_minified = re.sub(r'\s*}\s*', '}', css_minified)
css_minified = re.sub(r'\s*;\s*', ';', css_minified)
css_minified = re.sub(r'\s*:\s*', ':', css_minified)
css_minified = re.sub(r'\s*,\s*', ',', css_minified)
# Remove last semicolons
css_minified = re.sub(r';}', '}', css_minified)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(css_minified)
size_original = len(css_content)
size_minified = len(css_minified)
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
logger.info(f"Minified {css_file} - Reduced by {reduction:.1f}%")
minified_count += 1
except Exception as e:
logger.error(f"Error processing {css_file}: {e}")
logger.info(f"CSS minification: {minified_count} files minified, {skipped_count} files skipped")
return minified_count
def minify_html_templates():
"""Minify HTML template files."""
templates_dir = 'templates'
minified_count = 0
skipped_count = 0
for html_file in os.listdir(templates_dir):
if html_file.endswith('.html'):
try:
input_path = os.path.join(templates_dir, html_file)
with open(input_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# Minify HTML content while keeping important whitespace
minified = htmlmin.minify(html_content,
remove_comments=True,
remove_empty_space=True,
remove_all_empty_space=False,
reduce_boolean_attributes=True)
# Write back to the same file
with open(input_path, 'w', encoding='utf-8') as f:
f.write(minified)
size_original = len(html_content)
size_minified = len(minified)
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
logger.info(f"Minified {html_file} - Reduced by {reduction:.1f}%")
minified_count += 1
except Exception as e:
logger.error(f"Error processing {html_file}: {e}")
logger.info(f"HTML minification: {minified_count} files minified, {skipped_count} files skipped")
return minified_count
def create_size_report():
"""Create a report of file sizes before and after minification."""
results = []
# Check JS files
js_dir = 'static/js'
min_dir = os.path.join(js_dir, 'min')
if os.path.exists(min_dir):
for js_file in os.listdir(js_dir):
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
orig_path = os.path.join(js_dir, js_file)
min_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
if os.path.exists(min_path):
orig_size = os.path.getsize(orig_path)
min_size = os.path.getsize(min_path)
reduction = (1 - min_size / orig_size) * 100 if orig_size > 0 else 0
results.append({
'file': js_file,
'type': 'JavaScript',
'original_size': orig_size,
'minified_size': min_size,
'reduction': reduction
})
# Check CSS files
css_dir = 'static/css'
min_dir = os.path.join(css_dir, 'min')
if os.path.exists(min_dir):
for css_file in os.listdir(css_dir):
if css_file.endswith('.css') and not css_file.endswith('.min.css'):
orig_path = os.path.join(css_dir, css_file)
min_path = os.path.join(min_dir, css_file.replace('.css', '.min.css'))
if os.path.exists(min_path):
orig_size = os.path.getsize(orig_path)
min_size = os.path.getsize(min_path)
reduction = (1 - min_size / orig_size) * 100 if orig_size > 0 else 0
results.append({
'file': css_file,
'type': 'CSS',
'original_size': orig_size,
'minified_size': min_size,
'reduction': reduction
})
# Print the report
total_orig = sum(item['original_size'] for item in results)
total_min = sum(item['minified_size'] for item in results)
total_reduction = (1 - total_min / total_orig) * 100 if total_orig > 0 else 0
logger.info("\n" + "="*50)
logger.info("MINIFICATION REPORT")
logger.info("="*50)
logger.info(f"{'File':<30} {'Type':<10} {'Original':<10} {'Minified':<10} {'Reduction'}")
logger.info("-"*70)
for item in results:
logger.info(f"{item['file']:<30} {item['type']:<10} "
f"{item['original_size']/1024:.1f}KB {item['minified_size']/1024:.1f}KB "
f"{item['reduction']:.1f}%")
logger.info("-"*70)
logger.info(f"{'TOTAL:':<30} {'':<10} {total_orig/1024:.1f}KB {total_min/1024:.1f}KB {total_reduction:.1f}%")
logger.info("="*50)
def main():
"""Main function to run minification tasks."""
import argparse
parser = argparse.ArgumentParser(description='Minify web assets')
parser.add_argument('--js', action='store_true', help='Minify JavaScript files')
parser.add_argument('--css', action='store_true', help='Minify CSS files')
parser.add_argument('--html', action='store_true', help='Minify HTML templates')
parser.add_argument('--all', action='store_true', help='Minify all assets')
parser.add_argument('--report', action='store_true', help='Generate size report only')
args = parser.parse_args()
# If no arguments, default to --all
if not (args.js or args.css or args.html or args.report):
args.all = True
if args.all or args.js:
minify_js_files()
if args.all or args.css:
minify_css_files()
if args.all or args.html:
minify_html_templates()
# Always generate the report at the end if any minification was done
if args.report or args.all or args.js or args.css:
create_size_report()
if __name__ == "__main__": if __name__ == "__main__":
main() minify_js_files()

View File

@ -2,13 +2,11 @@
Data models for the Bitcoin Mining Dashboard. Data models for the Bitcoin Mining Dashboard.
""" """
from dataclasses import dataclass from dataclasses import dataclass
from typing import Optional, Dict, List, Union, Any
import logging import logging
@dataclass @dataclass
class OceanData: class OceanData:
"""Data structure for Ocean.xyz pool mining data.""" """Data structure for Ocean.xyz pool mining data."""
# Keep original definitions with None default to maintain backward compatibility
pool_total_hashrate: float = None pool_total_hashrate: float = None
pool_total_hashrate_unit: str = None pool_total_hashrate_unit: str = None
hashrate_24hr: float = None hashrate_24hr: float = None
@ -33,42 +31,6 @@ class OceanData:
blocks_found: str = None blocks_found: str = None
total_last_share: str = "N/A" total_last_share: str = "N/A"
last_block_earnings: str = None last_block_earnings: str = None
pool_fees_percentage: float = None # Added missing attribute
def get_normalized_hashrate(self, timeframe: str = "3hr") -> float:
"""
Get a normalized hashrate value in TH/s regardless of original units.
Args:
timeframe: The timeframe to get ("24hr", "3hr", "10min", "5min", "60sec")
Returns:
float: Normalized hashrate in TH/s
"""
if timeframe == "24hr" and self.hashrate_24hr is not None:
return convert_to_ths(self.hashrate_24hr, self.hashrate_24hr_unit)
elif timeframe == "3hr" and self.hashrate_3hr is not None:
return convert_to_ths(self.hashrate_3hr, self.hashrate_3hr_unit)
elif timeframe == "10min" and self.hashrate_10min is not None:
return convert_to_ths(self.hashrate_10min, self.hashrate_10min_unit)
elif timeframe == "5min" and self.hashrate_5min is not None:
return convert_to_ths(self.hashrate_5min, self.hashrate_5min_unit)
elif timeframe == "60sec" and self.hashrate_60sec is not None:
return convert_to_ths(self.hashrate_60sec, self.hashrate_60sec_unit)
return 0.0
def to_dict(self) -> Dict[str, Any]:
"""Convert the OceanData object to a dictionary."""
return {k: v for k, v in self.__dict__.items()}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OceanData':
"""Create an OceanData instance from a dictionary."""
filtered_data = {}
for k, v in data.items():
if k in cls.__annotations__:
filtered_data[k] = v
return cls(**filtered_data)
@dataclass @dataclass
class WorkerData: class WorkerData:
@ -88,61 +50,6 @@ class WorkerData:
power_consumption: float = 0 power_consumption: float = 0
temperature: float = 0 temperature: float = 0
def __post_init__(self):
"""
Validate worker data after initialization.
Ensures values are within acceptable ranges and formats.
"""
# Ensure hashrates are non-negative
if self.hashrate_60sec is not None and self.hashrate_60sec < 0:
self.hashrate_60sec = 0
if self.hashrate_3hr is not None and self.hashrate_3hr < 0:
self.hashrate_3hr = 0
# Ensure status is valid, but don't raise exceptions for backward compatibility
if self.status not in ["online", "offline"]:
logging.warning(f"Worker {self.name}: Invalid status '{self.status}', using 'offline'")
self.status = "offline"
# Ensure type is valid, but don't raise exceptions for backward compatibility
if self.type not in ["ASIC", "Bitaxe"]:
logging.warning(f"Worker {self.name}: Invalid type '{self.type}', using 'ASIC'")
self.type = "ASIC"
def get_normalized_hashrate(self, timeframe: str = "3hr") -> float:
"""
Get normalized hashrate in TH/s.
Args:
timeframe: The timeframe to get ("3hr" or "60sec")
Returns:
float: Normalized hashrate in TH/s
"""
if timeframe == "3hr":
return convert_to_ths(self.hashrate_3hr, self.hashrate_3hr_unit)
elif timeframe == "60sec":
return convert_to_ths(self.hashrate_60sec, self.hashrate_60sec_unit)
return 0.0
def to_dict(self) -> Dict[str, Any]:
"""Convert the WorkerData object to a dictionary."""
return {k: v for k, v in self.__dict__.items()}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'WorkerData':
"""Create a WorkerData instance from a dictionary."""
filtered_data = {}
for k, v in data.items():
if k in cls.__annotations__:
filtered_data[k] = v
return cls(**filtered_data)
class HashRateConversionError(Exception):
"""Exception raised for errors in hashrate unit conversion."""
pass
def convert_to_ths(value, unit): def convert_to_ths(value, unit):
""" """
Convert any hashrate unit to TH/s equivalent. Convert any hashrate unit to TH/s equivalent.

View File

@ -6,13 +6,6 @@ import uuid
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
# Constants to replace magic values
ONE_DAY_SECONDS = 86400
DEFAULT_TARGET_HOUR = 12
SIGNIFICANT_HASHRATE_CHANGE_PERCENT = 25
NOTIFICATION_WINDOW_MINUTES = 5
class NotificationLevel(Enum): class NotificationLevel(Enum):
INFO = "info" INFO = "info"
@ -47,78 +40,51 @@ class NotificationService:
# Load last block height from state # Load last block height from state
self._load_last_block_height() self._load_last_block_height()
def _get_redis_value(self, key: str, default: Any = None) -> Any: def _load_notifications(self):
"""Generic method to retrieve values from Redis.""" """Load notifications from persistent storage."""
try:
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
value = self.state_manager.redis_client.get(key)
if value:
return value.decode('utf-8')
return default
except Exception as e:
logging.error(f"[NotificationService] Error retrieving {key} from Redis: {e}")
return default
def _set_redis_value(self, key: str, value: Any) -> bool:
"""Generic method to set values in Redis."""
try:
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
self.state_manager.redis_client.set(key, str(value))
logging.info(f"[NotificationService] Saved {key} to Redis: {value}")
return True
return False
except Exception as e:
logging.error(f"[NotificationService] Error saving {key} to Redis: {e}")
return False
def _load_notifications(self) -> None:
"""Load notifications with enhanced error handling."""
try: try:
stored_notifications = self.state_manager.get_notifications() stored_notifications = self.state_manager.get_notifications()
if stored_notifications: if stored_notifications:
self.notifications = stored_notifications self.notifications = stored_notifications
logging.info(f"[NotificationService] Loaded {len(self.notifications)} notifications from storage") logging.info(f"Loaded {len(self.notifications)} notifications from storage")
else:
self.notifications = []
logging.info("[NotificationService] No notifications found in storage, starting with empty list")
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error loading notifications: {e}") logging.error(f"Error loading notifications: {e}")
self.notifications = [] # Ensure we have a valid list
def _load_last_block_height(self) -> None: def _load_last_block_height(self):
"""Load last block height from persistent storage.""" """Load last block height from persistent storage."""
try: try:
self.last_block_height = self._get_redis_value("last_block_height") if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
if self.last_block_height: # Use Redis if available
logging.info(f"[NotificationService] Loaded last block height from storage: {self.last_block_height}") last_height = self.state_manager.redis_client.get("last_block_height")
if last_height:
self.last_block_height = last_height.decode('utf-8')
logging.info(f"Loaded last block height from storage: {self.last_block_height}")
else: else:
logging.info("[NotificationService] No last block height found, starting with None") logging.info("Redis not available, starting with no last block height")
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error loading last block height: {e}") logging.error(f"Error loading last block height: {e}")
def _save_last_block_height(self) -> None: def _save_last_block_height(self):
"""Save last block height to persistent storage.""" """Save last block height to persistent storage."""
if self.last_block_height:
self._set_redis_value("last_block_height", self.last_block_height)
def _save_notifications(self) -> None:
"""Save notifications with improved pruning."""
try: try:
# Sort by timestamp before pruning to ensure we keep the most recent if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client and self.last_block_height:
self.state_manager.redis_client.set("last_block_height", str(self.last_block_height))
logging.info(f"Saved last block height to storage: {self.last_block_height}")
except Exception as e:
logging.error(f"Error saving last block height: {e}")
def _save_notifications(self):
"""Save notifications to persistent storage."""
try:
# Prune to max size before saving
if len(self.notifications) > self.max_notifications: if len(self.notifications) > self.max_notifications:
self.notifications.sort(key=lambda n: n.get("timestamp", ""), reverse=True) self.notifications = self.notifications[-self.max_notifications:]
self.notifications = self.notifications[:self.max_notifications]
self.state_manager.save_notifications(self.notifications) self.state_manager.save_notifications(self.notifications)
logging.info(f"[NotificationService] Saved {len(self.notifications)} notifications")
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error saving notifications: {e}") logging.error(f"Error saving notifications: {e}")
def add_notification(self, def add_notification(self, message, level=NotificationLevel.INFO, category=NotificationCategory.SYSTEM, data=None):
message: str,
level: NotificationLevel = NotificationLevel.INFO,
category: NotificationCategory = NotificationCategory.SYSTEM,
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
""" """
Add a new notification. Add a new notification.
@ -146,17 +112,12 @@ class NotificationService:
self.notifications.append(notification) self.notifications.append(notification)
self._save_notifications() self._save_notifications()
logging.info(f"[NotificationService] Added notification: {message}") logging.info(f"Added notification: {message}")
return notification return notification
def get_notifications(self, def get_notifications(self, limit=50, offset=0, unread_only=False, category=None, level=None):
limit: int = 50,
offset: int = 0,
unread_only: bool = False,
category: Optional[str] = None,
level: Optional[str] = None) -> List[Dict[str, Any]]:
""" """
Get filtered notifications with optimized filtering. Get filtered notifications.
Args: Args:
limit (int): Maximum number to return limit (int): Maximum number to return
@ -168,25 +129,31 @@ class NotificationService:
Returns: Returns:
list: Filtered notifications list: Filtered notifications
""" """
# Apply all filters in a single pass filtered = self.notifications
filtered = [
n for n in self.notifications # Apply filters
if (not unread_only or not n.get("read", False)) and if unread_only:
(not category or n.get("category") == category) and filtered = [n for n in filtered if not n.get("read", False)]
(not level or n.get("level") == level)
] if category:
filtered = [n for n in filtered if n.get("category") == category]
if level:
filtered = [n for n in filtered if n.get("level") == level]
# Sort by timestamp (newest first) # Sort by timestamp (newest first)
filtered = sorted(filtered, key=lambda n: n.get("timestamp", ""), reverse=True) filtered = sorted(filtered, key=lambda n: n.get("timestamp", ""), reverse=True)
# Apply pagination # Apply pagination
return filtered[offset:offset + limit] paginated = filtered[offset:offset + limit]
def get_unread_count(self) -> int: return paginated
def get_unread_count(self):
"""Get count of unread notifications.""" """Get count of unread notifications."""
return sum(1 for n in self.notifications if not n.get("read", False)) return sum(1 for n in self.notifications if not n.get("read", False))
def mark_as_read(self, notification_id: Optional[str] = None) -> bool: def mark_as_read(self, notification_id=None):
""" """
Mark notification(s) as read. Mark notification(s) as read.
@ -202,18 +169,16 @@ class NotificationService:
for n in self.notifications: for n in self.notifications:
if n.get("id") == notification_id: if n.get("id") == notification_id:
n["read"] = True n["read"] = True
logging.info(f"[NotificationService] Marked notification {notification_id} as read")
break break
else: else:
# Mark all as read # Mark all as read
for n in self.notifications: for n in self.notifications:
n["read"] = True n["read"] = True
logging.info(f"[NotificationService] Marked all {len(self.notifications)} notifications as read")
self._save_notifications() self._save_notifications()
return True return True
def delete_notification(self, notification_id: str) -> bool: def delete_notification(self, notification_id):
""" """
Delete a specific notification. Delete a specific notification.
@ -223,19 +188,13 @@ class NotificationService:
Returns: Returns:
bool: True if successful bool: True if successful
""" """
original_count = len(self.notifications)
self.notifications = [n for n in self.notifications if n.get("id") != notification_id] self.notifications = [n for n in self.notifications if n.get("id") != notification_id]
deleted = original_count - len(self.notifications)
if deleted > 0:
logging.info(f"[NotificationService] Deleted notification {notification_id}")
self._save_notifications() self._save_notifications()
return True
return deleted > 0 def clear_notifications(self, category=None, older_than_days=None):
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None) -> int:
""" """
Clear notifications with optimized filtering. Clear notifications.
Args: Args:
category (str, optional): Only clear specific category category (str, optional): Only clear specific category
@ -246,48 +205,44 @@ class NotificationService:
""" """
original_count = len(self.notifications) original_count = len(self.notifications)
cutoff_date = None if category and older_than_days:
if older_than_days:
cutoff_date = datetime.now() - timedelta(days=older_than_days) cutoff_date = datetime.now() - timedelta(days=older_than_days)
# Apply filters in a single pass
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 n.get("category") != category or
(not cutoff_date or datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date) datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date
] ]
elif category:
self.notifications = [n for n in self.notifications if n.get("category") != category]
elif older_than_days:
cutoff_date = datetime.now() - timedelta(days=older_than_days)
self.notifications = [
n for n in self.notifications
if datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date
]
else:
self.notifications = []
cleared_count = original_count - len(self.notifications)
if cleared_count > 0:
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
self._save_notifications() self._save_notifications()
return original_count - len(self.notifications)
return cleared_count def check_and_generate_notifications(self, current_metrics, previous_metrics):
def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
""" """
Check metrics and generate notifications for significant events. Check metrics and generate notifications for significant events.
Args:
current_metrics: Current system metrics
previous_metrics: Previous system metrics for comparison
Returns:
list: Newly created notifications
""" """
new_notifications = [] new_notifications = []
try: try:
# Skip if no metrics # Skip if no metrics
if not current_metrics: if not current_metrics:
logging.warning("[NotificationService] No current metrics available, skipping notification checks") logging.warning("No current metrics available, skipping notification checks")
return new_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")
if last_block_height and last_block_height != "N/A": if last_block_height and last_block_height != "N/A":
if self.last_block_height is not None and self.last_block_height != last_block_height: if self.last_block_height is not None and self.last_block_height != last_block_height:
logging.info(f"[NotificationService] Block change detected: {self.last_block_height} -> {last_block_height}") logging.info(f"Block change detected: {self.last_block_height} -> {last_block_height}")
block_notification = self._generate_block_notification(current_metrics) block_notification = self._generate_block_notification(current_metrics)
if block_notification: if block_notification:
new_notifications.append(block_notification) new_notifications.append(block_notification)
@ -318,7 +273,7 @@ class NotificationService:
return new_notifications return new_notifications
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error generating notifications: {e}") logging.error(f"Error generating notifications: {e}")
error_notification = self.add_notification( error_notification = self.add_notification(
f"Error generating notifications: {str(e)}", f"Error generating notifications: {str(e)}",
level=NotificationLevel.ERROR, level=NotificationLevel.ERROR,
@ -326,28 +281,42 @@ class NotificationService:
) )
return [error_notification] return [error_notification]
def _should_post_daily_stats(self) -> bool: def _should_post_daily_stats(self):
"""Check if it's time to post daily stats with improved clarity.""" """Check if it's time to post daily stats (once per day at 12 PM)."""
now = datetime.now() now = datetime.now()
# Only proceed if we're in the target hour and within first 5 minutes # Target time is 12 PM (noon)
if now.hour != DEFAULT_TARGET_HOUR or now.minute >= NOTIFICATION_WINDOW_MINUTES: target_hour = 12
return False current_hour = now.hour
current_minute = now.minute
# If we have a last_daily_stats timestamp, check if it's a different day # If we have a last_daily_stats timestamp
if self.last_daily_stats and now.date() <= self.last_daily_stats.date(): if self.last_daily_stats:
return False # Check if it's a different day
is_different_day = now.date() > self.last_daily_stats.date()
# All conditions met, update timestamp and return True # Only post if:
logging.info(f"[NotificationService] Posting daily stats at {now.hour}:{now.minute}") # 1. It's a different day AND
# 2. It's the target hour (12 PM) AND
# 3. It's within the first 5 minutes of that hour
if is_different_day and current_hour == target_hour and current_minute < 5:
logging.info(f"Posting daily stats at {current_hour}:{current_minute}")
self.last_daily_stats = now
return True
else:
# First time - post if we're at the target hour
if current_hour == target_hour and current_minute < 5:
logging.info(f"First time posting daily stats at {current_hour}:{current_minute}")
self.last_daily_stats = now self.last_daily_stats = now
return True return True
def _generate_daily_stats(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]: return False
def _generate_daily_stats(self, metrics):
"""Generate daily stats notification.""" """Generate daily stats notification."""
try: try:
if not metrics: if not metrics:
logging.warning("[NotificationService] No metrics available for daily stats") logging.warning("No metrics available for daily stats")
return None return None
# Format hashrate with appropriate unit # Format hashrate with appropriate unit
@ -362,7 +331,7 @@ class NotificationService:
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 (${daily_profit_usd:.2f})"
# Add notification # Add notification
logging.info(f"[NotificationService] Generating daily stats notification: {message}") logging.info(f"Generating daily stats notification: {message}")
return self.add_notification( return self.add_notification(
message, message,
level=NotificationLevel.INFO, level=NotificationLevel.INFO,
@ -375,16 +344,16 @@ class NotificationService:
} }
) )
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error generating daily stats notification: {e}") logging.error(f"Error generating daily stats notification: {e}")
return None return None
def _generate_block_notification(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]: def _generate_block_notification(self, metrics):
"""Generate notification for a new block found.""" """Generate notification for a new block found."""
try: try:
last_block_height = metrics.get("last_block_height", "Unknown") last_block_height = metrics.get("last_block_height", "Unknown")
last_block_earnings = metrics.get("last_block_earnings", "0") last_block_earnings = metrics.get("last_block_earnings", "0")
logging.info(f"[NotificationService] Generating block notification: height={last_block_height}, earnings={last_block_earnings}") logging.info(f"Generating block notification: height={last_block_height}, earnings={last_block_earnings}")
message = f"New block found by the pool! Block #{last_block_height}, earnings: {last_block_earnings} SATS" message = f"New block found by the pool! Block #{last_block_height}, earnings: {last_block_earnings} SATS"
@ -398,95 +367,87 @@ class NotificationService:
} }
) )
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error generating block notification: {e}") logging.error(f"Error generating block notification: {e}")
return None return None
def _parse_numeric_value(self, value_str: Any) -> float: def _check_hashrate_change(self, current, previous):
"""Parse numeric values from strings that may include units."""
if isinstance(value_str, (int, float)):
return float(value_str)
if isinstance(value_str, str):
# Extract just the numeric part
parts = value_str.split()
try:
return float(parts[0])
except (ValueError, IndexError):
pass
return 0.0
def _check_hashrate_change(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Check for significant hashrate changes using 10-minute average.""" """Check for significant hashrate changes using 10-minute average."""
try: try:
# Get 10min hashrate values # Change from 3hr to 10min hashrate values
current_10min = current.get("hashrate_10min", 0) current_10min = current.get("hashrate_10min", 0)
previous_10min = previous.get("hashrate_10min", 0) previous_10min = previous.get("hashrate_10min", 0)
# Log what we're comparing # Log what we're comparing
logging.debug(f"[NotificationService] Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}") logging.debug(f"Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}")
# Skip if values are missing # Skip if values are missing
if not current_10min or not previous_10min: if not current_10min or not previous_10min:
logging.debug("[NotificationService] Skipping hashrate check - missing values") logging.debug("Skipping hashrate check - missing values")
return None return None
# Parse values consistently # Handle strings with units (e.g., "10.5 TH/s")
current_value = self._parse_numeric_value(current_10min) if isinstance(current_10min, str):
previous_value = self._parse_numeric_value(previous_10min) current_10min = float(current_10min.split()[0])
else:
current_10min = float(current_10min)
logging.debug(f"[NotificationService] Converted 10min hashrates - current: {current_value}, previous: {previous_value}") if isinstance(previous_10min, str):
previous_10min = float(previous_10min.split()[0])
else:
previous_10min = float(previous_10min)
logging.debug(f"Converted 10min hashrates - current: {current_10min}, previous: {previous_10min}")
# Skip if previous was zero (prevents division by zero) # Skip if previous was zero (prevents division by zero)
if previous_value == 0: if previous_10min == 0:
logging.debug("[NotificationService] Skipping hashrate check - previous was zero") logging.debug("Skipping hashrate check - previous was zero")
return None return None
# Calculate percentage change # Calculate percentage change
percent_change = ((current_value - previous_value) / previous_value) * 100 percent_change = ((current_10min - previous_10min) / previous_10min) * 100
logging.debug(f"[NotificationService] 10min hashrate change: {percent_change:.1f}%") logging.debug(f"10min hashrate change: {percent_change:.1f}%")
# Significant decrease # Significant decrease (more than 25%)
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT: if percent_change <= -25:
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease" message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease"
logging.info(f"[NotificationService] Generating hashrate notification: {message}") logging.info(f"Generating hashrate notification: {message}")
return self.add_notification( return self.add_notification(
message, message,
level=NotificationLevel.WARNING, level=NotificationLevel.WARNING,
category=NotificationCategory.HASHRATE, category=NotificationCategory.HASHRATE,
data={ data={
"previous": previous_value, "previous": previous_10min,
"current": current_value, "current": current_10min,
"change": percent_change, "change": percent_change,
"timeframe": "10min" "timeframe": "10min" # Add timeframe to the data
} }
) )
# Significant increase # Significant increase (more than 25%)
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT: elif percent_change >= 25:
message = f"10min hashrate increase detected: {percent_change:.1f}% increase" message = f"10min hashrate increase detected: {percent_change:.1f}% increase"
logging.info(f"[NotificationService] Generating hashrate notification: {message}") logging.info(f"Generating hashrate notification: {message}")
return self.add_notification( return self.add_notification(
message, message,
level=NotificationLevel.SUCCESS, level=NotificationLevel.SUCCESS,
category=NotificationCategory.HASHRATE, category=NotificationCategory.HASHRATE,
data={ data={
"previous": previous_value, "previous": previous_10min,
"current": current_value, "current": current_10min,
"change": percent_change, "change": percent_change,
"timeframe": "10min" "timeframe": "10min" # Add timeframe to the data
} }
) )
return None return None
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error checking hashrate change: {e}") logging.error(f"Error checking hashrate change: {e}")
return None return None
def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]: def _check_earnings_progress(self, current, previous):
"""Check for significant earnings progress or payout approach.""" """Check for significant earnings progress or payout approach."""
try: try:
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0")) current_unpaid = float(current.get("unpaid_earnings", "0").split()[0]) if isinstance(current.get("unpaid_earnings"), str) else 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"):
@ -519,7 +480,7 @@ class NotificationService:
# Check for payout (unpaid balance reset) # Check for payout (unpaid balance reset)
if previous.get("unpaid_earnings"): if previous.get("unpaid_earnings"):
previous_unpaid = self._parse_numeric_value(previous.get("unpaid_earnings", "0")) previous_unpaid = float(previous.get("unpaid_earnings", "0").split()[0]) if isinstance(previous.get("unpaid_earnings"), str) else previous.get("unpaid_earnings", 0)
# If balance significantly decreased, likely a payout occurred # If balance significantly decreased, likely a payout occurred
if previous_unpaid > 0 and current_unpaid < previous_unpaid * 0.5: if previous_unpaid > 0 and current_unpaid < previous_unpaid * 0.5:
@ -537,12 +498,12 @@ class NotificationService:
return None return None
except Exception as e: except Exception as e:
logging.error(f"[NotificationService] Error checking earnings progress: {e}") logging.error(f"Error checking earnings progress: {e}")
return None return None
def _should_send_payout_notification(self) -> bool: def _should_send_payout_notification(self):
"""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 = datetime.now() - self.last_payout_notification_time time_since_last_notification = datetime.now() - self.last_payout_notification_time
return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS return time_since_last_notification.total_seconds() > 86400 # 1 Day

View File

@ -5,29 +5,22 @@ This document provides a comprehensive overview of the Bitcoin Mining Dashboard
## Directory Structure ## Directory Structure
``` ```
DeepSea-Dashboard/ bitcoin-mining-dashboard/
├── App.py # Main application entry point ├── App.py # Main application entry point and Flask routes
├── config.py # Configuration management ├── config.py # Configuration management utilities
├── config.json # Configuration file ├── config.json # User configuration file
├── data_service.py # Service for fetching mining data ├── data_service.py # Service for fetching mining/market data
├── models.py # Data models ├── models.py # Data models and conversion utilities
├── state_manager.py # Manager for persistent state ├── state_manager.py # Manager for persistent state and history
├── worker_service.py # Service for worker data management ├── worker_service.py # Service for worker data management
├── notification_service.py # Service for notifications
├── minify.py # Script for minifying assets
├── setup.py # Setup script for organizing files
├── requirements.txt # Python dependencies
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
├── templates/ # HTML templates ├── templates/ # HTML templates
│ ├── base.html # Base template with common elements │ ├── base.html # Base template with common elements
│ ├── boot.html # Boot sequence animation │ ├── boot.html # Boot sequence animation
│ ├── dashboard.html # Main dashboard template │ ├── dashboard.html # Main dashboard template
│ ├── workers.html # Workers dashboard template │ ├── workers.html # Workers overview template
│ ├── blocks.html # Bitcoin blocks template │ ├── blocks.html # Bitcoin blocks template
│ ├── notifications.html # Notifications template
│ └── error.html # Error page template │ └── error.html # Error page template
├── static/ # Static assets ├── static/ # Static assets
@ -37,24 +30,23 @@ DeepSea-Dashboard/
│ │ ├── workers.css # Workers page styles │ │ ├── workers.css # Workers page styles
│ │ ├── boot.css # Boot sequence styles │ │ ├── boot.css # Boot sequence styles
│ │ ├── blocks.css # Blocks page styles │ │ ├── blocks.css # Blocks page styles
│ │ ├── notifications.css # Notifications page styles
│ │ ├── error.css # Error page styles │ │ ├── error.css # Error page styles
│ │ ├── retro-refresh.css # Floating refresh bar styles │ │ └── retro-refresh.css # Floating system monitor styles
│ │ └── theme-toggle.css # Theme toggle styles
│ │ │ │
│ └── js/ # JavaScript files │ └── js/ # JavaScript files
│ ├── main.js # Main dashboard functionality │ ├── main.js # Main dashboard functionality
│ ├── workers.js # Workers page functionality │ ├── workers.js # Workers page functionality
│ ├── blocks.js # Blocks page functionality │ ├── blocks.js # Blocks page functionality
│ ├── notifications.js # Notifications functionality
│ ├── block-animation.js # Block mining animation │ ├── block-animation.js # Block mining animation
│ ├── BitcoinProgressBar.js # System monitor functionality │ └── BitcoinProgressBar.js # System monitor implementation
│ └── theme.js # Theme toggle functionality
├── deployment_steps.md # Deployment guide ├── logs/ # Application logs directory
├── project_structure.md # Additional structure documentation ├── requirements.txt # Python dependencies
├── LICENSE.md # License information ├── Dockerfile # Docker configuration
└── logs/ # Application logs (generated at runtime) ├── setup.py # Setup script for organizing files
├── deployment_steps.md # Deployment documentation
├── project_structure.md # This document
└── README.md # Project overview and instructions
``` ```
## Core Components ## Core Components
@ -128,7 +120,7 @@ The application uses Jinja2 templates with a retro-themed design:
Client-side functionality is organized into modular JavaScript files: Client-side functionality is organized into modular JavaScript files:
- **main.js**: Dashboard functionality, real-time updates, and chart rendering - **main.js**: Dashboard functionality, real-time updates, and chart rendering
- **workers.js**: Worker grid rendering, filtering, and mini-chart creation - **workers.js**: Worker grid rendering, filtering, and mini-chart creation
- **blocks.js**: Block explorer with data fetching from mempool.guide - **blocks.js**: Block explorer with data fetching from mempool.space
- **block-animation.js**: Interactive block mining animation - **block-animation.js**: Interactive block mining animation
- **BitcoinProgressBar.js**: Floating system monitor with uptime and connection status - **BitcoinProgressBar.js**: Floating system monitor with uptime and connection status
@ -218,7 +210,7 @@ All hashrates are normalized to TH/s internally because:
``` ```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Ocean.xyz API │ │ blockchain.info │ │ mempool.guide │ │ Ocean.xyz API │ │ blockchain.info │ │ mempool.space │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼

View File

@ -60,7 +60,6 @@ FILE_MAPPINGS = {
'retro-refresh.css': 'static/css/retro-refresh.css', 'retro-refresh.css': 'static/css/retro-refresh.css',
'blocks.css': 'static/css/blocks.css', 'blocks.css': 'static/css/blocks.css',
'notifications.css': 'static/css/notifications.css', 'notifications.css': 'static/css/notifications.css',
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
# JS files # JS files
'main.js': 'static/js/main.js', 'main.js': 'static/js/main.js',
@ -68,7 +67,6 @@ FILE_MAPPINGS = {
'blocks.js': 'static/js/blocks.js', 'blocks.js': 'static/js/blocks.js',
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js', 'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
'notifications.js': 'static/js/notifications.js', 'notifications.js': 'static/js/notifications.js',
'theme.js': 'static/js/theme.js', # Added theme.js
# Template files # Template files
'base.html': 'templates/base.html', 'base.html': 'templates/base.html',
@ -84,9 +82,7 @@ FILE_MAPPINGS = {
DEFAULT_CONFIG = { DEFAULT_CONFIG = {
"power_cost": 0.0, "power_cost": 0.0,
"power_usage": 0.0, "power_usage": 0.0,
"wallet": "yourwallethere", "wallet": "yourwallethere"
"timezone": "America/Los_Angeles", # Added default timezone
"network_fee": 0.0 # Added default network fee
} }
def parse_arguments(): def parse_arguments():
@ -96,13 +92,10 @@ def parse_arguments():
parser.add_argument('--wallet', type=str, help='Set your Ocean.xyz wallet address') parser.add_argument('--wallet', type=str, help='Set your Ocean.xyz wallet address')
parser.add_argument('--power-cost', type=float, help='Set your electricity cost per kWh') parser.add_argument('--power-cost', type=float, help='Set your electricity cost per kWh')
parser.add_argument('--power-usage', type=float, help='Set your power consumption in watts') parser.add_argument('--power-usage', type=float, help='Set your power consumption in watts')
parser.add_argument('--network-fee', type=float, help='Set your network fee percentage') # Added network fee parameter
parser.add_argument('--timezone', type=str, help='Set your timezone (e.g., America/Los_Angeles)') # Added timezone parameter
parser.add_argument('--skip-checks', action='store_true', help='Skip dependency checks') parser.add_argument('--skip-checks', action='store_true', help='Skip dependency checks')
parser.add_argument('--force', action='store_true', help='Force file overwrite') parser.add_argument('--force', action='store_true', help='Force file overwrite')
parser.add_argument('--config', type=str, help='Path to custom config.json') parser.add_argument('--config', type=str, help='Path to custom config.json')
parser.add_argument('--minify', action='store_true', help='Minify JavaScript files') parser.add_argument('--minify', action='store_true', help='Minify JavaScript files')
parser.add_argument('--theme', choices=['bitcoin', 'deepsea'], help='Set the default UI theme') # Added theme parameter
return parser.parse_args() return parser.parse_args()
def create_directory_structure(): def create_directory_structure():
@ -282,19 +275,6 @@ def create_config(args):
else: else:
logger.warning("Power usage cannot be negative, using default or existing value") logger.warning("Power usage cannot be negative, using default or existing value")
# Update config from command line arguments
if args.timezone:
config["timezone"] = args.timezone
if args.network_fee is not None:
if args.network_fee >= 0:
config["network_fee"] = args.network_fee
else:
logger.warning("Network fee cannot be negative, using default or existing value")
if args.theme:
config["theme"] = args.theme
# Save the configuration # Save the configuration
try: try:
with open(config_file, 'w') as f: with open(config_file, 'w') as f:
@ -308,9 +288,7 @@ def create_config(args):
logger.info("Current configuration:") logger.info("Current configuration:")
logger.info(f" ├── Wallet address: {config['wallet']}") logger.info(f" ├── Wallet address: {config['wallet']}")
logger.info(f" ├── Power cost: ${config['power_cost']} per kWh") logger.info(f" ├── Power cost: ${config['power_cost']} per kWh")
logger.info(f" ├── Power usage: {config['power_usage']} watts") logger.info(f" └── Power usage: {config['power_usage']} watts")
logger.info(f" ├── Network fee: {config['network_fee']}%")
logger.info(f" └── Timezone: {config['timezone']}")
return True return True

View File

@ -7,7 +7,6 @@ import time
import gc import gc
import threading import threading
import redis import redis
from config import get_timezone
# Global variables for arrow history, legacy hashrate history, and a log of full metrics snapshots. # Global variables for arrow history, legacy hashrate history, and a log of full metrics snapshots.
arrow_history = {} # stored per second arrow_history = {} # stored per second
@ -328,7 +327,7 @@ class StateManager:
from datetime import datetime from datetime import datetime
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
current_second = datetime.now(ZoneInfo(get_timezone())).strftime("%H:%M:%S") current_second = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%H:%M:%S")
with state_lock: with state_lock:
for key in arrow_keys: for key in arrow_keys:

View File

@ -112,6 +112,7 @@
font-size: 1.2rem; font-size: 1.2rem;
font-weight: bold; font-weight: bold;
color: var(--primary-color); color: var(--primary-color);
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
} }
.block-time { .block-time {
@ -141,22 +142,27 @@
.block-info-value.yellow { .block-info-value.yellow {
color: #ffd700; color: #ffd700;
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
} }
.block-info-value.green { .block-info-value.green {
color: #32CD32; color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
} }
.block-info-value.blue { .block-info-value.blue {
color: #00dfff; color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
} }
.block-info-value.white { .block-info-value.white {
color: #ffffff; color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
} }
.block-info-value.red { .block-info-value.red {
color: #ff5555; color: #ff5555;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
} }
/* Loader */ /* Loader */
@ -215,6 +221,7 @@
padding: 0.5rem 1rem; padding: 0.5rem 1rem;
font-size: 1.1rem; font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color); border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite; animation: flicker 4s infinite;
font-family: var(--header-font); font-family: var(--header-font);
display: flex; display: flex;
@ -235,6 +242,7 @@
.block-modal-close:hover, .block-modal-close:hover,
.block-modal-close:focus { .block-modal-close:focus {
color: #ffa500; color: #ffa500;
text-shadow: 0 0 10px rgba(255, 165, 0, 0.8);
} }
.block-modal-body { .block-modal-body {
@ -257,6 +265,7 @@
font-size: 1.1rem; font-size: 1.1rem;
color: var(--primary-color); color: var(--primary-color);
margin-bottom: 10px; margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
font-weight: bold; font-weight: bold;
} }

View File

@ -1,442 +1,3 @@
/* Config form styling - fixed width and hidden by default */
#config-form {
display: none;
width: 500px;
max-width: 90%;
margin: 30px auto;
padding: 20px;
background-color: #0d0d0d;
border: 1px solid #f7931a;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
border-radius: 4px;
}
/* Boot text color - updated with theme toggling */
body:not(.deepsea-theme) #terminal,
body:not(.deepsea-theme) #output,
body:not(.deepsea-theme) #prompt-container,
body:not(.deepsea-theme) #prompt-text,
body:not(.deepsea-theme) #user-input,
body:not(.deepsea-theme) #loading-message {
color: #f7931a;
}
/* DeepSea theme text color */
body.deepsea-theme #terminal,
body.deepsea-theme #output,
body.deepsea-theme #prompt-container,
body.deepsea-theme #prompt-text,
body.deepsea-theme #user-input,
body.deepsea-theme #loading-message {
color: #0088cc;
}
/* DeepSea cursor color */
body.deepsea-theme .cursor,
body.deepsea-theme .prompt-cursor {
background-color: #0088cc;
box-shadow: 0 0 5px rgba(0, 136, 204, 0.8);
}
/* Boot-specific DeepSea theme adjustments */
body.deepsea-theme #bitcoin-logo {
color: #0088cc;
border-color: #0088cc;
text-shadow: 0 0 10px rgba(0, 136, 204, 0.5);
box-shadow: 0 0 15px rgba(0, 136, 204, 0.5);
}
body.deepsea-theme #config-form {
border: 1px solid #0088cc;
box-shadow: 0 0 10px rgba(0, 136, 204, 0.5);
}
body.deepsea-theme .config-title {
color: #0088cc;
}
body.deepsea-theme .form-group label {
color: #0088cc;
}
body.deepsea-theme .form-group input,
body.deepsea-theme .form-group select {
border: 1px solid #0088cc;
}
body.deepsea-theme .form-group input:focus,
body.deepsea-theme .form-group select:focus {
box-shadow: 0 0 5px #0088cc;
}
body.deepsea-theme .btn {
background-color: #0088cc;
}
body.deepsea-theme .btn:hover {
background-color: #00b3ff;
}
body.deepsea-theme .btn-secondary {
background-color: #333;
color: #0088cc;
}
body.deepsea-theme .tooltip .tooltip-text {
border: 1px solid #0088cc;
}
body.deepsea-theme .form-group select {
background-image: linear-gradient(45deg, transparent 50%, #0088cc 50%), linear-gradient(135deg, #0088cc 50%, transparent 50%);
}
/* DeepSea skip button */
body.deepsea-theme #skip-button {
background-color: #0088cc;
box-shadow: 0 0 8px rgba(0, 136, 204, 0.5);
}
body.deepsea-theme #skip-button:hover {
background-color: #00b3ff;
box-shadow: 0 0 12px rgba(0, 136, 204, 0.7);
}
/* Original Bitcoin styling preserved by default */
.config-title {
font-size: 24px;
text-align: center;
margin-bottom: 20px;
color: #f7931a;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #f7931a;
}
.form-group input,
.form-group select {
width: 100%;
padding: 8px;
background-color: #0d0d0d;
border: 1px solid #f7931a;
color: #fff;
font-family: 'VT323', monospace;
font-size: 18px;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
box-shadow: 0 0 5px #f7931a;
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn {
padding: 8px 16px;
background-color: #f7931a;
color: #000;
border: none;
cursor: pointer;
font-family: 'VT323', monospace;
font-size: 18px;
}
.btn:hover {
background-color: #ffa32e;
}
.btn-secondary {
background-color: #333;
color: #f7931a;
}
#form-message {
margin-top: 15px;
padding: 10px;
border-radius: 3px;
display: none;
}
.message-success {
background-color: rgba(50, 205, 50, 0.2);
border: 1px solid #32CD32;
color: #32CD32;
}
.message-error {
background-color: rgba(255, 0, 0, 0.2);
border: 1px solid #ff0000;
color: #ff0000;
}
.tooltip {
position: relative;
display: inline-block;
margin-left: 5px;
width: 14px;
height: 14px;
background-color: #333;
color: #fff;
border-radius: 50%;
text-align: center;
line-height: 14px;
font-size: 10px;
cursor: help;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: #000;
color: #fff;
text-align: center;
border-radius: 3px;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
font-size: 14px;
border: 1px solid #f7931a;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Style the select dropdown with custom arrow */
.form-group select {
appearance: none;
-webkit-appearance: none;
-moz-appearance: none;
background-image: linear-gradient(45deg, transparent 50%, #f7931a 50%), linear-gradient(135deg, #f7931a 50%, transparent 50%);
background-position: calc(100% - 15px) calc(1em + 0px), calc(100% - 10px) calc(1em + 0px);
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 30px;
}
/* Base styling for the Bitcoin logo */
#bitcoin-logo {
position: relative;
white-space: pre;
font-family: monospace;
height: 130px; /* Set fixed height to match original logo */
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
/* Update the DeepSea theme logo styling */
body.deepsea-theme #bitcoin-logo {
color: transparent; /* Hide original logo */
position: relative;
text-shadow: none;
min-height: 120px; /* Ensure enough height for the new logo */
}
/* Add the new DeepSea ASCII art */
body.deepsea-theme #bitcoin-logo::after {
content: " ____ ____ \A| _ \\ ___ ___ _ __/ ___| ___ __ _ \A| | | |/ _ \\/ _ \\ '_ \\___ \\ / _ \\/ _` |\A| |_| | __/ __/ |_) |__) | __/ (_| |\A|____/ \\___|\\___|_.__/____/ \\___|\\__,_|\A|_| ";
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%); /* Center perfectly */
font-size: 100%; /* Full size */
font-weight: bold;
line-height: 1.2;
color: #0088cc;
white-space: pre;
display: block;
text-shadow: 0 0 10px rgba(0, 136, 204, 0.5);
font-family: monospace;
z-index: 1;
padding: 10px 0;
}
/* Add "DeepSea" version info */
body.deepsea-theme #bitcoin-logo::before {
content: "v.21";
position: absolute;
bottom: 0;
right: 10px;
color: #0088cc;
font-size: 16px;
text-shadow: 0 0 5px rgba(0, 136, 204, 0.5);
font-family: 'VT323', monospace;
z-index: 2; /* Ensure version displays on top */
}
/* Ocean Wave Ripple Effect for DeepSea Theme */
body.deepsea-theme::after {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
background: transparent;
opacity: 0.1;
z-index: 10;
animation: oceanRipple 8s infinite linear;
background-image: repeating-linear-gradient( 0deg, rgba(0, 136, 204, 0.1), rgba(0, 136, 204, 0.1) 1px, transparent 1px, transparent 6px );
background-size: 100% 6px;
}
/* Ocean waves moving animation */
@keyframes oceanRipple {
0% {
transform: translateY(0);
}
100% {
transform: translateY(6px);
}
}
/* Retro glitch effect for DeepSea Theme */
body.deepsea-theme::before {
content: "";
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 3;
opacity: 0.15;
background-image: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 73, 109, 0.1) 50%), linear-gradient(90deg, rgba(0, 81, 122, 0.03), rgba(0, 136, 204, 0.08), rgba(0, 191, 255, 0.03));
background-size: 100% 2px, 3px 100%;
animation: glitchEffect 2s infinite;
}
/* Glitch animation */
@keyframes glitchEffect {
0% {
opacity: 0.15;
background-position: 0 0;
}
20% {
opacity: 0.17;
}
40% {
opacity: 0.14;
background-position: -1px 0;
}
60% {
opacity: 0.15;
background-position: 1px 0;
}
80% {
opacity: 0.16;
background-position: -2px 0;
}
100% {
opacity: 0.15;
background-position: 0 0;
}
}
/* Deep underwater light rays */
body.deepsea-theme {
position: relative;
overflow: hidden;
}
body.deepsea-theme .underwater-rays {
position: fixed;
top: -50%;
left: -50%;
right: -50%;
bottom: -50%;
width: 200%;
height: 200%;
background: rgba(0, 0, 0, 0);
pointer-events: none;
z-index: 1;
background-image: radial-gradient(ellipse at top, rgba(0, 136, 204, 0.1) 0%, rgba(0, 136, 204, 0) 70%), radial-gradient(ellipse at bottom, rgba(0, 91, 138, 0.15) 0%, rgba(0, 0, 0, 0) 70%);
animation: lightRays 15s ease infinite alternate;
}
/* Light ray animation */
@keyframes lightRays {
0% {
transform: rotate(0deg) scale(1);
opacity: 0.3;
}
50% {
opacity: 0.4;
}
100% {
transform: rotate(360deg) scale(1.1);
opacity: 0.3;
}
}
/* Subtle digital noise texture */
body.deepsea-theme .digital-noise {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4woEFQwNDaabTQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAACASURBVGje7dixDcIwFEbhb8QMKWn5dwEWY4fswAasRJkBkhfAIarsNDEF5x5LrV/dJ1cEAAAAAOzHuefF5byzZ7tS6xDj6qoQpdRxUvNM6lH3rPeM1+ZJ3ROtqe9feGcjY8z74M8UvJGxEVHxTcIbGSsR+SECAAAAsC9/8G82GwHDD80AAAAASUVORK5CYII=');
opacity: 0.05;
z-index: 2;
pointer-events: none;
animation: noise 0.5s steps(5) infinite;
}
/* Noise animation */
@keyframes noise {
0% {
transform: translate(0, 0);
}
20% {
transform: translate(-1px, 1px);
}
40% {
transform: translate(1px, -1px);
}
60% {
transform: translate(-2px, -1px);
}
80% {
transform: translate(2px, 1px);
}
100% {
transform: translate(0, 0);
}
}
/* Base Styles with a subtle radial background for extra depth */ /* Base Styles with a subtle radial background for extra depth */
body { body {
background: linear-gradient(135deg, #121212, #000000); background: linear-gradient(135deg, #121212, #000000);
@ -447,6 +8,7 @@ body {
margin: 0; margin: 0;
padding: 10px; padding: 10px;
overflow-x: hidden; overflow-x: hidden;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.4);
height: calc(100vh - 100px); height: calc(100vh - 100px);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -517,26 +79,32 @@ body::before {
/* Neon-inspired color classes */ /* Neon-inspired color classes */
.green { .green {
color: #39ff14 !important; color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
} }
.blue { .blue {
color: #00dfff !important; color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
} }
.yellow { .yellow {
color: #ffd700 !important; color: #ffd700 !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
} }
.white { .white {
color: #ffffff !important; color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
} }
.red { .red {
color: #ff2d2d !important; color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
} }
.magenta { .magenta {
color: #ff2d95 !important; color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
} }
/* Bitcoin Logo styling with extra neon border */ /* Bitcoin Logo styling with extra neon border */
@ -575,7 +143,6 @@ body::before {
font-size: 16px; font-size: 16px;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5); box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
transition: all 0.2s ease; transition: all 0.2s ease;
z-index: 50; /* Lower z-index value */
} }
#skip-button:hover { #skip-button:hover {
@ -583,24 +150,6 @@ body::before {
box-shadow: 0 0 12px rgba(247, 147, 26, 0.7); box-shadow: 0 0 12px rgba(247, 147, 26, 0.7);
} }
/* Mobile-specific adjustments */
@media (max-width: 768px) {
#skip-button {
bottom: 25px;
right: 10px;
padding: 10px 18px; /* Larger touch target for mobile */
font-size: 18px;
height: 40px;
z-index: 50;
}
}
/* Add this to your CSS */
#config-form {
z-index: 100; /* Higher than the skip button */
position: relative; /* Needed for z-index to work properly */
}
/* Prompt Styling */ /* Prompt Styling */
#prompt-container { #prompt-container {
display: none; display: none;
@ -610,6 +159,7 @@ body::before {
#prompt-text { #prompt-text {
color: #f7931a; color: #f7931a;
margin-right: 5px; margin-right: 5px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline; display: inline;
} }
@ -625,6 +175,7 @@ body::before {
height: 33px; height: 33px;
padding: 0; padding: 0;
margin: 0; margin: 0;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
} }
@ -652,6 +203,7 @@ body::before {
#loading-message { #loading-message {
text-align: center; text-align: center;
margin-bottom: 10px; margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
} }
#debug-info { #debug-info {

View File

@ -1,73 +1,3 @@
.footer {
margin-top: 30px;
padding: 10px 0;
color: grey;
font-size: 0.9rem;
border-top: 1px solid rgba(128, 128, 128, 0.2);
}
</style >
<!-- Preload theme to prevent flicker -->
<style id="theme-preload" >
/* Theme-aware loading state */
html.bitcoin-theme {
background-color: #111111;
}
html.deepsea-theme {
background-color: #0c141a;
}
#theme-loader {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
font-family: 'VT323', monospace;
}
html.bitcoin-theme #theme-loader {
background-color: #111111;
color: #f2a900;
}
html.deepsea-theme #theme-loader {
background-color: #0c141a;
color: #0088cc;
}
#loader-icon {
font-size: 48px;
margin-bottom: 20px;
animation: spin 2s infinite linear;
}
#loader-text {
font-size: 24px;
text-transform: uppercase;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Hide content during load */
body {
visibility: hidden;
}
/* Common styling elements shared across all pages */ /* Common styling elements shared across all pages */
:root { :root {
--bg-color: #0a0a0a; --bg-color: #0a0a0a;
@ -131,6 +61,7 @@ h1 {
color: var(--primary-color); color: var(--primary-color);
font-family: var(--header-font); font-family: var(--header-font);
letter-spacing: 1px; letter-spacing: 1px;
text-shadow: 0 0 10px var(--primary-color);
animation: flicker 4s infinite; animation: flicker 4s infinite;
} }
@ -179,6 +110,7 @@ h1 {
color: grey; color: grey;
text-decoration: none; text-decoration: none;
font-size: 0.7rem; /* Decreased font size */ font-size: 0.7rem; /* Decreased font size */
text-shadow: 0 0 5px grey;
padding: 5px 10px; /* Add padding for a larger clickable area */ padding: 5px 10px; /* Add padding for a larger clickable area */
transition: background-color 0.3s ease; /* Optional: Add hover effect */ transition: background-color 0.3s ease; /* Optional: Add hover effect */
} }
@ -235,6 +167,7 @@ h1 {
padding: 0.3rem 0.5rem; padding: 0.3rem 0.5rem;
font-size: 1.1rem; font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color); border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite; animation: flicker 4s infinite;
font-family: var(--header-font); font-family: var(--header-font);
} }
@ -256,6 +189,7 @@ h1 {
border-radius: 5px; border-radius: 5px;
z-index: 9999; z-index: 9999;
font-size: 0.9rem; font-size: 0.9rem;
text-shadow: 0 0 5px rgba(255, 0, 0, 0.8);
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5); box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
} }
@ -320,7 +254,7 @@ h1 {
position: relative; position: relative;
top: -1px; top: -1px;
animation: glow 3s infinite; animation: glow 3s infinite;
box-shadow: 0 0 10px red, 0 0 20px red !important; box-shadow: 0 0 10px red, 0 0 20px red;
} }
@keyframes glowRed { @keyframes glowRed {
@ -335,44 +269,53 @@ h1 {
.red-glow, .status-red { .red-glow, .status-red {
color: #ff2d2d !important; color: #ff2d2d !important;
text-shadow: 0 0 2px #ff2d2d, 0 0 2px #ff2d2d;
} }
.yellow-glow { .yellow-glow {
color: #ffd700 !important; color: #ffd700 !important;
text-shadow: 0 0 2px #ffd700, 0 0 2px #ffd700;
} }
.blue-glow { .blue-glow {
color: #00dfff !important; color: #00dfff !important;
text-shadow: 0 0 2px #00dfff, 0 0 2px #00dfff;
} }
.white-glow { .white-glow {
color: #ffffff !important; color: #ffffff !important;
text-shadow: 0 0 2px #ffffff, 0 0 2px #ffffff;
} }
/* Basic color classes for backward compatibility */ /* Basic color classes for backward compatibility */
.green { .green {
color: #39ff14 !important; color: #39ff14 !important;
text-shadow: 0 0 2px #39ff14, 0 0 2px #39ff14;
} }
.blue { .blue {
color: #00dfff !important; color: #00dfff !important;
text-shadow: 0 0 2px #00dfff, 0 0 2px #00dfff;
} }
.yellow { .yellow {
color: #ffd700 !important; color: #ffd700 !important;
font-weight: normal !important; text-shadow: 0 0 2px #ffd700, 0 0 2px #ffd700;
} }
.white { .white {
color: #ffffff !important; color: #ffffff !important;
text-shadow: 0 0 2px #ffffff, 0 0 2px #ffffff;
} }
.red { .red {
color: #ff2d2d !important; color: #ff2d2d !important;
text-shadow: 0 0 2px #ff2d2d, 0 0 2px #ff2d2d;
} }
.magenta { .magenta {
color: #ff2d95 !important; color: #ff2d95 !important;
text-shadow: 0 0 2px #ff2d95, 0 0 2px #ff2d95;
} }
/* Bitcoin Progress Bar Styles */ /* Bitcoin Progress Bar Styles */
@ -455,6 +398,7 @@ h1 {
font-size: 1rem; font-size: 1rem;
color: var(--primary-color); color: var(--primary-color);
margin-top: 0.3rem; margin-top: 0.3rem;
text-shadow: 0 0 5px var(--primary-color);
text-align: center; text-align: center;
width: 100%; width: 100%;
} }

View File

@ -113,6 +113,7 @@
.metric-value { .metric-value {
color: var(--text-color); color: var(--text-color);
font-weight: bold; font-weight: bold;
text-shadow: 0 0 6px #32cd32;
} }
/* Yellow color family (BTC price, sats metrics, time to payout) */ /* Yellow color family (BTC price, sats metrics, time to payout) */
@ -124,6 +125,7 @@
#estimated_rewards_in_window_sats, #estimated_rewards_in_window_sats,
#est_time_to_payout { #est_time_to_payout {
color: #ffd700; color: #ffd700;
text-shadow: 0 0 6px rgba(255, 215, 0, 0.6);
} }
/* Green color family (profits, earnings) */ /* Green color family (profits, earnings) */
@ -132,11 +134,13 @@
#daily_profit_usd, #daily_profit_usd,
#monthly_profit_usd { #monthly_profit_usd {
color: #32CD32; color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
} }
/* Red color family (costs) */ /* Red color family (costs) */
#daily_power_cost { #daily_power_cost {
color: #ff5555 !important; color: #ff5555 !important;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
} }
/* White metrics (general stats) */ /* White metrics (general stats) */
@ -150,16 +154,19 @@
#last_block_height, #last_block_height,
#pool_fees_percentage { #pool_fees_percentage {
color: #ffffff; color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
} }
/* Blue metrics (time data) */ /* Blue metrics (time data) */
#last_block_time { #last_block_time {
color: #00dfff; color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
} }
.card-body strong { .card-body strong {
color: var(--primary-color); color: var(--primary-color);
margin-right: 0.25rem; margin-right: 0.25rem;
text-shadow: 0 0 2px var(--primary-color);
} }
.card-body p { .card-body p {
@ -218,30 +225,14 @@
} }
.datum-label { .datum-label {
color: #ffffff; /* White color */ color: #ff9d00; /* Orange color */
font-size: 0.95em; font-size: 0.85em;
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
margin-left: 4px; margin-left: 4px;
padding: 2px 5px;
background-color: rgba(0, 0, 0, 0.2); background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px; border-radius: 3px;
letter-spacing: 2px; letter-spacing: 0.5px;
} vertical-align: middle;
/* Pool luck indicators */
.very-lucky {
color: #32CD32 !important;
font-weight: bold !important;
}
.lucky {
color: #90EE90 !important;
}
.normal-luck {
color: #ffd700 !important;
}
.unlucky {
color: #ff5555 !important;
} }

View File

@ -39,6 +39,7 @@ body {
color: var(--text-color); color: var(--text-color);
padding-top: 50px; padding-top: 50px;
font-family: var(--terminal-font); font-family: var(--terminal-font);
text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
} }
a.btn-primary { a.btn-primary {
@ -96,6 +97,7 @@ h1 {
margin-bottom: 1rem; margin-bottom: 1rem;
font-family: var(--header-font); font-family: var(--header-font);
font-weight: bold; font-weight: bold;
text-shadow: 0 0 10px var(--primary-color);
position: relative; position: relative;
z-index: 2; z-index: 2;
} }
@ -106,6 +108,7 @@ p {
position: relative; position: relative;
z-index: 2; z-index: 2;
color: #ff5555; color: #ff5555;
text-shadow: 0 0 8px rgba(255, 85, 85, 0.6);
} }
/* Cursor blink for terminal feel */ /* Cursor blink for terminal feel */
@ -130,5 +133,6 @@ p {
font-family: var(--terminal-font); font-family: var(--terminal-font);
font-size: 1.2rem; font-size: 1.2rem;
color: #00dfff; color: #00dfff;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
margin-bottom: 1rem; margin-bottom: 1rem;
} }

View File

@ -70,6 +70,7 @@ body {
font-weight: bold; font-weight: bold;
font-size: 1.1rem; /* Match card header font size */ font-size: 1.1rem; /* Match card header font size */
border-bottom: none; border-bottom: none;
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite; /* Add flicker animation from card headers */ animation: flicker 4s infinite; /* Add flicker animation from card headers */
font-family: var(--header-font); /* Use the same font variable */ font-family: var(--header-font); /* Use the same font variable */
padding: 0.3rem 0; /* Match card header padding */ padding: 0.3rem 0; /* Match card header padding */
@ -231,6 +232,7 @@ body {
#retro-terminal-bar #progress-text { #retro-terminal-bar #progress-text {
font-size: 16px; font-size: 16px;
color: var(--terminal-text); color: var(--terminal-text);
text-shadow: 0 0 5px var(--terminal-text);
margin-top: 5px; margin-top: 5px;
text-align: center; text-align: center;
position: relative; position: relative;
@ -240,6 +242,7 @@ body {
#retro-terminal-bar #uptimeTimer { #retro-terminal-bar #uptimeTimer {
font-size: 16px; font-size: 16px;
color: var(--terminal-text); color: var(--terminal-text);
text-shadow: 0 0 5px var(--terminal-text);
text-align: center; text-align: center;
position: relative; position: relative;
z-index: 2; z-index: 2;

View File

@ -1,209 +0,0 @@
/* Theme Toggle Button with positioning logic similar to topRightLink */
#themeToggle,
.theme-toggle-btn {
position: absolute; /* Change from fixed to absolute like topRightLink */
z-index: 1000;
background: transparent;
border-width: 1px;
border-style: solid;
font-family: 'VT323', monospace;
transition: all 0.3s ease;
cursor: pointer;
white-space: nowrap;
text-transform: uppercase;
outline: none;
display: flex;
align-items: center;
justify-content: center;
top: 30px; /* Match the top positioning of topRightLink */
left: 15px; /* Keep on left side */
}
/* Desktop specific styling */
@media screen and (min-width: 768px) {
#themeToggle,
.theme-toggle-btn {
padding: 6px 12px;
font-size: 14px;
border-radius: 3px;
letter-spacing: 0.5px;
}
/* Add theme icon for desktop view */
#themeToggle:before,
.theme-toggle-btn:before {
content: " ₿|🌊";
margin-right: 5px;
font-size: 14px;
}
/* Hover effects for desktop */
#themeToggle:hover,
.theme-toggle-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
}
/* Mobile-specific styling */
@media screen and (max-width: 767px) {
#themeToggle,
.theme-toggle-btn {
padding: 10px;
font-size: 12px;
border-radius: 3px;
width: 40px;
height: 35px;
}
/* Use just icon for mobile to save space */
#themeToggle:before,
.theme-toggle-btn:before {
content: " ₿|🌊";
margin-right: 0;
font-size: 14px;
}
/* Hide text on mobile */
#themeToggle span,
.theme-toggle-btn span {
display: none;
}
/* Adjust position when in portrait mode on very small screens */
@media screen and (max-height: 500px) {
#themeToggle,
.theme-toggle-btn {
top: 5px;
left: 5px; /* Keep on left side */
width: 35px;
height: 35px;
font-size: 10px;
}
}
}
/* The rest of the CSS remains unchanged */
/* Active state for the button */
#themeToggle:active,
.theme-toggle-btn:active {
transform: translateY(1px);
box-shadow: 0 0 2px rgba(0, 0, 0, 0.3);
}
/* Bitcoin theme specific styling (orange) */
body:not(.deepsea-theme) #themeToggle,
body:not(.deepsea-theme) .theme-toggle-btn {
color: #f2a900;
border-color: #f2a900;
}
body:not(.deepsea-theme) #themeToggle:hover,
body:not(.deepsea-theme) .theme-toggle-btn:hover {
background-color: rgba(242, 169, 0, 0.1);
box-shadow: 0 4px 8px rgba(242, 169, 0, 0.3);
}
/* DeepSea theme specific styling (blue) */
body.deepsea-theme #themeToggle,
body.deepsea-theme .theme-toggle-btn {
color: #0088cc;
border-color: #0088cc;
}
body.deepsea-theme #themeToggle:hover,
body.deepsea-theme .theme-toggle-btn:hover {
background-color: rgba(0, 136, 204, 0.1);
box-shadow: 0 4px 8px rgba(0, 136, 204, 0.3);
}
/* Transition effect for smoother theme switching */
#themeToggle,
.theme-toggle-btn,
#themeToggle:before,
.theme-toggle-btn:before {
transition: all 0.3s ease;
}
/* Accessibility improvements */
#themeToggle:focus,
.theme-toggle-btn:focus {
box-shadow: 0 0 0 3px rgba(0, 0, 0, 0.3);
outline: none;
}
body:not(.deepsea-theme) #themeToggle:focus,
body:not(.deepsea-theme) .theme-toggle-btn:focus {
box-shadow: 0 0 0 3px rgba(242, 169, 0, 0.3);
}
body.deepsea-theme #themeToggle:focus,
body.deepsea-theme .theme-toggle-btn:focus {
box-shadow: 0 0 0 3px rgba(0, 136, 204, 0.3);
}
/* Add to your common.css or theme-toggle.css */
html.deepsea-theme {
--primary-color: #0088cc;
}
html.bitcoin-theme {
--primary-color: #f2a900;
}
/* Add these theme-specific loading styles */
#theme-loader {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
font-family: 'VT323', monospace;
}
html.bitcoin-theme #theme-loader {
background-color: #111111;
color: #f2a900;
}
html.deepsea-theme #theme-loader {
background-color: #0c141a;
color: #0088cc;
}
#loader-icon {
font-size: 48px;
margin-bottom: 20px;
animation: spin 2s infinite linear;
}
#loader-text {
font-size: 24px;
text-transform: uppercase;
letter-spacing: 1px;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%, 100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}

View File

@ -95,6 +95,7 @@
color: var(--primary-color); color: var(--primary-color);
font-weight: bold; font-weight: bold;
font-size: 1.2rem; font-size: 1.2rem;
text-shadow: 0 0 5px var(--primary-color);
margin-bottom: 5px; margin-bottom: 5px;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
@ -156,12 +157,14 @@
background-color: rgba(50, 205, 50, 0.2); background-color: rgba(50, 205, 50, 0.2);
border: 1px solid #32CD32; border: 1px solid #32CD32;
color: #32CD32; color: #32CD32;
text-shadow: 0 0 5px rgba(50, 205, 50, 0.8);
} }
.status-badge-offline { .status-badge-offline {
background-color: rgba(255, 85, 85, 0.2); background-color: rgba(255, 85, 85, 0.2);
border: 1px solid #ff5555; border: 1px solid #ff5555;
color: #ff5555; color: #ff5555;
text-shadow: 0 0 5px rgba(255, 85, 85, 0.8);
} }
/* Stats bars */ /* Stats bars */

View File

@ -2,40 +2,11 @@
* BitcoinMinuteRefresh.js - Simplified Bitcoin-themed floating uptime monitor * BitcoinMinuteRefresh.js - Simplified Bitcoin-themed floating uptime monitor
* *
* This module creates a Bitcoin-themed terminal that shows server uptime. * This module creates a Bitcoin-themed terminal that shows server uptime.
* Now includes DeepSea theme support.
*/ */
const BitcoinMinuteRefresh = (function () { const BitcoinMinuteRefresh = (function () {
// Constants // Constants
const STORAGE_KEY = 'bitcoin_last_refresh_time'; const STORAGE_KEY = 'bitcoin_last_refresh_time'; // For cross-page sync
const BITCOIN_COLOR = '#f7931a';
const DEEPSEA_COLOR = '#0088cc';
const DOM_IDS = {
TERMINAL: 'bitcoin-terminal',
STYLES: 'bitcoin-terminal-styles',
CLOCK: 'terminal-clock',
UPTIME_HOURS: 'uptime-hours',
UPTIME_MINUTES: 'uptime-minutes',
UPTIME_SECONDS: 'uptime-seconds',
MINIMIZED_UPTIME: 'minimized-uptime-value',
SHOW_BUTTON: 'bitcoin-terminal-show'
};
const STORAGE_KEYS = {
THEME: 'useDeepSeaTheme',
COLLAPSED: 'bitcoin_terminal_collapsed',
SERVER_OFFSET: 'serverTimeOffset',
SERVER_START: 'serverStartTime',
REFRESH_EVENT: 'bitcoin_refresh_event'
};
const SELECTORS = {
HEADER: '.terminal-header',
TITLE: '.terminal-title',
TIMER: '.uptime-timer',
SEPARATORS: '.uptime-separator',
UPTIME_TITLE: '.uptime-title',
MINI_LABEL: '.mini-uptime-label',
TERMINAL_DOT: '.terminal-dot'
};
// Private variables // Private variables
let terminalElement = null; let terminalElement = null;
@ -45,141 +16,18 @@ 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 dragListenersAdded = false;
/**
* Logging helper function
* @param {string} message - Message to log
* @param {string} level - Log level (log, warn, error)
*/
function log(message, level = 'log') {
const prefix = "BitcoinMinuteRefresh: ";
if (level === 'error') {
console.error(prefix + message);
} else if (level === 'warn') {
console.warn(prefix + message);
} else {
console.log(prefix + message);
}
}
/**
* Helper function to set multiple styles on an element
* @param {Element} element - The DOM element to style
* @param {Object} styles - Object with style properties
*/
function applyStyles(element, styles) {
Object.keys(styles).forEach(key => {
element.style[key] = styles[key];
});
}
/**
* Apply the current theme color
*/
function applyThemeColor() {
// Check if theme toggle is set to DeepSea
const isDeepSeaTheme = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
currentThemeColor = isDeepSeaTheme ? DEEPSEA_COLOR : BITCOIN_COLOR;
// Don't try to update DOM elements if they don't exist yet
if (!terminalElement) return;
// Define color values based on theme
const rgbValues = isDeepSeaTheme ? '0, 136, 204' : '247, 147, 26';
// Create theme config
const themeConfig = {
color: currentThemeColor,
borderColor: currentThemeColor,
boxShadow: `0 0 5px rgba(${rgbValues}, 0.3)`,
textShadow: `0 0 5px rgba(${rgbValues}, 0.8)`,
borderColorRGBA: `rgba(${rgbValues}, 0.5)`,
textShadowStrong: `0 0 8px rgba(${rgbValues}, 0.8)`
};
// Apply styles to terminal
applyStyles(terminalElement, {
borderColor: themeConfig.color,
color: themeConfig.color,
boxShadow: themeConfig.boxShadow
});
// Update header border
const headerElement = terminalElement.querySelector(SELECTORS.HEADER);
if (headerElement) {
headerElement.style.borderColor = themeConfig.color;
}
// Update terminal title
const titleElement = terminalElement.querySelector(SELECTORS.TITLE);
if (titleElement) {
applyStyles(titleElement, {
color: themeConfig.color,
textShadow: themeConfig.textShadow
});
}
// Update uptime timer border
const uptimeTimer = terminalElement.querySelector(SELECTORS.TIMER);
if (uptimeTimer) {
uptimeTimer.style.borderColor = themeConfig.borderColorRGBA;
}
// Update uptime separators
const separators = terminalElement.querySelectorAll(SELECTORS.SEPARATORS);
separators.forEach(sep => {
sep.style.textShadow = themeConfig.textShadowStrong;
});
// Update uptime title
const uptimeTitle = terminalElement.querySelector(SELECTORS.UPTIME_TITLE);
if (uptimeTitle) {
uptimeTitle.style.textShadow = themeConfig.textShadow;
}
// Update minimized view
const miniLabel = terminalElement.querySelector(SELECTORS.MINI_LABEL);
if (miniLabel) {
miniLabel.style.color = themeConfig.color;
}
}
/**
* Listen for theme changes
*/
function setupThemeChangeListener() {
// Listen for theme change events from localStorage
window.addEventListener('storage', function (e) {
if (e.key === STORAGE_KEYS.THEME) {
applyThemeColor();
}
});
}
/**
* Debounce function to limit execution frequency
*/
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
/** /**
* Add dragging functionality to the terminal * Add dragging functionality to the terminal
*/ */
function addDraggingBehavior() { function addDraggingBehavior() {
// Find the terminal element // Find the terminal element
const terminal = document.getElementById(DOM_IDS.TERMINAL) || const terminal = document.getElementById('bitcoin-terminal') ||
document.querySelector('.bitcoin-terminal') || document.querySelector('.bitcoin-terminal') ||
document.getElementById('retro-terminal-bar'); document.getElementById('retro-terminal-bar');
if (!terminal) { if (!terminal) {
log('Terminal element not found for drag behavior', 'warn'); console.warn('Terminal element not found for drag behavior');
return; return;
} }
@ -193,7 +41,7 @@ const BitcoinMinuteRefresh = (function () {
if (window.innerWidth < 768) return; if (window.innerWidth < 768) return;
// Don't handle drag if clicking on controls // Don't handle drag if clicking on controls
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return; if (e.target.closest('.terminal-dot')) return;
isDragging = true; isDragging = true;
terminal.classList.add('dragging'); terminal.classList.add('dragging');
@ -215,8 +63,8 @@ const BitcoinMinuteRefresh = (function () {
e.preventDefault(); // Prevent text selection e.preventDefault(); // Prevent text selection
} }
// Function to handle mouse move (dragging) with debounce for better performance // Function to handle mouse move (dragging)
const handleMouseMove = debounce(function (e) { function handleMouseMove(e) {
if (!isDragging) return; if (!isDragging) return;
// Calculate the horizontal movement - vertical stays fixed // Calculate the horizontal movement - vertical stays fixed
@ -231,7 +79,7 @@ const BitcoinMinuteRefresh = (function () {
terminal.style.left = newLeft + 'px'; terminal.style.left = newLeft + 'px';
terminal.style.right = 'auto'; // Remove right positioning terminal.style.right = 'auto'; // Remove right positioning
terminal.style.transform = 'none'; // Remove transformations terminal.style.transform = 'none'; // Remove transformations
}, 10); }
// Function to handle mouse up (drag end) // Function to handle mouse up (drag end)
function handleMouseUp() { function handleMouseUp() {
@ -242,7 +90,7 @@ const BitcoinMinuteRefresh = (function () {
} }
// Find the terminal header for dragging // Find the terminal header for dragging
const terminalHeader = terminal.querySelector(SELECTORS.HEADER); const terminalHeader = terminal.querySelector('.terminal-header');
if (terminalHeader) { if (terminalHeader) {
terminalHeader.addEventListener('mousedown', handleMouseDown); terminalHeader.addEventListener('mousedown', handleMouseDown);
} else { } else {
@ -250,67 +98,10 @@ const BitcoinMinuteRefresh = (function () {
terminal.addEventListener('mousedown', handleMouseDown); terminal.addEventListener('mousedown', handleMouseDown);
} }
// Add touch support for mobile/tablet
function handleTouchStart(e) {
if (window.innerWidth < 768) return;
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
const touch = e.touches[0];
isDragging = true;
terminal.classList.add('dragging');
startX = touch.clientX;
const style = window.getComputedStyle(terminal);
if (style.left !== 'auto') {
startLeft = parseInt(style.left) || 0;
} else {
startLeft = window.innerWidth - (parseInt(style.right) || 0) - terminal.offsetWidth;
}
e.preventDefault();
}
function handleTouchMove(e) {
if (!isDragging) return;
const touch = e.touches[0];
const deltaX = touch.clientX - startX;
let newLeft = startLeft + deltaX;
const maxLeft = window.innerWidth - terminal.offsetWidth;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
terminal.style.left = newLeft + 'px';
terminal.style.right = 'auto';
terminal.style.transform = 'none';
e.preventDefault();
}
function handleTouchEnd() {
if (isDragging) {
isDragging = false;
terminal.classList.remove('dragging');
}
}
if (terminalHeader) {
terminalHeader.addEventListener('touchstart', handleTouchStart);
} else {
terminal.addEventListener('touchstart', handleTouchStart);
}
// Add event listeners only once to prevent memory leaks
if (!dragListenersAdded) {
// Add mousemove and mouseup listeners to document // Add mousemove and mouseup listeners to document
document.addEventListener('mousemove', handleMouseMove); document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp); document.addEventListener('mouseup', handleMouseUp);
// Add touch event listeners
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
// Handle window resize to keep terminal visible // Handle window resize to keep terminal visible
window.addEventListener('resize', function () { window.addEventListener('resize', function () {
if (window.innerWidth < 768) { if (window.innerWidth < 768) {
@ -328,10 +119,6 @@ const BitcoinMinuteRefresh = (function () {
} }
} }
}); });
// Mark listeners as added
dragListenersAdded = true;
}
} }
/** /**
@ -340,7 +127,7 @@ const BitcoinMinuteRefresh = (function () {
function createTerminalElement() { function createTerminalElement() {
// Container element // Container element
terminalElement = document.createElement('div'); terminalElement = document.createElement('div');
terminalElement.id = DOM_IDS.TERMINAL; terminalElement.id = 'bitcoin-terminal';
terminalElement.className = 'bitcoin-terminal'; terminalElement.className = 'bitcoin-terminal';
// Terminal content - simplified for uptime-only // Terminal content - simplified for uptime-only
@ -358,23 +145,23 @@ const BitcoinMinuteRefresh = (function () {
<div class="status-dot connected"></div> <div class="status-dot connected"></div>
<span>LIVE</span> <span>LIVE</span>
</div> </div>
<span id="${DOM_IDS.CLOCK}" class="terminal-clock">00:00:00</span> <span id="terminal-clock" class="terminal-clock">00:00:00</span>
</div> </div>
<div id="uptime-timer" class="uptime-timer"> <div id="uptime-timer" class="uptime-timer">
<div class="uptime-title">UPTIME</div> <div class="uptime-title">UPTIME</div>
<div class="uptime-display"> <div class="uptime-display">
<div class="uptime-value"> <div class="uptime-value">
<span id="${DOM_IDS.UPTIME_HOURS}" class="uptime-number">00</span> <span id="uptime-hours" class="uptime-number">00</span>
<span class="uptime-label">H</span> <span class="uptime-label">H</span>
</div> </div>
<div class="uptime-separator">:</div> <div class="uptime-separator">:</div>
<div class="uptime-value"> <div class="uptime-value">
<span id="${DOM_IDS.UPTIME_MINUTES}" class="uptime-number">00</span> <span id="uptime-minutes" class="uptime-number">00</span>
<span class="uptime-label">M</span> <span class="uptime-label">M</span>
</div> </div>
<div class="uptime-separator">:</div> <div class="uptime-separator">:</div>
<div class="uptime-value"> <div class="uptime-value">
<span id="${DOM_IDS.UPTIME_SECONDS}" class="uptime-number">00</span> <span id="uptime-seconds" class="uptime-number">00</span>
<span class="uptime-label">S</span> <span class="uptime-label">S</span>
</div> </div>
</div> </div>
@ -383,7 +170,7 @@ const BitcoinMinuteRefresh = (function () {
<div class="terminal-minimized"> <div class="terminal-minimized">
<div class="minimized-uptime"> <div class="minimized-uptime">
<span class="mini-uptime-label">UPTIME</span> <span class="mini-uptime-label">UPTIME</span>
<span id="${DOM_IDS.MINIMIZED_UPTIME}">00:00:00</span> <span id="minimized-uptime-value">00:00:00</span>
</div> </div>
<div class="minimized-status-dot connected"></div> <div class="minimized-status-dot connected"></div>
</div> </div>
@ -399,12 +186,12 @@ const BitcoinMinuteRefresh = (function () {
uptimeElement = document.getElementById('uptime-timer'); uptimeElement = document.getElementById('uptime-timer');
// Check if terminal was previously collapsed // Check if terminal was previously collapsed
if (localStorage.getItem(STORAGE_KEYS.COLLAPSED) === 'true') { if (localStorage.getItem('bitcoin_terminal_collapsed') === 'true') {
terminalElement.classList.add('collapsed'); terminalElement.classList.add('collapsed');
} }
// Add custom styles if not already present // Add custom styles if not already present
if (!document.getElementById(DOM_IDS.STYLES)) { if (!document.getElementById('bitcoin-terminal-styles')) {
addStyles(); addStyles();
} }
} }
@ -413,13 +200,8 @@ 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
const styleElement = document.createElement('style'); const styleElement = document.createElement('style');
styleElement.id = DOM_IDS.STYLES; styleElement.id = 'bitcoin-terminal-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 +210,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 #f7931a;
color: ${currentThemeColor}; color: #f7931a;
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(247, 147, 26, 0.3);
} }
/* Terminal Header */ /* Terminal Header */
@ -443,10 +225,10 @@ 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 #f7931a;
padding-bottom: 5px; padding-bottom: 5px;
margin-bottom: 8px; margin-bottom: 8px;
cursor: grab; /* Add grab cursor on hover */ cursor: pointer; /* Add pointer (hand) cursor on hover */
} }
/* Apply grabbing cursor during active drag */ /* Apply grabbing cursor during active drag */
@ -456,9 +238,10 @@ const BitcoinMinuteRefresh = (function () {
} }
.terminal-title { .terminal-title {
color: ${currentThemeColor}; color: #f7931a;
font-weight: bold; font-weight: bold;
font-size: 1.1rem; font-size: 1.1rem;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
animation: terminal-flicker 4s infinite; animation: terminal-flicker 4s infinite;
} }
@ -522,6 +305,7 @@ const BitcoinMinuteRefresh = (function () {
.terminal-clock { .terminal-clock {
font-size: 1rem; font-size: 1rem;
font-weight: bold; font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
} }
/* Uptime Display - Modern Digital Clock Style (Horizontal) */ /* Uptime Display - Modern Digital Clock Style (Horizontal) */
@ -531,7 +315,7 @@ const BitcoinMinuteRefresh = (function () {
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(247, 147, 26, 0.5);
margin-top: 5px; margin-top: 5px;
} }
@ -558,6 +342,7 @@ const BitcoinMinuteRefresh = (function () {
display: inline-block; display: inline-block;
text-align: center; text-align: center;
letter-spacing: 2px; letter-spacing: 2px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
color: #dee2e6; color: #dee2e6;
} }
@ -571,6 +356,7 @@ const BitcoinMinuteRefresh = (function () {
font-size: 1.4rem; font-size: 1.4rem;
font-weight: bold; font-weight: bold;
padding: 0 2px; padding: 0 2px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
} }
.uptime-title { .uptime-title {
@ -578,15 +364,16 @@ const BitcoinMinuteRefresh = (function () {
font-weight: bold; font-weight: bold;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 2px; letter-spacing: 2px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
margin-bottom: 3px; margin-bottom: 3px;
} }
/* Show button */ /* Show button */
#${DOM_IDS.SHOW_BUTTON} { #bitcoin-terminal-show {
position: fixed; position: fixed;
bottom: 10px; bottom: 10px;
right: 10px; right: 10px;
background-color: ${currentThemeColor}; background-color: #f7931a;
color: #000; color: #000;
border: none; border: none;
padding: 8px 12px; padding: 8px 12px;
@ -594,7 +381,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(247, 147, 26, 0.5);
} }
/* CRT scanline effect */ /* CRT scanline effect */
@ -660,12 +447,13 @@ const BitcoinMinuteRefresh = (function () {
letter-spacing: 1px; letter-spacing: 1px;
opacity: 0.7; opacity: 0.7;
margin-left: 45px; margin-left: 45px;
color: ${currentThemeColor}; color: #f7931a;
} }
#${DOM_IDS.MINIMIZED_UPTIME} { #minimized-uptime-value {
font-size: 0.9rem; font-size: 0.9rem;
font-weight: bold; font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
margin-left: 45px; margin-left: 45px;
color: #dee2e6; color: #dee2e6;
} }
@ -749,25 +537,21 @@ const BitcoinMinuteRefresh = (function () {
function updateClock() { function updateClock() {
try { try {
const now = new Date(Date.now() + (serverTimeOffset || 0)); const now = new Date(Date.now() + (serverTimeOffset || 0));
// Use the global timezone setting if available let hours = now.getHours();
const timeZone = window.dashboardTimezone || 'America/Los_Angeles'; const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
// Format the time in the configured timezone const ampm = hours >= 12 ? 'PM' : 'AM';
const timeString = now.toLocaleTimeString('en-US', { hours = hours % 12;
hour: '2-digit', hours = hours ? hours : 12; // the hour '0' should be '12'
minute: '2-digit', const timeString = `${String(hours).padStart(2, '0')}:${minutes}:${seconds} ${ampm}`;
second: '2-digit',
hour12: true,
timeZone: timeZone
});
// Update clock in normal view // Update clock in normal view
const clockElement = document.getElementById(DOM_IDS.CLOCK); const clockElement = document.getElementById('terminal-clock');
if (clockElement) { if (clockElement) {
clockElement.textContent = timeString; clockElement.textContent = timeString;
} }
} catch (e) { } catch (e) {
log("Error updating clock: " + e.message, 'error'); console.error("BitcoinMinuteRefresh: Error updating clock:", e);
} }
} }
@ -785,67 +569,40 @@ const BitcoinMinuteRefresh = (function () {
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000); const seconds = Math.floor((diff % (1000 * 60)) / 1000);
// Format numbers with leading zeros
const formattedTime = {
hours: String(hours).padStart(2, '0'),
minutes: String(minutes).padStart(2, '0'),
seconds: String(seconds).padStart(2, '0')
};
// Update the main uptime display with digital clock style // Update the main uptime display with digital clock style
const elements = { const uptimeHoursElement = document.getElementById('uptime-hours');
hours: document.getElementById(DOM_IDS.UPTIME_HOURS), const uptimeMinutesElement = document.getElementById('uptime-minutes');
minutes: document.getElementById(DOM_IDS.UPTIME_MINUTES), const uptimeSecondsElement = document.getElementById('uptime-seconds');
seconds: document.getElementById(DOM_IDS.UPTIME_SECONDS),
minimized: document.getElementById(DOM_IDS.MINIMIZED_UPTIME)
};
// Update each element if it exists if (uptimeHoursElement) {
if (elements.hours) elements.hours.textContent = formattedTime.hours; uptimeHoursElement.textContent = String(hours).padStart(2, '0');
if (elements.minutes) elements.minutes.textContent = formattedTime.minutes; }
if (elements.seconds) elements.seconds.textContent = formattedTime.seconds; if (uptimeMinutesElement) {
uptimeMinutesElement.textContent = String(minutes).padStart(2, '0');
}
if (uptimeSecondsElement) {
uptimeSecondsElement.textContent = String(seconds).padStart(2, '0');
}
// Update the minimized uptime display // Update the minimized uptime display
if (elements.minimized) { const minimizedUptimeElement = document.getElementById('minimized-uptime-value');
elements.minimized.textContent = `${formattedTime.hours}:${formattedTime.minutes}:${formattedTime.seconds}`; if (minimizedUptimeElement) {
minimizedUptimeElement.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
} }
} catch (e) { } catch (e) {
log("Error updating uptime: " + e.message, 'error'); console.error("BitcoinMinuteRefresh: Error updating uptime:", e);
} }
} }
} }
/**
* Start animation frame loop for smooth updates
*/
function startAnimationLoop() {
let lastUpdate = 0;
const updateInterval = 1000; // Update every second
function animationFrame(timestamp) {
// Only update once per second to save resources
if (timestamp - lastUpdate >= updateInterval) {
updateClock();
updateUptime();
lastUpdate = timestamp;
}
// Continue the animation loop
requestAnimationFrame(animationFrame);
}
// Start the loop
requestAnimationFrame(animationFrame);
}
/** /**
* Notify other tabs that data has been refreshed * Notify other tabs that data has been refreshed
*/ */
function notifyRefresh() { function notifyRefresh() {
const now = Date.now(); const now = Date.now();
localStorage.setItem(STORAGE_KEY, now.toString()); localStorage.setItem(STORAGE_KEY, now.toString());
localStorage.setItem(STORAGE_KEYS.REFRESH_EVENT, 'refresh-' + now); localStorage.setItem('bitcoin_refresh_event', 'refresh-' + now);
log("Notified other tabs of refresh at " + new Date(now).toISOString()); console.log("BitcoinMinuteRefresh: Notified other tabs of refresh at " + new Date(now).toISOString());
} }
/** /**
@ -855,30 +612,21 @@ 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('bitcoin-terminal')) {
createTerminalElement(); createTerminalElement();
} else { } else {
// Get references to existing elements // Get references to existing elements
terminalElement = document.getElementById(DOM_IDS.TERMINAL); terminalElement = document.getElementById('bitcoin-terminal');
uptimeElement = document.getElementById('uptime-timer'); uptimeElement = document.getElementById('uptime-timer');
// Apply theme to existing element
applyThemeColor();
} }
// Set up listener for theme changes
setupThemeChangeListener();
// Try to get stored server time information // Try to get stored server time information
try { try {
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0'); serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0'); serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
} catch (e) { } catch (e) {
log("Error reading server time from localStorage: " + e.message, 'error'); console.error("BitcoinMinuteRefresh: Error reading server time from localStorage:", e);
} }
// Clear any existing intervals // Clear any existing intervals
@ -886,8 +634,11 @@ const BitcoinMinuteRefresh = (function () {
clearInterval(uptimeInterval); clearInterval(uptimeInterval);
} }
// Use requestAnimationFrame for smoother animations // Set up interval for updating clock and uptime display
startAnimationLoop(); uptimeInterval = setInterval(function () {
updateClock();
updateUptime();
}, 1000); // Update every second is sufficient for uptime display
// Listen for storage events to sync across tabs // Listen for storage events to sync across tabs
window.removeEventListener('storage', handleStorageChange); window.removeEventListener('storage', handleStorageChange);
@ -900,15 +651,15 @@ const BitcoinMinuteRefresh = (function () {
// Mark as initialized // Mark as initialized
isInitialized = true; isInitialized = true;
log("Initialized"); console.log("BitcoinMinuteRefresh: Initialized");
} }
/** /**
* Handle storage changes for cross-tab synchronization * Handle storage changes for cross-tab synchronization
*/ */
function handleStorageChange(event) { function handleStorageChange(event) {
if (event.key === STORAGE_KEYS.REFRESH_EVENT) { if (event.key === 'bitcoin_refresh_event') {
log("Detected refresh from another tab"); console.log("BitcoinMinuteRefresh: Detected refresh from another tab");
// If another tab refreshed, consider refreshing this one too // If another tab refreshed, consider refreshing this one too
// But don't refresh if it was just refreshed recently (5 seconds) // But don't refresh if it was just refreshed recently (5 seconds)
@ -916,12 +667,12 @@ const BitcoinMinuteRefresh = (function () {
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) { if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
refreshCallback(); refreshCallback();
} }
} else if (event.key === STORAGE_KEYS.SERVER_OFFSET || event.key === STORAGE_KEYS.SERVER_START) { } else if (event.key === 'serverTimeOffset' || event.key === 'serverStartTime') {
try { try {
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0'); serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0'); serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
} catch (e) { } catch (e) {
log("Error reading updated server time: " + e.message, 'error'); console.error("BitcoinMinuteRefresh: Error reading updated server time:", e);
} }
} }
} }
@ -931,7 +682,7 @@ const BitcoinMinuteRefresh = (function () {
*/ */
function handleVisibilityChange() { function handleVisibilityChange() {
if (!document.hidden) { if (!document.hidden) {
log("Page became visible, updating"); console.log("BitcoinMinuteRefresh: Page became visible, updating");
// Update immediately when page becomes visible // Update immediately when page becomes visible
updateClock(); updateClock();
@ -955,13 +706,13 @@ const BitcoinMinuteRefresh = (function () {
serverStartTime = startTime; serverStartTime = startTime;
// Store in localStorage for cross-page sharing // Store in localStorage for cross-page sharing
localStorage.setItem(STORAGE_KEYS.SERVER_OFFSET, serverTimeOffset.toString()); localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
localStorage.setItem(STORAGE_KEYS.SERVER_START, serverStartTime.toString()); localStorage.setItem('serverStartTime', serverStartTime.toString());
// Update the uptime immediately // Update the uptime immediately
updateUptime(); updateUptime();
log("Server time updated - offset: " + serverTimeOffset + " ms"); console.log("BitcoinMinuteRefresh: Server time updated - offset:", serverTimeOffset, "ms");
} }
/** /**
@ -971,7 +722,7 @@ const BitcoinMinuteRefresh = (function () {
if (!terminalElement) return; if (!terminalElement) return;
terminalElement.classList.toggle('collapsed'); terminalElement.classList.toggle('collapsed');
localStorage.setItem(STORAGE_KEYS.COLLAPSED, terminalElement.classList.contains('collapsed')); localStorage.setItem('bitcoin_terminal_collapsed', terminalElement.classList.contains('collapsed'));
} }
/** /**
@ -983,15 +734,15 @@ const BitcoinMinuteRefresh = (function () {
terminalElement.style.display = 'none'; terminalElement.style.display = 'none';
// Create show button if it doesn't exist // Create show button if it doesn't exist
if (!document.getElementById(DOM_IDS.SHOW_BUTTON)) { if (!document.getElementById('bitcoin-terminal-show')) {
const showButton = document.createElement('button'); const showButton = document.createElement('button');
showButton.id = DOM_IDS.SHOW_BUTTON; showButton.id = 'bitcoin-terminal-show';
showButton.textContent = 'Show Monitor'; showButton.textContent = 'Show Monitor';
showButton.onclick = showTerminal; showButton.onclick = showTerminal;
document.body.appendChild(showButton); document.body.appendChild(showButton);
} }
document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block'; document.getElementById('bitcoin-terminal-show').style.display = 'block';
} }
/** /**
@ -1001,10 +752,7 @@ const BitcoinMinuteRefresh = (function () {
if (!terminalElement) return; if (!terminalElement) return;
terminalElement.style.display = 'block'; terminalElement.style.display = 'block';
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON); document.getElementById('bitcoin-terminal-show').style.display = 'none';
if (showButton) {
showButton.style.display = 'none';
}
} }
// Public API // Public API
@ -1014,12 +762,11 @@ const BitcoinMinuteRefresh = (function () {
updateServerTime: updateServerTime, updateServerTime: updateServerTime,
toggleTerminal: toggleTerminal, toggleTerminal: toggleTerminal,
hideTerminal: hideTerminal, hideTerminal: hideTerminal,
showTerminal: showTerminal, showTerminal: showTerminal
updateTheme: applyThemeColor
}; };
})(); })();
// Auto-initialize when document is ready // Auto-initialize when document is ready if a refresh function is available in the global scope
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', function () {
// Check if manualRefresh function exists in global scope // Check if manualRefresh function exists in global scope
if (typeof window.manualRefresh === 'function') { if (typeof window.manualRefresh === 'function') {
@ -1027,7 +774,4 @@ document.addEventListener('DOMContentLoaded', function () {
} else { } else {
console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually"); console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually");
} }
// Update theme based on current setting
setTimeout(() => BitcoinMinuteRefresh.updateTheme(), 100);
}); });

View File

@ -1,112 +1,15 @@
"use strict"; "use strict";
// Constants for configuration
const REFRESH_INTERVAL = 60000; // 60 seconds
const TOAST_DISPLAY_TIME = 3000; // 3 seconds
const DEFAULT_TIMEZONE = 'America/Los_Angeles';
const SATOSHIS_PER_BTC = 100000000;
const MAX_CACHE_SIZE = 20; // Number of block heights to cache
// POOL configuration
const POOL_CONFIG = {
oceanPools: ['ocean', 'oceanpool', 'oceanxyz', 'ocean.xyz'],
oceanColor: '#00ffff',
defaultUnknownColor: '#999999'
};
// Global variables // Global variables
let currentStartHeight = null; let currentStartHeight = null;
const mempoolBaseUrl = "https://mempool.guide"; // Switched from mempool.space to mempool.guide - more aligned with Ocean.xyz ethos const mempoolBaseUrl = "https://mempool.space";
let blocksCache = {}; let blocksCache = {};
let isLoading = false; let isLoading = false;
// Helper function for debouncing
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
// Helper function to validate block height
function isValidBlockHeight(height) {
height = parseInt(height);
if (isNaN(height) || height < 0) {
showToast("Please enter a valid block height");
return false;
}
return height;
}
// Helper function to add items to cache with size management
function addToCache(height, data) {
blocksCache[height] = data;
// Remove oldest entries if cache exceeds maximum size
const cacheKeys = Object.keys(blocksCache).map(Number).sort((a, b) => a - b);
if (cacheKeys.length > MAX_CACHE_SIZE) {
const keysToRemove = cacheKeys.slice(0, cacheKeys.length - MAX_CACHE_SIZE);
keysToRemove.forEach(key => delete blocksCache[key]);
}
}
// Clean up event handlers when refreshing or navigating
function cleanupEventHandlers() {
$(window).off("click.blockModal");
$(document).off("keydown.blockModal");
}
// Setup keyboard navigation for modal
function setupModalKeyboardNavigation() {
$(document).on('keydown.blockModal', function (e) {
const modal = $("#block-modal");
if (modal.css('display') === 'block') {
if (e.keyCode === 27) { // ESC key
closeModal();
}
}
});
}
// DOM ready initialization // DOM ready initialization
$(document).ready(function () { $(document).ready(function() {
console.log("Blocks page initialized"); console.log("Blocks page initialized");
// Load timezone setting early
(function loadTimezoneEarly() {
// First try to get from localStorage for instant access
try {
const storedTimezone = localStorage.getItem('dashboardTimezone');
if (storedTimezone) {
window.dashboardTimezone = storedTimezone;
console.log(`Using cached timezone: ${storedTimezone}`);
}
} catch (e) {
console.error("Error reading timezone from localStorage:", e);
}
// Then fetch from server to ensure we have the latest setting
fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
window.dashboardTimezone = data.timezone;
console.log(`Set timezone from server: ${data.timezone}`);
// Cache for future use
try {
localStorage.setItem('dashboardTimezone', data.timezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
}
})
.catch(error => {
console.error("Error fetching timezone:", error);
});
})();
// Initialize notification badge // Initialize notification badge
initNotificationBadge(); initNotificationBadge();
@ -114,28 +17,32 @@ $(document).ready(function () {
loadLatestBlocks(); loadLatestBlocks();
// Set up event listeners // Set up event listeners
$("#load-blocks").on("click", function () { $("#load-blocks").on("click", function() {
const height = isValidBlockHeight($("#block-height").val()); const height = $("#block-height").val();
if (height !== false) { if (height && !isNaN(height)) {
loadBlocksFromHeight(height); loadBlocksFromHeight(height);
} else {
showToast("Please enter a valid block height");
} }
}); });
$("#latest-blocks").on("click", loadLatestBlocks); $("#latest-blocks").on("click", loadLatestBlocks);
// Handle Enter key on the block height input with debouncing // Handle Enter key on the block height input
$("#block-height").on("keypress", debounce(function (e) { $("#block-height").on("keypress", function(e) {
if (e.which === 13) { if (e.which === 13) {
const height = isValidBlockHeight($(this).val()); const height = $(this).val();
if (height !== false) { if (height && !isNaN(height)) {
loadBlocksFromHeight(height); loadBlocksFromHeight(height);
} else {
showToast("Please enter a valid block height");
} }
} }
}, 300)); });
// Close the modal when clicking the X or outside the modal // Close the modal when clicking the X or outside the modal
$(".block-modal-close").on("click", closeModal); $(".block-modal-close").on("click", closeModal);
$(window).on("click.blockModal", function (event) { $(window).on("click", function(event) {
if ($(event.target).hasClass("block-modal")) { if ($(event.target).hasClass("block-modal")) {
closeModal(); closeModal();
} }
@ -146,12 +53,6 @@ $(document).ready(function () {
BitcoinMinuteRefresh.initialize(loadLatestBlocks); BitcoinMinuteRefresh.initialize(loadLatestBlocks);
console.log("BitcoinMinuteRefresh initialized with refresh function"); console.log("BitcoinMinuteRefresh initialized with refresh function");
} }
// Setup keyboard navigation for modals
setupModalKeyboardNavigation();
// Cleanup before unload
$(window).on('beforeunload', cleanupEventHandlers);
}); });
// Update unread notifications badge in navigation // Update unread notifications badge in navigation
@ -178,9 +79,8 @@ function initNotificationBadge() {
updateNotificationBadge(); updateNotificationBadge();
// Update every 60 seconds // Update every 60 seconds
setInterval(updateNotificationBadge, REFRESH_INTERVAL); setInterval(updateNotificationBadge, 60000);
} }
// Helper function to format timestamps as readable dates // Helper function to format timestamps as readable dates
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
const date = new Date(timestamp * 1000); const date = new Date(timestamp * 1000);
@ -191,8 +91,7 @@ function formatTimestamp(timestamp) {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
hour12: true, hour12: true
timeZone: window.dashboardTimezone || DEFAULT_TIMEZONE // Use global timezone setting
}; };
return date.toLocaleString('en-US', options); return date.toLocaleString('en-US', options);
} }
@ -210,40 +109,6 @@ function formatFileSize(bytes) {
else return (bytes / 1048576).toFixed(2) + " MB"; else return (bytes / 1048576).toFixed(2) + " MB";
} }
// Helper function to create common info items
function createInfoItem(label, value, valueClass = '') {
const item = $("<div>", { class: "block-info-item" });
item.append($("<div>", {
class: "block-info-label",
text: label
}));
item.append($("<div>", {
class: `block-info-value ${valueClass}`,
text: value
}));
return item;
}
// Helper function for creating detail items
function createDetailItem(label, value, valueClass = '') {
const item = $("<div>", { class: "block-detail-item" });
item.append($("<div>", {
class: "block-detail-label",
text: label
}));
item.append($("<div>", {
class: `block-detail-value ${valueClass}`,
text: value
}));
return item;
}
// Helper function to show toast messages // Helper function to show toast messages
function showToast(message) { function showToast(message) {
// Check if we already have a toast container // Check if we already have a toast container
@ -282,11 +147,11 @@ function showToast(message) {
setTimeout(() => { setTimeout(() => {
toast.css("opacity", 1); toast.css("opacity", 1);
// Hide and remove the toast after the configured time // Hide and remove the toast after 3 seconds
setTimeout(() => { setTimeout(() => {
toast.css("opacity", 0); toast.css("opacity", 0);
setTimeout(() => toast.remove(), 300); setTimeout(() => toast.remove(), 300);
}, TOAST_DISPLAY_TIME); }, 3000);
}, 100); }, 100);
} }
@ -298,10 +163,10 @@ function getPoolColor(poolName) {
// Define color mappings for common mining pools with Ocean pool featured prominently // Define color mappings for common mining pools with Ocean pool featured prominently
const poolColors = { const poolColors = {
// OCEAN pool with a distinctive bright cyan color for prominence // OCEAN pool with a distinctive bright cyan color for prominence
'ocean': POOL_CONFIG.oceanColor, 'ocean': '#00ffff', // Bright Cyan for Ocean
'oceanpool': POOL_CONFIG.oceanColor, 'oceanpool': '#00ffff', // Bright Cyan for Ocean
'oceanxyz': POOL_CONFIG.oceanColor, 'oceanxyz': '#00ffff', // Bright Cyan for Ocean
'ocean.xyz': POOL_CONFIG.oceanColor, 'ocean.xyz': '#00ffff', // Bright Cyan for Ocean
// Other common mining pools with more muted colors // Other common mining pools with more muted colors
'f2pool': '#1a9eff', // Blue 'f2pool': '#1a9eff', // Blue
@ -316,7 +181,7 @@ function getPoolColor(poolName) {
'sbicrypto': '#cc9933', // Bronze 'sbicrypto': '#cc9933', // Bronze
'mara': '#8844cc', // Violet 'mara': '#8844cc', // Violet
'ultimuspool': '#09c7be', // Teal 'ultimuspool': '#09c7be', // Teal
'unknown': POOL_CONFIG.defaultUnknownColor // Grey for unknown pools 'unknown': '#999999' // Grey for unknown pools
}; };
// Check for partial matches in pool names (for variations like "F2Pool" vs "F2pool.com") // Check for partial matches in pool names (for variations like "F2Pool" vs "F2pool.com")
@ -338,12 +203,6 @@ function getPoolColor(poolName) {
return `hsl(${hue}, 70%, 60%)`; return `hsl(${hue}, 70%, 60%)`;
} }
// Function to check if a pool is an Ocean pool
function isOceanPool(poolName) {
const normalizedName = poolName.toLowerCase();
return POOL_CONFIG.oceanPools.some(name => normalizedName.includes(name));
}
// Function to create a block card // Function to create a block card
function createBlockCard(block) { function createBlockCard(block) {
const timestamp = formatTimestamp(block.timestamp); const timestamp = formatTimestamp(block.timestamp);
@ -357,23 +216,20 @@ function createBlockCard(block) {
const poolColor = getPoolColor(poolName); const poolColor = getPoolColor(poolName);
// Check if this is an Ocean pool block for special styling // Check if this is an Ocean pool block for special styling
const isPoolOcean = isOceanPool(poolName); const isOceanPool = poolName.toLowerCase().includes('ocean');
// Calculate total fees in BTC // Calculate total fees in BTC
const totalFees = block.extras ? (block.extras.totalFees / SATOSHIS_PER_BTC).toFixed(8) : "N/A"; const totalFees = block.extras ? (block.extras.totalFees / 100000000).toFixed(8) : "N/A";
// Create the block card with accessibility attributes // Create the block card
const blockCard = $("<div>", { const blockCard = $("<div>", {
class: "block-card", class: "block-card",
"data-height": block.height, "data-height": block.height,
"data-hash": block.id, "data-hash": block.id
tabindex: "0", // Make focusable
role: "button",
"aria-label": `Block ${block.height} mined by ${poolName} on ${timestamp}`
}); });
// Apply pool color border - with special emphasis for Ocean pool // Apply pool color border - with special emphasis for Ocean pool
if (isPoolOcean) { if (isOceanPool) {
// Give Ocean pool blocks a more prominent styling // Give Ocean pool blocks a more prominent styling
blockCard.css({ blockCard.css({
"border": `2px solid ${poolColor}`, "border": `2px solid ${poolColor}`,
@ -412,6 +268,15 @@ function createBlockCard(block) {
class: "block-info" class: "block-info"
}); });
// Add transaction count with conditional coloring based on count
const txCountItem = $("<div>", {
class: "block-info-item"
});
txCountItem.append($("<div>", {
class: "block-info-label",
text: "Transactions"
}));
// Determine transaction count color based on thresholds // Determine transaction count color based on thresholds
let txCountClass = "green"; // Default for high transaction counts (2000+) let txCountClass = "green"; // Default for high transaction counts (2000+)
if (block.tx_count < 500) { if (block.tx_count < 500) {
@ -420,11 +285,25 @@ function createBlockCard(block) {
txCountClass = "yellow"; // Between 500 and 1999 transactions txCountClass = "yellow"; // Between 500 and 1999 transactions
} }
// Add transaction count using helper txCountItem.append($("<div>", {
blockInfo.append(createInfoItem("Transactions", formattedTxCount, txCountClass)); class: `block-info-value ${txCountClass}`,
text: formattedTxCount
}));
blockInfo.append(txCountItem);
// Add size using helper // Add size
blockInfo.append(createInfoItem("Size", formattedSize, "white")); const sizeItem = $("<div>", {
class: "block-info-item"
});
sizeItem.append($("<div>", {
class: "block-info-label",
text: "Size"
}));
sizeItem.append($("<div>", {
class: "block-info-value white",
text: formattedSize
}));
blockInfo.append(sizeItem);
// Add miner/pool with custom color // Add miner/pool with custom color
const minerItem = $("<div>", { const minerItem = $("<div>", {
@ -441,13 +320,13 @@ function createBlockCard(block) {
text: poolName, text: poolName,
css: { css: {
color: poolColor, color: poolColor,
textShadow: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
fontWeight: isPoolOcean ? "bold" : "normal" fontWeight: isOceanPool ? "bold" : "normal"
} }
}); });
// Add a special indicator icon for Ocean pool // Add a special indicator icon for Ocean pool
if (isPoolOcean) { if (isOceanPool) {
minerValue.prepend($("<span>", { minerValue.prepend($("<span>", {
html: "★ ", html: "★ ",
css: { color: poolColor } css: { color: poolColor }
@ -457,23 +336,27 @@ function createBlockCard(block) {
minerItem.append(minerValue); minerItem.append(minerValue);
blockInfo.append(minerItem); blockInfo.append(minerItem);
// Add Avg Fee Rate using helper // Add Avg Fee Rate
const feeRateText = block.extras && block.extras.avgFeeRate ? block.extras.avgFeeRate + " sat/vB" : "N/A"; const feesItem = $("<div>", {
blockInfo.append(createInfoItem("Avg Fee Rate", feeRateText, "yellow")); class: "block-info-item"
});
feesItem.append($("<div>", {
class: "block-info-label",
text: "Avg Fee Rate"
}));
feesItem.append($("<div>", {
class: "block-info-value yellow",
text: block.extras && block.extras.avgFeeRate ? block.extras.avgFeeRate + " sat/vB" : "N/A"
}));
blockInfo.append(feesItem);
blockCard.append(blockInfo); blockCard.append(blockInfo);
// Add event listeners for clicking and keyboard on the block card // Add event listener for clicking on the block card
blockCard.on("click", function () { blockCard.on("click", function () {
showBlockDetails(block); showBlockDetails(block);
}); });
blockCard.on("keypress", function (e) {
if (e.which === 13 || e.which === 32) { // Enter or Space key
showBlockDetails(block);
}
});
return blockCard; return blockCard;
} }
@ -507,9 +390,9 @@ function loadBlocksFromHeight(height) {
method: "GET", method: "GET",
dataType: "json", dataType: "json",
timeout: 10000, timeout: 10000,
success: function (data) { success: function(data) {
// Cache the data using helper // Cache the data
addToCache(height, data); blocksCache[height] = data;
// Display the blocks // Display the blocks
displayBlocks(data); displayBlocks(data);
@ -519,14 +402,14 @@ function loadBlocksFromHeight(height) {
updateLatestBlockStats(data[0]); updateLatestBlockStats(data[0]);
} }
}, },
error: function (xhr, status, error) { error: function(xhr, status, error) {
console.error("Error fetching blocks:", error); console.error("Error fetching blocks:", error);
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>'); $("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
// Show error toast // Show error toast
showToast("Failed to load blocks. Please try again later."); showToast("Failed to load blocks. Please try again later.");
}, },
complete: function () { complete: function() {
isLoading = false; isLoading = false;
} }
}); });
@ -551,7 +434,7 @@ function loadLatestBlocks() {
// Cache the data (use the first block's height as the key) // Cache the data (use the first block's height as the key)
if (data.length > 0) { if (data.length > 0) {
currentStartHeight = data[0].height; currentStartHeight = data[0].height;
addToCache(currentStartHeight, data); blocksCache[currentStartHeight] = data;
// Update the block height input with the latest height // Update the block height input with the latest height
$("#block-height").val(currentStartHeight); $("#block-height").val(currentStartHeight);
@ -576,22 +459,19 @@ function loadLatestBlocks() {
}).then(data => data.length > 0 ? data[0].height : null); }).then(data => data.length > 0 ? data[0].height : null);
} }
// Refresh blocks page every 60 seconds if there are new blocks - with smart refresh // Refresh blocks page every 60 seconds if there are new blocks
setInterval(function () { setInterval(function () {
console.log("Checking for new blocks at " + new Date().toLocaleTimeString()); console.log("Checking for new blocks at " + new Date().toLocaleTimeString());
loadLatestBlocks().then(latestHeight => { loadLatestBlocks().then(latestHeight => {
if (latestHeight && latestHeight > currentStartHeight) { if (latestHeight && latestHeight > currentStartHeight) {
console.log("New blocks detected, loading latest blocks"); console.log("New blocks detected, refreshing the page");
// Instead of reloading the page, just load the latest blocks location.reload();
currentStartHeight = latestHeight;
loadLatestBlocks();
// Show a notification
showToast("New blocks detected! View updated.");
} else { } else {
console.log("No new blocks detected"); console.log("No new blocks detected");
} }
}); });
}, REFRESH_INTERVAL); }, 60000);
// Function to update the latest block stats section // Function to update the latest block stats section
function updateLatestBlockStats(block) { function updateLatestBlockStats(block) {
@ -606,7 +486,7 @@ function updateLatestBlockStats(block) {
// Pool info with color coding // Pool info with color coding
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown"; const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
const poolColor = getPoolColor(poolName); const poolColor = getPoolColor(poolName);
const isPoolOcean = isOceanPool(poolName); const isOceanPool = poolName.toLowerCase().includes('ocean');
// Clear previous content of the pool span // Clear previous content of the pool span
const poolSpan = $("#latest-pool"); const poolSpan = $("#latest-pool");
@ -617,13 +497,13 @@ function updateLatestBlockStats(block) {
text: poolName, text: poolName,
css: { css: {
color: poolColor, color: poolColor,
textShadow: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
fontWeight: isPoolOcean ? "bold" : "normal" fontWeight: isOceanPool ? "bold" : "normal"
} }
}); });
// Add star icon for Ocean pool // Add star icon for Ocean pool
if (isPoolOcean) { if (isOceanPool) {
poolElement.prepend($("<span>", { poolElement.prepend($("<span>", {
html: "★ ", html: "★ ",
css: { color: poolColor } css: { color: poolColor }
@ -635,7 +515,7 @@ function updateLatestBlockStats(block) {
// If this is the latest block from Ocean pool, add a subtle highlight to the stats card // If this is the latest block from Ocean pool, add a subtle highlight to the stats card
const statsCard = $(".latest-block-stats").closest(".card"); const statsCard = $(".latest-block-stats").closest(".card");
if (isPoolOcean) { if (isOceanPool) {
statsCard.css({ statsCard.css({
"border": `2px solid ${poolColor}`, "border": `2px solid ${poolColor}`,
"box-shadow": `0 0 10px ${poolColor}`, "box-shadow": `0 0 10px ${poolColor}`,
@ -670,18 +550,12 @@ function displayBlocks(blocks) {
return; return;
} }
// Use document fragment for batch DOM operations
const fragment = document.createDocumentFragment();
// Create a card for each block // Create a card for each block
blocks.forEach(function (block) { blocks.forEach(function(block) {
const blockCard = createBlockCard(block); const blockCard = createBlockCard(block);
fragment.appendChild(blockCard[0]); blocksGrid.append(blockCard);
}); });
// Add all cards at once
blocksGrid.append(fragment);
// Add navigation controls if needed // Add navigation controls if needed
addNavigationControls(blocks); addNavigationControls(blocks);
} }
@ -701,11 +575,10 @@ function addNavigationControls(blocks) {
if (firstBlockHeight !== currentStartHeight) { if (firstBlockHeight !== currentStartHeight) {
const newerButton = $("<button>", { const newerButton = $("<button>", {
class: "block-button", class: "block-button",
text: "Newer Blocks", text: "Newer Blocks"
"aria-label": "Load newer blocks"
}); });
newerButton.on("click", function () { newerButton.on("click", function() {
loadBlocksFromHeight(firstBlockHeight + 15); loadBlocksFromHeight(firstBlockHeight + 15);
}); });
@ -715,11 +588,10 @@ function addNavigationControls(blocks) {
// Older blocks button // Older blocks button
const olderButton = $("<button>", { const olderButton = $("<button>", {
class: "block-button", class: "block-button",
text: "Older Blocks", text: "Older Blocks"
"aria-label": "Load older blocks"
}); });
olderButton.on("click", function () { olderButton.on("click", function() {
loadBlocksFromHeight(lastBlockHeight - 1); loadBlocksFromHeight(lastBlockHeight - 1);
}); });
@ -734,12 +606,6 @@ function showBlockDetails(block) {
const modal = $("#block-modal"); const modal = $("#block-modal");
const blockDetails = $("#block-details"); const blockDetails = $("#block-details");
// Clean up previous handlers
cleanupEventHandlers();
// Re-add scoped handlers
setupModalKeyboardNavigation();
// Clear the details // Clear the details
blockDetails.empty(); blockDetails.empty();
@ -770,7 +636,7 @@ function showBlockDetails(block) {
})); }));
headerSection.append(hashItem); headerSection.append(hashItem);
// Add mempool.guide link // Add mempool.space link
const linkItem = $("<div>", { const linkItem = $("<div>", {
class: "block-detail-item" class: "block-detail-item"
}); });
@ -783,8 +649,7 @@ function showBlockDetails(block) {
href: `${mempoolBaseUrl}/block/${block.id}`, href: `${mempoolBaseUrl}/block/${block.id}`,
target: "_blank", target: "_blank",
class: "mempool-link", class: "mempool-link",
text: "View on mempool.guide", text: "View on mempool.space",
"aria-label": `View block ${block.height} on mempool.guide (opens in new window)`,
css: { css: {
color: "#f7931a", color: "#f7931a",
textDecoration: "none" textDecoration: "none"
@ -807,8 +672,19 @@ function showBlockDetails(block) {
headerSection.append(linkItem); headerSection.append(linkItem);
// Add timestamp using helper // Add timestamp
headerSection.append(createDetailItem("Timestamp", timestamp)); const timeItem = $("<div>", {
class: "block-detail-item"
});
timeItem.append($("<div>", {
class: "block-detail-label",
text: "Timestamp"
}));
timeItem.append($("<div>", {
class: "block-detail-value",
text: timestamp
}));
headerSection.append(timeItem);
// Add merkle root // Add merkle root
const merkleItem = $("<div>", { const merkleItem = $("<div>", {
@ -860,7 +736,7 @@ function showBlockDetails(block) {
})); }));
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown"; const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
const poolColor = getPoolColor(poolName); const poolColor = getPoolColor(poolName);
const isPoolOcean = isOceanPool(poolName); const isOceanPool = poolName.toLowerCase().includes('ocean');
// Apply special styling for Ocean pool in the modal // Apply special styling for Ocean pool in the modal
const minerValue = $("<div>", { const minerValue = $("<div>", {
@ -868,13 +744,13 @@ function showBlockDetails(block) {
text: poolName, text: poolName,
css: { css: {
color: poolColor, color: poolColor,
textShadow: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`, textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
fontWeight: isPoolOcean ? "bold" : "normal" fontWeight: isOceanPool ? "bold" : "normal"
} }
}); });
// Add a special indicator icon for Ocean pool // Add a special indicator icon for Ocean pool
if (isPoolOcean) { if (isOceanPool) {
minerValue.prepend($("<span>", { minerValue.prepend($("<span>", {
html: "★ ", html: "★ ",
css: { color: poolColor } css: { color: poolColor }
@ -894,26 +770,62 @@ function showBlockDetails(block) {
minerItem.append(minerValue); minerItem.append(minerValue);
miningSection.append(minerItem); miningSection.append(minerItem);
// Add difficulty with helper // Rest of the function remains unchanged
miningSection.append(createDetailItem( // Add difficulty
"Difficulty", const difficultyItem = $("<div>", {
numberWithCommas(Math.round(block.difficulty)) class: "block-detail-item"
)); });
difficultyItem.append($("<div>", {
class: "block-detail-label",
text: "Difficulty"
}));
difficultyItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(Math.round(block.difficulty))
}));
miningSection.append(difficultyItem);
// Add nonce with helper // Add nonce
miningSection.append(createDetailItem( const nonceItem = $("<div>", {
"Nonce", class: "block-detail-item"
numberWithCommas(block.nonce) });
)); nonceItem.append($("<div>", {
class: "block-detail-label",
text: "Nonce"
}));
nonceItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.nonce)
}));
miningSection.append(nonceItem);
// Add bits with helper // Add bits
miningSection.append(createDetailItem("Bits", block.bits)); const bitsItem = $("<div>", {
class: "block-detail-item"
});
bitsItem.append($("<div>", {
class: "block-detail-label",
text: "Bits"
}));
bitsItem.append($("<div>", {
class: "block-detail-value",
text: block.bits
}));
miningSection.append(bitsItem);
// Add version with helper // Add version
miningSection.append(createDetailItem( const versionItem = $("<div>", {
"Version", class: "block-detail-item"
"0x" + block.version.toString(16) });
)); versionItem.append($("<div>", {
class: "block-detail-label",
text: "Version"
}));
versionItem.append($("<div>", {
class: "block-detail-value",
text: "0x" + block.version.toString(16)
}));
miningSection.append(versionItem);
blockDetails.append(miningSection); blockDetails.append(miningSection);
@ -927,23 +839,47 @@ function showBlockDetails(block) {
text: "Transaction Details" text: "Transaction Details"
})); }));
// Add transaction count with helper // Add transaction count
txSection.append(createDetailItem( const txCountItem = $("<div>", {
"Transaction Count", class: "block-detail-item"
numberWithCommas(block.tx_count) });
)); txCountItem.append($("<div>", {
class: "block-detail-label",
text: "Transaction Count"
}));
txCountItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.tx_count)
}));
txSection.append(txCountItem);
// Add size with helper // Add size
txSection.append(createDetailItem( const sizeItem = $("<div>", {
"Size", class: "block-detail-item"
formatFileSize(block.size) });
)); sizeItem.append($("<div>", {
class: "block-detail-label",
text: "Size"
}));
sizeItem.append($("<div>", {
class: "block-detail-value",
text: formatFileSize(block.size)
}));
txSection.append(sizeItem);
// Add weight with helper // Add weight
txSection.append(createDetailItem( const weightItem = $("<div>", {
"Weight", class: "block-detail-item"
numberWithCommas(block.weight) + " WU" });
)); weightItem.append($("<div>", {
class: "block-detail-label",
text: "Weight"
}));
weightItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.weight) + " WU"
}));
txSection.append(weightItem);
blockDetails.append(txSection); blockDetails.append(txSection);
@ -958,31 +894,77 @@ function showBlockDetails(block) {
text: "Fee Details" text: "Fee Details"
})); }));
// Add total fees with helper // Add total fees
const totalFees = (block.extras.totalFees / SATOSHIS_PER_BTC).toFixed(8); const totalFeesItem = $("<div>", {
feeSection.append(createDetailItem("Total Fees", totalFees + " BTC")); class: "block-detail-item"
});
totalFeesItem.append($("<div>", {
class: "block-detail-label",
text: "Total Fees"
}));
const totalFees = (block.extras.totalFees / 100000000).toFixed(8);
totalFeesItem.append($("<div>", {
class: "block-detail-value",
text: totalFees + " BTC"
}));
feeSection.append(totalFeesItem);
// Add reward with helper // Add reward
const reward = (block.extras.reward / SATOSHIS_PER_BTC).toFixed(8); const rewardItem = $("<div>", {
feeSection.append(createDetailItem("Block Reward", reward + " BTC")); class: "block-detail-item"
});
rewardItem.append($("<div>", {
class: "block-detail-label",
text: "Block Reward"
}));
const reward = (block.extras.reward / 100000000).toFixed(8);
rewardItem.append($("<div>", {
class: "block-detail-value",
text: reward + " BTC"
}));
feeSection.append(rewardItem);
// Add median fee with helper // Add median fee
feeSection.append(createDetailItem( const medianFeeItem = $("<div>", {
"Median Fee Rate", class: "block-detail-item"
block.extras.medianFee + " sat/vB" });
)); medianFeeItem.append($("<div>", {
class: "block-detail-label",
text: "Median Fee Rate"
}));
medianFeeItem.append($("<div>", {
class: "block-detail-value",
text: block.extras.medianFee + " sat/vB"
}));
feeSection.append(medianFeeItem);
// Add average fee with helper // Add average fee
feeSection.append(createDetailItem( const avgFeeItem = $("<div>", {
"Average Fee", class: "block-detail-item"
numberWithCommas(block.extras.avgFee) + " sat" });
)); avgFeeItem.append($("<div>", {
class: "block-detail-label",
text: "Average Fee"
}));
avgFeeItem.append($("<div>", {
class: "block-detail-value",
text: numberWithCommas(block.extras.avgFee) + " sat"
}));
feeSection.append(avgFeeItem);
// Add average fee rate with helper // Add average fee rate
feeSection.append(createDetailItem( const avgFeeRateItem = $("<div>", {
"Average Fee Rate", class: "block-detail-item"
block.extras.avgFeeRate + " sat/vB" });
)); avgFeeRateItem.append($("<div>", {
class: "block-detail-label",
text: "Average Fee Rate"
}));
avgFeeRateItem.append($("<div>", {
class: "block-detail-value",
text: block.extras.avgFeeRate + " sat/vB"
}));
feeSection.append(avgFeeRateItem);
// Add fee range with visual representation // Add fee range with visual representation
if (block.extras.feeRange && block.extras.feeRange.length > 0) { if (block.extras.feeRange && block.extras.feeRange.length > 0) {
@ -1004,8 +986,7 @@ function showBlockDetails(block) {
// Add visual fee bar // Add visual fee bar
const feeBarContainer = $("<div>", { const feeBarContainer = $("<div>", {
class: "fee-bar-container", class: "fee-bar-container"
"aria-label": "Fee rate range visualization"
}); });
const feeBar = $("<div>", { const feeBar = $("<div>", {
@ -1026,15 +1007,11 @@ function showBlockDetails(block) {
blockDetails.append(feeSection); blockDetails.append(feeSection);
} }
// Show the modal with aria attributes // Show the modal
modal.attr("aria-hidden", "false");
modal.css("display", "block"); modal.css("display", "block");
} }
// Function to close the modal // Function to close the modal
function closeModal() { function closeModal() {
const modal = $("#block-modal"); $("#block-modal").css("display", "none");
modal.css("display", "none");
modal.attr("aria-hidden", "true");
cleanupEventHandlers();
} }

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,10 @@ const pageSize = 20;
let hasMoreNotifications = true; let hasMoreNotifications = true;
let isLoading = false; let isLoading = false;
// Timezone configuration
let dashboardTimezone = 'America/Los_Angeles'; // Default
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
// Initialize when document is ready // Initialize when document is ready
$(document).ready(() => { $(document).ready(() => {
console.log("Notification page initializing..."); console.log("Notification page initializing...");
// Fetch timezone configuration
fetchTimezoneConfig();
// Set up filter buttons // Set up filter buttons
$('.filter-button').click(function () { $('.filter-button').click(function () {
$('.filter-button').removeClass('active'); $('.filter-button').removeClass('active');
@ -48,34 +41,6 @@ $(document).ready(() => {
setInterval(updateNotificationTimestamps, 30000); setInterval(updateNotificationTimestamps, 30000);
}); });
// Fetch timezone configuration from server
function fetchTimezoneConfig() {
return fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
dashboardTimezone = data.timezone;
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
console.log(`Notifications page using timezone: ${dashboardTimezone}`);
// Store in localStorage for future use
try {
localStorage.setItem('dashboardTimezone', dashboardTimezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
// Update all timestamps with the new timezone
updateNotificationTimestamps();
return dashboardTimezone;
}
})
.catch(error => {
console.error('Error fetching timezone config:', error);
return null;
});
}
// Load notifications with current filter // Load notifications with current filter
function loadNotifications() { function loadNotifications() {
if (isLoading) return; if (isLoading) return;
@ -139,36 +104,14 @@ function refreshNotifications() {
} }
} }
// This refreshes all timestamps on the page periodically // Update notification timestamps to relative time
function updateNotificationTimestamps() { function updateNotificationTimestamps() {
$('.notification-item').each(function () { $('.notification-item').each(function () {
const timestampStr = $(this).attr('data-timestamp'); const timestampStr = $(this).attr('data-timestamp');
if (timestampStr) { if (timestampStr) {
try {
const timestamp = new Date(timestampStr); const timestamp = new Date(timestampStr);
const relativeTime = formatTimestamp(timestamp);
// Update relative time $(this).find('.notification-time').text(relativeTime);
$(this).find('.notification-time').text(formatTimestamp(timestamp));
// Update full timestamp with configured timezone
if ($(this).find('.full-timestamp').length) {
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
};
const fullTimestamp = timestamp.toLocaleString('en-US', options);
$(this).find('.full-timestamp').text(fullTimestamp);
}
} catch (e) {
console.error("Error updating timestamp:", e, timestampStr);
}
} }
}); });
} }
@ -247,28 +190,14 @@ function createNotificationElement(notification) {
iconElement.addClass('fa-bell'); iconElement.addClass('fa-bell');
} }
// Important: Do not append "Z" here, as that can cause timezone issues // Append "Z" to indicate UTC if not present
// Create a date object from the notification timestamp let utcTimestampStr = notification.timestamp;
let notificationDate; if (!utcTimestampStr.endsWith('Z')) {
try { utcTimestampStr += 'Z';
// Parse the timestamp directly without modifications
notificationDate = new Date(notification.timestamp);
// Validate the date object - if invalid, try alternative approach
if (isNaN(notificationDate.getTime())) {
console.warn("Invalid date from notification timestamp, trying alternative format");
// Try adding Z to make it explicit UTC if not already ISO format
if (!notification.timestamp.endsWith('Z') && !notification.timestamp.includes('+')) {
notificationDate = new Date(notification.timestamp + 'Z');
}
}
} catch (e) {
console.error("Error parsing notification date:", e);
notificationDate = new Date(); // Fallback to current date
} }
const utcDate = new Date(utcTimestampStr);
// Format the timestamp using the configured timezone // Convert UTC date to Los Angeles time with a timezone name for clarity
const options = { const options = {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
@ -276,25 +205,16 @@ function createNotificationElement(notification) {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
hour12: true, hour12: true
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
}; };
const fullTimestamp = utcDate.toLocaleString('en-US', options);
// Format full timestamp with configured timezone // Append the full timestamp to the notification message
let fullTimestamp;
try {
fullTimestamp = notificationDate.toLocaleString('en-US', options);
} catch (e) {
console.error("Error formatting timestamp with timezone:", e);
fullTimestamp = notificationDate.toLocaleString('en-US'); // Fallback without timezone
}
// Append the message and formatted timestamp
const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`; const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`;
element.find('.notification-message').html(messageWithTimestamp); element.find('.notification-message').html(messageWithTimestamp);
// Set metadata for relative time display // Set metadata for relative time display
element.find('.notification-time').text(formatTimestamp(notificationDate)); element.find('.notification-time').text(formatTimestamp(utcDate));
element.find('.notification-category').text(notification.category); element.find('.notification-category').text(notification.category);
// Set up action buttons // Set up action buttons
@ -315,21 +235,10 @@ function createNotificationElement(notification) {
return element; return element;
} }
// Format timestamp as relative time
function formatTimestamp(timestamp) { function formatTimestamp(timestamp) {
// Ensure we have a valid date object
let dateObj = timestamp;
if (!(timestamp instanceof Date) || isNaN(timestamp.getTime())) {
try {
dateObj = new Date(timestamp);
} catch (e) {
console.error("Invalid timestamp in formatTimestamp:", e);
return "unknown time";
}
}
// Calculate time difference in local timezone context
const now = new Date(); const now = new Date();
const diffMs = now - dateObj; const diffMs = now - timestamp;
const diffSec = Math.floor(diffMs / 1000); const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60); const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60); const diffHour = Math.floor(diffMin / 60);
@ -344,14 +253,17 @@ function formatTimestamp(timestamp) {
} else if (diffDay < 30) { } else if (diffDay < 30) {
return `${diffDay}d ago`; return `${diffDay}d ago`;
} else { } else {
// Format as date for older notifications using configured timezone // Format as date for older notifications
const options = { const options = {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',
timeZone: window.dashboardTimezone || 'America/Los_Angeles' hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
}; };
return dateObj.toLocaleDateString('en-US', options); return timestamp.toLocaleDateString('en-US', options);
} }
} }

View File

@ -1,440 +0,0 @@
// Add this flag at the top of your file, outside the function
let isApplyingTheme = false;
// Bitcoin Orange theme (default)
const BITCOIN_THEME = {
PRIMARY: '#f2a900',
PRIMARY_RGB: '242, 169, 0',
SHARED: {
GREEN: '#32CD32',
RED: '#ff5555',
YELLOW: '#ffd700'
},
CHART: {
GRADIENT_START: '#f2a900',
GRADIENT_END: 'rgba(242, 169, 0, 0.2)',
ANNOTATION: '#ffd700'
}
};
// DeepSea theme (blue alternative)
const DEEPSEA_THEME = {
PRIMARY: '#0088cc',
PRIMARY_RGB: '0, 136, 204',
SHARED: {
GREEN: '#32CD32',
RED: '#ff5555',
YELLOW: '#ffd700'
},
CHART: {
GRADIENT_START: '#0088cc',
GRADIENT_END: 'rgba(0, 136, 204, 0.2)',
ANNOTATION: '#00b3ff'
}
};
// Global theme constants
const THEME = {
BITCOIN: BITCOIN_THEME,
DEEPSEA: DEEPSEA_THEME,
SHARED: BITCOIN_THEME.SHARED
};
// Function to get the current theme based on localStorage setting
function getCurrentTheme() {
const useDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
return useDeepSea ? DEEPSEA_THEME : BITCOIN_THEME;
}
// Make globals available
window.THEME = THEME;
window.getCurrentTheme = getCurrentTheme;
// Use window-scoped variable to prevent conflicts
window.themeProcessing = false;
// Fixed applyDeepSeaTheme function with recursion protection
function applyDeepSeaTheme() {
// Check if we're already applying the theme to prevent recursion
if (window.themeProcessing) {
console.log("Theme application already in progress, avoiding recursion");
return;
}
// Set the guard flag
isApplyingTheme = true;
try {
console.log("Applying DeepSea theme...");
// Create or update CSS variables for the DeepSea theme
const styleElement = document.createElement('style');
styleElement.id = 'deepSeaThemeStyles'; // Give it an ID so we can check if it exists
// Enhanced CSS with clean, organized structure
styleElement.textContent = `
/* Base theme variables */
:root {
--primary-color: #0088cc;
--primary-color-rgb: 0, 136, 204;
--accent-color: #00b3ff;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #131b20);
}
/* Card styling */
.card {
border: 1px solid var(--primary-color) !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.3) !important;
}
.card-header, .card > .card-header {
background: linear-gradient(to right, var(--primary-color), #006699) !important;
border-bottom: 1px solid var(--primary-color) !important;
color: #fff !important;
}
/* Navigation */
.nav-link {
border: 1px solid var(--primary-color) !important;
color: var(--primary-color) !important;
}
.nav-link:hover, .nav-link.active {
background-color: var(--primary-color) !important;
color: #fff !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
}
/* Interface elements */
#terminal-cursor {
background-color: var(--primary-color) !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.8) !important;
}
#lastUpdated {
color: var(--primary-color) !important;
}
h1, .text-center h1 {
color: var(--primary-color) !important;
}
.nav-badge {
background-color: var(--primary-color) !important;
}
/* Bitcoin progress elements */
.bitcoin-progress-inner {
background: linear-gradient(90deg, var(--primary-color), var(--accent-color)) !important;
}
.bitcoin-progress-container {
border: 1px solid var(--primary-color) !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
}
/* Theme toggle button styling */
#themeToggle, button.theme-toggle, .toggle-theme-btn {
background: transparent !important;
border: 1px solid var(--primary-color) !important;
color: var(--primary-color) !important;
transition: all 0.3s ease !important;
}
#themeToggle:hover, button.theme-toggle:hover, .toggle-theme-btn:hover {
background-color: rgba(var(--primary-color-rgb), 0.1) !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.3) !important;
}
/* ===== SPECIAL CASE FIXES ===== */
/* Pool hashrate - always white */
[id^="pool_"] {
color: #ffffff !important;
}
/* Block page elements */
.stat-item strong,
.block-height,
.block-detail-title {
color: var(--primary-color) !important;
}
/* Block inputs and button styles */
.block-input:focus {
outline: none !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
}
.block-button:hover {
background-color: var(--primary-color) !important;
color: #000 !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
}
/* Notification page elements */
.filter-button.active {
background-color: var(--primary-color) !important;
color: #000 !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
}
.filter-button:hover,
.action-button:hover:not(.danger),
.load-more-button:hover {
background-color: rgba(var(--primary-color-rgb), 0.2) !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.3) !important;
}
/* Block cards and modals */
.block-card:hover {
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
transform: translateY(-2px);
}
.block-modal-content {
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.5) !important;
}
.block-modal-close:hover,
.block-modal-close:focus {
color: var(--accent-color) !important;
}
/* ===== COLOR CATEGORIES ===== */
/* YELLOW - SATOSHI EARNINGS & BTC PRICE */
[id$="_sats"],
#btc_price,
.metric-value[id$="_sats"],
.est_time_to_payout:not(.green):not(.red) {
color: #ffd700 !important;
}
/* GREEN - POSITIVE USD VALUES */
.metric-value.green,
span.green,
#daily_revenue:not([style*="color: #ff"]),
#monthly_profit_usd:not([style*="color: #ff"]),
#daily_profit_usd:not([style*="color: #ff"]),
.status-green,
#pool_luck.very-lucky,
#pool_luck.lucky {
color: #32CD32 !important;
}
.online-dot {
background: #32CD32 !important;
box-shadow: 0 0 10px #32CD32, 0 0 10px #32CD32 !important;
}
/* Light green for "lucky" status */
#pool_luck.lucky {
color: #90EE90 !important;
}
/* NORMAL LUCK - KHAKI */
#pool_luck.normal-luck {
color: #F0E68C !important;
}
/* RED - NEGATIVE VALUES & WARNINGS */
.metric-value.red,
span.red,
.status-red,
#daily_power_cost,
#pool_luck.unlucky {
color: #ff5555 !important;
}
.offline-dot {
background: #ff5555 !important;
box-shadow: 0 0 10px #ff5555, 0 0 10px #ff5555 !important;
}
/* WHITE - NETWORK STATS & WORKER DATA */
#block_number,
#difficulty,
#network_hashrate,
#pool_fees_percentage,
#workers_hashing,
#last_share,
#blocks_found,
#last_block_height,
#hashrate_24hr,
#hashrate_3hr,
#hashrate_10min,
#hashrate_60sec {
color: #ffffff !important;
}
/* CYAN - TIME AGO IN LAST BLOCK */
#last_block_time {
color: #00ffff !important;
}
/* CONGRATULATIONS MESSAGE */
#congratsMessage {
background: var(--primary-color) !important;
box-shadow: 0 0 10px rgba(var(--primary-color-rgb), 0.7) !important;
}
/* ANIMATIONS */
@keyframes waitingPulse {
0%, 100% { box-shadow: 0 0 10px var(--primary-color), 0 0 10px var(--primary-color) !important; opacity: 0.8; }
50% { box-shadow: 0 0 10px var(--primary-color), 0 0 10px var(--primary-color) !important; opacity: 1; }
}
@keyframes glow {
0%, 100% { box-shadow: 0 0 10px var(--primary-color), 0 0 10px var(--primary-color) !important; }
50% { box-shadow: 0 0 10px var(--primary-color), 0 0 10px var(--primary-color) !important; }
}
`;
// Check if our style element already exists
const existingStyle = document.getElementById('deepSeaThemeStyles');
if (existingStyle) {
existingStyle.parentNode.removeChild(existingStyle);
}
// Add our new style element to the head
document.head.appendChild(styleElement);
// Update page title
document.title = document.title.replace("BTC-OS", "DeepSea");
document.title = document.title.replace("Bitcoin", "DeepSea");
// Update header text
const headerElement = document.querySelector('h1');
if (headerElement) {
headerElement.innerHTML = headerElement.innerHTML.replace("BTC-OS", "DeepSea");
headerElement.innerHTML = headerElement.innerHTML.replace("BITCOIN", "DEEPSEA");
}
// Update theme toggle button
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.style.borderColor = '#0088cc';
themeToggle.style.color = '#0088cc';
}
console.log("DeepSea theme applied with color adjustments");
} finally {
// Reset the guard flag when done, even if there's an error
setTimeout(() => { isApplyingTheme = false; }, 100);
}
}
// Make the function accessible globally
window.applyDeepSeaTheme = applyDeepSeaTheme;
// Toggle theme with hard page refresh
function toggleTheme() {
const useDeepSea = localStorage.getItem('useDeepSeaTheme') !== 'true';
// Save the new theme preference
saveThemePreference(useDeepSea);
// Show a themed loading message
const loadingMessage = document.createElement('div');
loadingMessage.id = 'theme-loader';
const icon = document.createElement('div');
icon.id = 'loader-icon';
icon.innerHTML = useDeepSea ? '🌊' : '₿';
const text = document.createElement('div');
text.id = 'loader-text';
text.textContent = 'Applying ' + (useDeepSea ? 'DeepSea' : 'Bitcoin') + ' Theme';
loadingMessage.appendChild(icon);
loadingMessage.appendChild(text);
// Apply immediate styling
loadingMessage.style.position = 'fixed';
loadingMessage.style.top = '0';
loadingMessage.style.left = '0';
loadingMessage.style.width = '100%';
loadingMessage.style.height = '100%';
loadingMessage.style.backgroundColor = useDeepSea ? '#0c141a' : '#111111';
loadingMessage.style.color = useDeepSea ? '#0088cc' : '#f2a900';
loadingMessage.style.display = 'flex';
loadingMessage.style.flexDirection = 'column';
loadingMessage.style.justifyContent = 'center';
loadingMessage.style.alignItems = 'center';
loadingMessage.style.zIndex = '9999';
loadingMessage.style.fontFamily = "'VT323', monospace";
document.body.appendChild(loadingMessage);
// Short delay before refreshing
setTimeout(() => {
// Hard reload the page
window.location.reload();
}, 500);
}
// Set theme preference to localStorage
function saveThemePreference(useDeepSea) {
try {
localStorage.setItem('useDeepSeaTheme', useDeepSea);
} catch (e) {
console.error("Error saving theme preference:", e);
}
}
// Check if this is the first startup by checking for the "firstStartup" flag
function isFirstStartup() {
return localStorage.getItem('hasStartedBefore') !== 'true';
}
// Mark that the app has started before
function markAppStarted() {
try {
localStorage.setItem('hasStartedBefore', 'true');
} catch (e) {
console.error("Error marking app as started:", e);
}
}
// Initialize DeepSea as default on first startup
function initializeDefaultTheme() {
if (isFirstStartup()) {
console.log("First startup detected, setting DeepSea as default theme");
saveThemePreference(true); // Set DeepSea theme as default (true)
markAppStarted();
return true;
}
return false;
}
// Check for theme preference in localStorage
function loadThemePreference() {
try {
// Check if it's first startup - if so, set DeepSea as default
const isFirstTime = initializeDefaultTheme();
// Get theme preference from localStorage
const themePreference = localStorage.getItem('useDeepSeaTheme');
// Apply theme based on preference
if (themePreference === 'true' || isFirstTime) {
applyDeepSeaTheme();
} else {
// Make sure the toggle button is styled correctly for Bitcoin theme
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
themeToggle.style.borderColor = '#f2a900';
themeToggle.style.color = '#f2a900';
}
}
} catch (e) {
console.error("Error loading theme preference:", e);
}
}
// Apply theme on page load
document.addEventListener('DOMContentLoaded', loadThemePreference);
// For pages that load content dynamically, also check when the window loads
window.addEventListener('load', loadThemePreference);

View File

@ -92,40 +92,6 @@ $(document).ready(function () {
}); });
}); });
// Load timezone setting early
(function loadTimezoneEarly() {
// First try to get from localStorage for instant access
try {
const storedTimezone = localStorage.getItem('dashboardTimezone');
if (storedTimezone) {
window.dashboardTimezone = storedTimezone;
console.log(`Using cached timezone: ${storedTimezone}`);
}
} catch (e) {
console.error("Error reading timezone from localStorage:", e);
}
// Then fetch from server to ensure we have the latest setting
fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
window.dashboardTimezone = data.timezone;
console.log(`Set timezone from server: ${data.timezone}`);
// Cache for future use
try {
localStorage.setItem('dashboardTimezone', data.timezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
}
})
.catch(error => {
console.error("Error fetching timezone:", error);
});
})();
// Initialize page elements // Initialize page elements
function initializePage() { function initializePage() {
console.log("Initializing page elements..."); console.log("Initializing page elements...");
@ -355,40 +321,20 @@ function createWorkerCard(worker) {
</div> </div>
`); `);
// 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
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
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
}
}
card.append(` card.append(`
<div class="worker-stats"> <div class="worker-stats">
<div class="worker-stats-row"> <div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div> <div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${formattedLastShare}</div> <div class="blue-glow">${typeof worker.last_share === 'string' ? worker.last_share.split(' ')[1] || worker.last_share : 'N/A'}</div>
</div> </div>
<div class="worker-stats-row"> <div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div> <div class="worker-stats-label">Earnings:</div>
<div class="green-glow">${worker.earnings.toFixed(8)}</div> <div class="green-glow">${worker.earnings.toFixed(8)}</div>
</div> </div>
<div class="worker-stats-row">
<div class="worker-stats-label">Accept Rate:</div>
<div class="white-glow">${worker.acceptance_rate}%</div>
</div>
</div> </div>
`); `);
@ -440,6 +386,7 @@ function updateSummaryStats() {
$('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`); $('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`);
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`); $('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`);
$('#avg-acceptance-rate').text(`${(workerData.avg_acceptance_rate || 0).toFixed(2)}%`);
} }
// Initialize mini chart // Initialize mini chart
@ -527,11 +474,6 @@ function updateLastUpdated() {
try { try {
const timestamp = new Date(workerData.timestamp); const timestamp = new Date(workerData.timestamp);
// Get the configured timezone with a fallback
const configuredTimezone = window.dashboardTimezone || 'America/Los_Angeles';
// Format with the configured timezone
const options = { const options = {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
@ -539,22 +481,12 @@ function updateLastUpdated() {
hour: '2-digit', hour: '2-digit',
minute: '2-digit', minute: '2-digit',
second: '2-digit', second: '2-digit',
hour12: true, hour12: true
timeZone: configuredTimezone // Explicitly use the configured timezone
}; };
// Format the timestamp and update the DOM
const formattedTime = timestamp.toLocaleString('en-US', options);
$("#lastUpdated").html("<strong>Last Updated:</strong> " + $("#lastUpdated").html("<strong>Last Updated:</strong> " +
formattedTime + "<span id='terminal-cursor'></span>"); timestamp.toLocaleString('en-US', options) + "<span id='terminal-cursor'></span>");
console.log(`Last updated timestamp using timezone: ${configuredTimezone}`);
} catch (e) { } catch (e) {
console.error("Error formatting timestamp:", e); console.error("Error formatting timestamp:", e);
// Fallback to basic timestamp if there's an error
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
new Date().toLocaleString() + "<span id='terminal-cursor'></span>");
} }
} }

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 MINING DASHBOARD v 0.3{% 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">
@ -17,79 +17,10 @@
<!-- Common CSS --> <!-- Common CSS -->
<link rel="stylesheet" href="/static/css/common.css"> <link rel="stylesheet" href="/static/css/common.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/static/css/theme-toggle.css">
<!-- Theme JS (added to ensure consistent application of theme) -->
<script src="/static/js/theme.js"></script>
<!-- Page-specific CSS --> <!-- Page-specific CSS -->
{% block css %}{% endblock %} {% block css %}{% endblock %}
<script>
// Execute this immediately to preload theme
(function () {
const useDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
const themeClass = useDeepSea ? 'deepsea-theme' : 'bitcoin-theme';
// Apply theme class to html element
document.documentElement.classList.add(themeClass);
// Create and add loader
document.addEventListener('DOMContentLoaded', function () {
// Create loader element
const loader = document.createElement('div');
loader.id = 'theme-loader';
const icon = document.createElement('div');
icon.id = 'loader-icon';
icon.innerHTML = useDeepSea ? '🌊' : '₿';
const text = document.createElement('div');
text.id = 'loader-text';
text.textContent = 'Loading ' + (useDeepSea ? 'DeepSea' : 'Bitcoin') + ' Theme';
loader.appendChild(icon);
loader.appendChild(text);
document.body.appendChild(loader);
// Add fade-in effect for content once theme is loaded
setTimeout(function () {
document.body.style.visibility = 'visible';
// Fade out loader
loader.style.transition = 'opacity 0.5s ease';
loader.style.opacity = '0';
// Remove loader after fade
setTimeout(function () {
if (loader && loader.parentNode) {
loader.parentNode.removeChild(loader);
}
}, 500);
}, 300);
});
})();
</script>
</head> </head>
<body> <body>
<script>
// Add underwater effects for DeepSea theme
document.addEventListener('DOMContentLoaded', function () {
// Check if DeepSea theme is active
if (localStorage.getItem('useDeepSeaTheme') === 'true') {
// Create underwater light rays
const rays = document.createElement('div');
rays.className = 'underwater-rays';
document.body.appendChild(rays);
// Create digital noise
const noise = document.createElement('div');
noise.className = 'digital-noise';
document.body.appendChild(noise);
}
});
</script>
<div class="container-fluid"> <div class="container-fluid">
<!-- Connection status indicator --> <!-- Connection status indicator -->
<div id="connectionStatus"></div> <div id="connectionStatus"></div>
@ -103,11 +34,6 @@
<!-- Top right link --> <!-- Top right link -->
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">MADE BY @DJO₿LEEZY</a> <a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">MADE BY @DJO₿LEEZY</a>
<!-- Theme toggle button (new) -->
<button id="themeToggle" class="theme-toggle-btn">
<span>Toggle Theme</span>
</button>
{% block last_updated %} {% block last_updated %}
<p class="text-center" id="lastUpdated" style="color: #f7931a; text-transform: uppercase;"><strong>LAST UPDATED:</strong> {{ current_time }}<span id="terminal-cursor"></span></p> <p class="text-center" id="lastUpdated" style="color: #f7931a; text-transform: uppercase;"><strong>LAST UPDATED:</strong> {{ current_time }}<span id="terminal-cursor"></span></p>
{% endblock %} {% endblock %}
@ -131,11 +57,6 @@
{% block congrats_message %} {% block congrats_message %}
<div id="congratsMessage" style="display:none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; background: #f7931a; color: #000; padding: 10px; border-radius: 5px; box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);"></div> <div id="congratsMessage" style="display:none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; background: #f7931a; color: #000; padding: 10px; border-radius: 5px; box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);"></div>
{% endblock %} {% endblock %}
<!-- Footer -->
<footer class="footer text-center">
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
</footer>
</div> </div>
<!-- External JavaScript libraries --> <!-- External JavaScript libraries -->
@ -143,32 +64,6 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@1.1.0"></script> <script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@1.1.0"></script>
<!-- Theme toggle initialization -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Initialize theme toggle button based on current theme
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
// Check current theme
const isDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
// Update button style based on theme
if (isDeepSea) {
themeToggle.style.borderColor = '#0088cc';
themeToggle.style.color = '#0088cc';
} else {
themeToggle.style.borderColor = '#f2a900';
themeToggle.style.color = '#f2a900';
}
// Add click event listener
themeToggle.addEventListener('click', function () {
toggleTheme(); // This will now trigger a page refresh
});
}
});
</script>
<!-- Page-specific JavaScript --> <!-- Page-specific JavaScript -->
{% block javascript %}{% endblock %} {% block javascript %}{% endblock %}

View File

@ -1,10 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD {% endblock %} {% block title %}BLOCKS - BTC-OS MINING DASHBOARD v 0.3{% endblock %}
{% block css %} {% block css %}
<link rel="stylesheet" href="/static/css/blocks.css"> <link rel="stylesheet" href="/static/css/blocks.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %} {% endblock %}
{% block header %}BLOCKCHAIN MONITOR v 0.1{% endblock %} {% block header %}BLOCKCHAIN MONITOR v 0.1{% endblock %}
@ -64,7 +63,7 @@
<div id="blocks-grid" class="blocks-grid"> <div id="blocks-grid" class="blocks-grid">
<!-- Blocks will be generated here via JavaScript --> <!-- Blocks will be generated here via JavaScript -->
<div class="loader"> <div class="loader">
<span class="loader-text">Connecting to mempool.guide API<span class="terminal-cursor"></span></span> <span class="loader-text">Connecting to mempool.space API<span class="terminal-cursor"></span></span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -6,32 +6,170 @@
<title>Ocean.xyz Pool Miner - Initializing...</title> <title>Ocean.xyz Pool Miner - Initializing...</title>
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/boot.css"> <link rel="stylesheet" href="/static/css/boot.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css"> <style>
<!-- Add Theme JS --> /* Added styles for configuration form */
<script src="/static/js/theme.js"></script> #config-form {
display: none;
margin-top: 20px;
background-color: rgba(0, 0, 0, 0.7);
border: 1px solid #f7931a;
padding: 15px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
.config-title {
color: #f7931a;
font-size: 22px;
text-align: center;
margin-bottom: 15px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #f7931a;
}
.form-group input {
width: 100%;
background-color: #111;
border: 1px solid #f7931a;
padding: 8px;
color: white;
font-family: 'VT323', monospace;
font-size: 18px;
}
.form-group input:focus {
outline: none;
box-shadow: 0 0 5px #f7931a;
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn {
background-color: #f7931a;
border: none;
color: black;
padding: 8px 15px;
font-family: 'VT323', monospace;
font-size: 18px;
cursor: pointer;
min-width: 120px;
text-align: center;
}
.btn:hover {
background-color: #ffa642;
}
.btn-secondary {
background-color: #333;
color: #f7931a;
}
.btn-secondary:hover {
background-color: #444;
}
/* Make skip button more mobile-friendly */
#skip-button {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
padding: 12px 20px;
font-size: 18px;
border-radius: 8px;
}
@media (max-width: 768px) {
#skip-button {
bottom: 10px;
right: 10px;
padding: 15px 25px;
font-size: 20px; /* Larger font size for better tap targets */
border-radius: 10px;
width: auto;
}
.form-actions {
flex-direction: column;
gap: 10px;
}
.btn {
width: 100%;
padding: 12px;
font-size: 20px;
}
}
/* Tooltip styles */
.tooltip {
position: relative;
display: inline-block;
margin-left: 5px;
cursor: help;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: #000;
color: #fff;
text-align: center;
border: 1px solid #f7931a;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
font-size: 14px;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Form success/error message */
#form-message {
margin-top: 10px;
padding: 8px;
text-align: center;
display: none;
}
.message-success {
background-color: rgba(50, 205, 50, 0.2);
border: 1px solid #32CD32;
color: #32CD32;
}
.message-error {
background-color: rgba(255, 0, 0, 0.2);
border: 1px solid #ff0000;
color: #ff0000;
}
</style>
</head> </head>
<body> <body>
<script>
// Add underwater effects for DeepSea theme
document.addEventListener('DOMContentLoaded', function () {
// Check if DeepSea theme is active
if (localStorage.getItem('useDeepSeaTheme') === 'true') {
// Create underwater light rays
const rays = document.createElement('div');
rays.className = 'underwater-rays';
document.body.appendChild(rays);
// Create digital noise
const noise = document.createElement('div');
noise.className = 'digital-noise';
document.body.appendChild(noise);
}
});
</script>
<!-- Theme toggle button (new) -->
<button id="themeToggle" class="theme-toggle-btn">
<span>Toggle Theme</span>
</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>
@ -42,7 +180,7 @@
██╔══██╗ ██║ ██║ ██║ ██║╚════██║ ██╔══██╗ ██║ ██║ ██║ ██║╚════██║
██████╔╝ ██║ ╚██████╗ ╚██████╔╝███████║ ██████╔╝ ██║ ╚██████╗ ╚██████╔╝███████║
╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
v.21 v.21
</div> </div>
<div id="terminal"> <div id="terminal">
<div id="terminal-content"> <div id="terminal-content">
@ -90,40 +228,6 @@ v.21
</label> </label>
<input type="number" id="power-usage" step="50" min="0" placeholder="13450" value=""> <input type="number" id="power-usage" step="50" min="0" placeholder="13450" value="">
</div> </div>
<div class="form-group">
<label for="network-fee">
Network Fee (%)
<span class="tooltip">
?
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
</span>
</label>
<input type="number" id="network-fee" step="0.1" min="0" max="10" placeholder="0.0" value="">
</div>
<div class="form-group">
<label for="timezone">
Timezone
<span class="tooltip">
?
<span class="tooltip-text">Your local timezone for displaying time information</span>
</span>
</label>
<select id="timezone" class="form-control">
<optgroup label="Common Timezones">
<option value="America/Los_Angeles">Los Angeles (Pacific Time)</option>
<option value="America/Denver">Denver (Mountain Time)</option>
<option value="America/Chicago">Chicago (Central Time)</option>
<option value="America/New_York">New York (Eastern Time)</option>
<option value="Europe/London">London (GMT/BST)</option>
<option value="Europe/Paris">Paris (Central European Time)</option>
<option value="Asia/Tokyo">Tokyo (Japan Standard Time)</option>
<option value="Australia/Sydney">Sydney (Australian Eastern Time)</option>
</optgroup>
<optgroup label="Other Timezones" id="other-timezones">
<!-- Will be populated by JavaScript -->
</optgroup>
</select>
</div>
<div id="form-message"></div> <div id="form-message"></div>
<div class="form-actions"> <div class="form-actions">
<button class="btn btn-secondary" id="use-defaults">Use Defaults</button> <button class="btn btn-secondary" id="use-defaults">Use Defaults</button>
@ -132,147 +236,6 @@ v.21
</div> </div>
<script> <script>
// Theme toggle initialization
document.addEventListener('DOMContentLoaded', function () {
// Initialize theme toggle button based on current theme
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
// Check current theme
const isDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
// Update button style based on theme
if (isDeepSea) {
document.body.classList.add('deepsea-theme');
themeToggle.style.borderColor = '#0088cc';
themeToggle.style.color = '#0088cc';
} else {
document.body.classList.remove('deepsea-theme');
themeToggle.style.borderColor = '#f2a900';
themeToggle.style.color = '#f2a900';
}
// Add click event listener
themeToggle.addEventListener('click', function () {
toggleTheme(); // This will now trigger a page refresh
});
}
// Update terminal colors based on theme (boot.html specific)
function updateTerminalColors() {
const isDeepSeaTheme = localStorage.getItem('useDeepSeaTheme') === 'true';
if (isDeepSeaTheme) {
document.body.classList.add('deepsea-theme');
} else {
document.body.classList.remove('deepsea-theme');
}
}
// Initialize terminal colors
updateTerminalColors();
});
// Add a function to populate all available timezones
function populateTimezones() {
const otherTimezones = document.getElementById('other-timezones');
// Common timezone areas to include
const commonAreas = [
'Africa', 'America', 'Antarctica', 'Asia', 'Atlantic',
'Australia', 'Europe', 'Indian', 'Pacific'
];
// Fetch the list of available timezones
fetch('/api/available_timezones')
.then(response => response.json())
.then(data => {
if (!data.timezones || !Array.isArray(data.timezones)) {
console.error('Invalid timezone data received');
return;
}
// Sort timezones and filter to include only common areas
const sortedTimezones = data.timezones
.filter(tz => commonAreas.some(area => tz.startsWith(area + '/')))
.sort();
// Add options for each timezone (excluding those already in common list)
const commonOptions = Array.from(document.querySelectorAll('#timezone optgroup:first-child option'))
.map(opt => opt.value);
sortedTimezones.forEach(tz => {
if (!commonOptions.includes(tz)) {
const option = document.createElement('option');
option.value = tz;
option.textContent = tz.replace('_', ' ');
otherTimezones.appendChild(option);
}
});
})
.catch(error => console.error('Error fetching timezones:', error));
}
// Call this when the page loads
document.addEventListener('DOMContentLoaded', populateTimezones);
// Load the current timezone from configuration
function loadTimezoneFromConfig() {
if (currentConfig && currentConfig.timezone) {
const timezoneSelect = document.getElementById('timezone');
// First, check if the option exists
let optionExists = false;
for (let i = 0; i < timezoneSelect.options.length; i++) {
if (timezoneSelect.options[i].value === currentConfig.timezone) {
timezoneSelect.selectedIndex = i;
optionExists = true;
break;
}
}
// If the option doesn't exist yet (might be in the 'other' group being loaded)
// set a data attribute to select it when options are loaded
if (!optionExists) {
timezoneSelect.setAttribute('data-select-value', currentConfig.timezone);
}
}
}
// Call this after loading config
loadConfig().then(() => {
loadTimezoneFromConfig();
});
// Update saveConfig to include network fee
function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const timezone = document.getElementById('timezone').value;
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
const updatedConfig = {
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
power_cost: powerCost,
power_usage: powerUsage,
timezone: timezone,
network_fee: networkFee
};
return fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConfig)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to save configuration');
}
return response.json();
});
}
// Debug logging // Debug logging
function updateDebug(message) { function updateDebug(message) {
document.getElementById('debug-info').textContent = message; document.getElementById('debug-info').textContent = message;
@ -306,10 +269,10 @@ v.21
power_usage: 0.0 power_usage: 0.0
}; };
// Update loadConfig function to include network fee // Replace the current loadConfig function with this improved version
function loadConfig() { function loadConfig() {
return new Promise((resolve, reject) => { // Always make a fresh request to get the latest config
fetch('/api/config?nocache=' + new Date().getTime()) fetch('/api/config?nocache=' + new Date().getTime()) // Add cache-busting parameter
.then(response => { .then(response => {
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to load configuration: ' + response.statusText); throw new Error('Failed to load configuration: ' + response.statusText);
@ -320,13 +283,11 @@ v.21
console.log("Loaded configuration:", data); console.log("Loaded configuration:", data);
currentConfig = data; currentConfig = data;
// Update form fields with latest values // After loading, always update the form fields with the latest values
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 || "";
configLoaded = true; configLoaded = true;
resolve(currentConfig);
}) })
.catch(err => { .catch(err => {
console.error("Error loading config:", err); console.error("Error loading config:", err);
@ -334,16 +295,13 @@ v.21
currentConfig = { currentConfig = {
wallet: "yourwallethere", wallet: "yourwallethere",
power_cost: 0.0, power_cost: 0.0,
power_usage: 0.0, power_usage: 0.0
network_fee: 0.0
}; };
// Still update the form with default values
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 || "";
resolve(currentConfig);
});
}); });
} }
@ -370,6 +328,33 @@ v.21
}); });
}); });
// Save configuration
function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const updatedConfig = {
wallet: wallet || currentConfig.wallet,
power_cost: powerCost,
power_usage: powerUsage
};
return fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConfig)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to save configuration');
}
return response.json();
});
}
// Safety timeout: redirect after 120 seconds if boot not complete // Safety timeout: redirect after 120 seconds if boot not complete
window.addEventListener('load', function () { window.addEventListener('load', function () {
setTimeout(function () { setTimeout(function () {
@ -399,19 +384,29 @@ v.21
}); });
}); });
// Update Use Defaults button handler // Replace the current Use Defaults button event listener with this fixed version
document.getElementById('use-defaults').addEventListener('click', function () { document.getElementById('use-defaults').addEventListener('click', function () {
// Set default values including network fee console.log("Use Defaults button clicked");
document.getElementById('wallet-address').value = "35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS";
document.getElementById('power-cost').value = 0.0;
document.getElementById('power-usage').value = 0.0;
document.getElementById('network-fee').value = 0.0;
// Visual feedback // Always use the hardcoded default values, not the currentConfig
const defaultWallet = "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9";
const defaultPowerCost = 0.0;
const defaultPowerUsage = 0.0;
console.log("Setting to default values");
// Apply the hardcoded default values to the form fields
document.getElementById('wallet-address').value = defaultWallet;
document.getElementById('power-cost').value = defaultPowerCost;
document.getElementById('power-usage').value = defaultPowerUsage;
// Show visual feedback that the button was clicked
const btn = document.getElementById('use-defaults'); const btn = document.getElementById('use-defaults');
const originalText = btn.textContent; const originalText = btn.textContent;
btn.textContent = "Defaults Applied"; btn.textContent = "Defaults Applied";
btn.style.backgroundColor = "#32CD32"; btn.style.backgroundColor = "#32CD32";
// Reset the button after a short delay
setTimeout(function () { setTimeout(function () {
btn.textContent = originalText; btn.textContent = originalText;
btn.style.backgroundColor = ""; btn.style.backgroundColor = "";
@ -503,14 +498,10 @@ v.21
} }
setTimeout(processNextMessage, 500); setTimeout(processNextMessage, 500);
} else { } else {
// If user selects 'N', show configuration form directly without boot messages // If user selects 'N', just redirect to dashboard
outputElement.innerHTML += "N\n\nDASHBOARD INITIALIZATION ABORTED.\n"; outputElement.innerHTML += "N\n\nDASHBOARD INITIALIZATION ABORTED.\n";
outputElement.innerHTML += "\nPlease configure your mining setup:\n"; outputElement.innerHTML += "\nUsing default configuration values.\n";
setTimeout(redirectToDashboard, 2000);
// Short pause and then show the configuration form
setTimeout(function () {
document.getElementById('config-form').style.display = 'block';
}, 1000);
} }
} catch (err) { } catch (err) {
setTimeout(redirectToDashboard, 1000); setTimeout(redirectToDashboard, 1000);
@ -607,7 +598,7 @@ v.21
// Fallback messages (used immediately) // Fallback messages (used immediately)
function setupFallbackMessages() { function setupFallbackMessages() {
bootMessages = [ bootMessages = [
{ text: "BITCOIN OS - MINING CONTROL SYSTEM - v21.000.000\n", speed: 25, delay: 300 }, { text: "BITCOIN OS - MINING SYSTEM - v21.000.000\n", speed: 25, delay: 300 },
{ text: "Copyright (c) 2009-2025 Satoshi Nakamoto\n", speed: 20, delay: 250 }, { text: "Copyright (c) 2009-2025 Satoshi Nakamoto\n", speed: 20, delay: 250 },
{ text: "All rights reserved.\n\n", speed: 25, delay: 300 }, { text: "All rights reserved.\n\n", speed: 25, delay: 300 },
{ text: "INITIALIZING SYSTEM...\n", speed: 25, delay: 300 }, { text: "INITIALIZING SYSTEM...\n", speed: 25, delay: 300 },

View File

@ -1,10 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}BTC-OS Mining Dashboard {% endblock %} {% block title %}BTC-OS Mining Dashboard v 0.3{% endblock %}
{% block css %} {% block css %}
<link rel="stylesheet" href="/static/css/dashboard.css"> <link rel="stylesheet" href="/static/css/dashboard.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %} {% endblock %}
{% block dashboard_active %}active{% endblock %} {% block dashboard_active %}active{% endblock %}
@ -41,11 +40,18 @@
<span id="last_share" class="metric-value">{{ metrics.total_last_share or "N/A" }}</span> <span id="last_share" class="metric-value">{{ metrics.total_last_share or "N/A" }}</span>
</p> </p>
<p> <p>
<strong>Blocks Found:</strong> <strong>Pool Fees:</strong>
<span id="blocks_found" class="metric-value white"> <span id="pool_fees_percentage" class="metric-value">
{{ metrics.blocks_found if metrics and metrics.blocks_found else "0" }} {% if metrics and metrics.pool_fees_percentage is defined %}
{{ metrics.pool_fees_percentage }}%
{% if metrics.pool_fees_percentage >= 0.9 and metrics.pool_fees_percentage <= 1.3 %}
<span class="fee-star"></span> <span class="datum-label">DATUM</span>
{% endif %}
{% else %}
N/A
{% endif %}
</span> </span>
<span id="indicator_blocks_found"></span> <span id="indicator_pool_fees_percentage"></span>
</p> </p>
</div> </div>
</div> </div>
@ -93,18 +99,11 @@
<span id="indicator_est_time_to_payout"></span> <span id="indicator_est_time_to_payout"></span>
</p> </p>
<p> <p>
<strong>Pool Fees:</strong> <strong>Blocks Found:</strong>
<span id="pool_fees_percentage" class="metric-value"> <span id="blocks_found" class="metric-value white">
{% if metrics and metrics.pool_fees_percentage is defined and metrics.pool_fees_percentage is not none %} {{ metrics.blocks_found if metrics and metrics.blocks_found else "0" }}
{{ metrics.pool_fees_percentage }}%
{% if metrics.pool_fees_percentage is not none and metrics.pool_fees_percentage >= 0.9 and metrics.pool_fees_percentage <= 1.3 %}
<span class="fee-star"></span> <span class="datum-label">DATUM</span> <span class="fee-star"></span>
{% endif %}
{% else %}
N/A
{% endif %}
</span> </span>
<span id="indicator_pool_fees_percentage"></span> <span id="indicator_blocks_found"></span>
</p> </p>
</div> </div>
</div> </div>
@ -118,7 +117,7 @@
<div class="card-header">Pool Hashrates</div> <div class="card-header">Pool Hashrates</div>
<div class="card-body"> <div class="card-body">
<p> <p>
<strong>Pool Hashrate:</strong> <strong>Pool Total Hashrate:</strong>
<span id="pool_total_hashrate" class="metric-value white"> <span id="pool_total_hashrate" class="metric-value white">
{% if metrics and metrics.pool_total_hashrate and metrics.pool_total_hashrate_unit %} {% if metrics and metrics.pool_total_hashrate and metrics.pool_total_hashrate_unit %}
{{ metrics.pool_total_hashrate }} {{ metrics.pool_total_hashrate_unit[:-2]|upper ~ metrics.pool_total_hashrate_unit[-2:] }} {{ metrics.pool_total_hashrate }} {{ metrics.pool_total_hashrate_unit[:-2]|upper ~ metrics.pool_total_hashrate_unit[-2:] }}
@ -201,17 +200,6 @@
<div class="card"> <div class="card">
<div class="card-header">Network Stats</div> <div class="card-header">Network Stats</div>
<div class="card-body"> <div class="card-body">
<p>
<strong>BTC Price:</strong>
<span id="btc_price" class="metric-value yellow">
{% if metrics and metrics.btc_price %}
${{ "%.2f"|format(metrics.btc_price) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_btc_price"></span>
</p>
<p> <p>
<strong>Block Number:</strong> <strong>Block Number:</strong>
<span id="block_number" class="metric-value white"> <span id="block_number" class="metric-value white">
@ -223,6 +211,17 @@
</span> </span>
<span id="indicator_block_number"></span> <span id="indicator_block_number"></span>
</p> </p>
<p>
<strong>BTC Price:</strong>
<span id="btc_price" class="metric-value yellow">
{% if metrics and metrics.btc_price %}
${{ "%.2f"|format(metrics.btc_price) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_btc_price"></span>
</p>
<p> <p>
<strong>Network Hashrate:</strong> <strong>Network Hashrate:</strong>
<span id="network_hashrate" class="metric-value white"> <span id="network_hashrate" class="metric-value white">
@ -257,7 +256,7 @@
<div class="card-header">SATOSHI EARNINGS</div> <div class="card-header">SATOSHI EARNINGS</div>
<div class="card-body"> <div class="card-body">
<p> <p>
<strong>Projected Daily (Net):</strong> <strong>Daily Mined (Net):</strong>
<span id="daily_mined_sats" class="metric-value yellow"> <span id="daily_mined_sats" class="metric-value yellow">
{% if metrics and metrics.daily_mined_sats %} {% if metrics and metrics.daily_mined_sats %}
{{ metrics.daily_mined_sats|commafy }} SATS {{ metrics.daily_mined_sats|commafy }} SATS
@ -268,7 +267,7 @@
<span id="indicator_daily_mined_sats"></span> <span id="indicator_daily_mined_sats"></span>
</p> </p>
<p> <p>
<strong>Projected Monthly (Net):</strong> <strong>Monthly Mined (Net):</strong>
<span id="monthly_mined_sats" class="metric-value yellow"> <span id="monthly_mined_sats" class="metric-value yellow">
{% if metrics and metrics.monthly_mined_sats %} {% if metrics and metrics.monthly_mined_sats %}
{{ metrics.monthly_mined_sats|commafy }} SATS {{ metrics.monthly_mined_sats|commafy }} SATS

View File

@ -1,10 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD {% endblock %} {% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD v 0.3{% endblock %}
{% block css %} {% block css %}
<link rel="stylesheet" href="/static/css/notifications.css"> <link rel="stylesheet" href="/static/css/notifications.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %} {% endblock %}
{% block header %}NOTIFICATION CENTER v 0.1{% endblock %} {% block header %}NOTIFICATION CENTER v 0.1{% endblock %}

View File

@ -1,10 +1,9 @@
{% extends "base.html" %} {% extends "base.html" %}
{% block title %}WORKERS - BTC-OS MINING DASHBOARD {% endblock %} {% block title %}WORKERS - BTC-OS MINING DASHBOARD v 0.3{% endblock %}
{% block css %} {% block css %}
<link rel="stylesheet" href="/static/css/workers.css"> <link rel="stylesheet" href="/static/css/workers.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %} {% endblock %}
{% block header %}WORKERS OVERVIEW{% endblock %} {% block header %}WORKERS OVERVIEW{% endblock %}
@ -67,6 +66,17 @@
</div> </div>
<div class="summary-stat-label">DAILY SATS</div> <div class="summary-stat-label">DAILY SATS</div>
</div> </div>
<div class="summary-stat">
<div class="summary-stat-value white-glow" id="avg-acceptance-rate">
{% if avg_acceptance_rate is defined %}
{{ "%.2f"|format(avg_acceptance_rate) }}%
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">ACCEPTANCE</div>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,7 +5,6 @@ import logging
import random import random
from datetime import datetime, timedelta from datetime import datetime, timedelta
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from config import get_timezone
class WorkerService: class WorkerService:
"""Service for retrieving and managing worker data.""" """Service for retrieving and managing worker data."""
@ -48,8 +47,9 @@ class WorkerService:
"hashrate_unit": "TH/s", "hashrate_unit": "TH/s",
"total_earnings": 0.0, "total_earnings": 0.0,
"daily_sats": 0, "daily_sats": 0,
"avg_acceptance_rate": 0.0,
"hashrate_history": [], "hashrate_history": [],
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat() "timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
} }
def get_workers_data(self, cached_metrics, force_refresh=False): def get_workers_data(self, cached_metrics, force_refresh=False):
@ -286,7 +286,7 @@ class WorkerService:
dict: Default worker data dict: Default worker data
""" """
is_online = status == "online" is_online = status == "online"
current_time = datetime.now(ZoneInfo(get_timezone())) current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
# Generate some reasonable hashrate and other values # Generate some reasonable hashrate and other values
hashrate = round(random.uniform(50, 100), 2) if is_online else 0 hashrate = round(random.uniform(50, 100), 2) if is_online else 0
@ -306,6 +306,7 @@ class WorkerService:
"efficiency": round(random.uniform(80, 95), 1) if is_online else 0, "efficiency": round(random.uniform(80, 95), 1) if is_online else 0,
"last_share": last_share, "last_share": last_share,
"earnings": round(random.uniform(0.0001, 0.001), 8), "earnings": round(random.uniform(0.0001, 0.001), 8),
"acceptance_rate": round(random.uniform(95, 99), 1),
"power_consumption": round(random.uniform(2000, 3500)) if is_online else 0, "power_consumption": round(random.uniform(2000, 3500)) if is_online else 0,
"temperature": round(random.uniform(55, 75)) if is_online else 0 "temperature": round(random.uniform(55, 75)) if is_online else 0
} }
@ -327,14 +328,10 @@ class WorkerService:
return self.generate_default_workers_data() return self.generate_default_workers_data()
# Check if we have workers_hashing information # Check if we have workers_hashing information
workers_count = cached_metrics.get("workers_hashing") workers_count = cached_metrics.get("workers_hashing", 0)
# Handle None value for workers_count
if workers_count is None:
logging.warning("No workers_hashing value in cached metrics, defaulting to 1 worker")
workers_count = 1
# Force at least 1 worker if the count is 0 # Force at least 1 worker if the count is 0
elif workers_count <= 0: if workers_count <= 0:
logging.warning("No workers reported in metrics, forcing 1 worker") logging.warning("No workers reported in metrics, forcing 1 worker")
workers_count = 1 workers_count = 1
@ -439,8 +436,9 @@ class WorkerService:
"hashrate_unit": hashrate_unit, "hashrate_unit": hashrate_unit,
"total_earnings": total_earnings, "total_earnings": total_earnings,
"daily_sats": daily_sats, # Fixed daily_sats value "daily_sats": daily_sats, # Fixed daily_sats value
"avg_acceptance_rate": 98.8, # Default value
"hashrate_history": hashrate_history, "hashrate_history": hashrate_history,
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat() "timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
} }
# Update cache # Update cache
@ -484,7 +482,7 @@ class WorkerService:
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0) avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
workers = [] workers = []
current_time = datetime.now(ZoneInfo(get_timezone())) current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
# Default total unpaid earnings if not provided # Default total unpaid earnings if not provided
if total_unpaid_earnings is None or total_unpaid_earnings <= 0: if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
@ -512,6 +510,9 @@ class WorkerService:
minutes_ago = random.randint(0, 5) minutes_ago = random.randint(0, 5)
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M") last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
# Generate acceptance rate (95-100%)
acceptance_rate = round(random.uniform(95, 100), 1)
# Generate temperature (normal operating range) # Generate temperature (normal operating range)
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55) temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
@ -530,6 +531,7 @@ class WorkerService:
"efficiency": round(random.uniform(65, 95), 1), "efficiency": round(random.uniform(65, 95), 1),
"last_share": last_share, "last_share": last_share,
"earnings": 0, # Will be set after all workers are generated "earnings": 0, # Will be set after all workers are generated
"acceptance_rate": acceptance_rate,
"power_consumption": model_info["power"], "power_consumption": model_info["power"],
"temperature": temperature "temperature": temperature
}) })
@ -568,6 +570,7 @@ class WorkerService:
"efficiency": 0, "efficiency": 0,
"last_share": last_share, "last_share": last_share,
"earnings": 0, # Minimal earnings for offline workers "earnings": 0, # Minimal earnings for offline workers
"acceptance_rate": round(random.uniform(95, 99), 1),
"power_consumption": 0, "power_consumption": 0,
"temperature": 0 "temperature": 0
}) })
@ -632,7 +635,7 @@ class WorkerService:
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0) avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
workers = [] workers = []
current_time = datetime.now(ZoneInfo(get_timezone())) current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
# Default total unpaid earnings if not provided # Default total unpaid earnings if not provided
if total_unpaid_earnings is None or total_unpaid_earnings <= 0: if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
@ -669,6 +672,9 @@ class WorkerService:
minutes_ago = random.randint(0, 3) minutes_ago = random.randint(0, 3)
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M") last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
# Generate acceptance rate (95-100%)
acceptance_rate = round(random.uniform(95, 100), 1)
# Generate temperature (normal operating range) # Generate temperature (normal operating range)
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55) temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
@ -694,6 +700,7 @@ class WorkerService:
"efficiency": round(random.uniform(65, 95), 1), "efficiency": round(random.uniform(65, 95), 1),
"last_share": last_share, "last_share": last_share,
"earnings": 0, # Will be set after all workers are generated "earnings": 0, # Will be set after all workers are generated
"acceptance_rate": acceptance_rate,
"power_consumption": model_info["power"], "power_consumption": model_info["power"],
"temperature": temperature "temperature": temperature
}) })
@ -739,6 +746,7 @@ class WorkerService:
"efficiency": 0, "efficiency": 0,
"last_share": last_share, "last_share": last_share,
"earnings": 0, # Minimal earnings for offline workers "earnings": 0, # Minimal earnings for offline workers
"acceptance_rate": round(random.uniform(95, 99), 1),
"power_consumption": 0, "power_consumption": 0,
"temperature": 0 "temperature": 0
}) })