mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 11:10:44 +02:00

- Introduced `theme-toggle.css` and `theme.js` to support a new theme toggle feature. - Updated default configuration to include timezone and network fee. - Enhanced command line arguments for network fee, timezone, and theme selection. - Modified `create_config` to handle new configuration values from command line. - Updated logging to reflect new network fee and timezone settings. - Changed theme icons in `theme-toggle.css` for desktop and mobile views.
521 lines
18 KiB
Python
521 lines
18 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Enhanced setup script for Bitcoin Mining Dashboard.
|
|
|
|
This script prepares the project structure, installs dependencies,
|
|
verifies configuration, and provides system checks for optimal operation.
|
|
"""
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import logging
|
|
import argparse
|
|
import subprocess
|
|
import json
|
|
import re
|
|
from pathlib import Path
|
|
|
|
# Configure logging with color support
|
|
try:
|
|
import colorlog
|
|
|
|
handler = colorlog.StreamHandler()
|
|
handler.setFormatter(colorlog.ColoredFormatter(
|
|
'%(log_color)s%(asctime)s - %(levelname)s - %(message)s',
|
|
log_colors={
|
|
'DEBUG': 'cyan',
|
|
'INFO': 'green',
|
|
'WARNING': 'yellow',
|
|
'ERROR': 'red',
|
|
'CRITICAL': 'red,bg_white',
|
|
}
|
|
))
|
|
|
|
logger = colorlog.getLogger()
|
|
logger.addHandler(handler)
|
|
logger.setLevel(logging.INFO)
|
|
except ImportError:
|
|
# Fallback to standard logging if colorlog is not available
|
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
|
logger = logging.getLogger()
|
|
|
|
# Directory structure to create
|
|
DIRECTORIES = [
|
|
'static/css',
|
|
'static/js',
|
|
'static/js/min', # For minified JS files
|
|
'templates',
|
|
'logs',
|
|
'data' # For temporary data storage
|
|
]
|
|
|
|
# Files to move to their correct locations
|
|
FILE_MAPPINGS = {
|
|
# CSS files
|
|
'common.css': 'static/css/common.css',
|
|
'dashboard.css': 'static/css/dashboard.css',
|
|
'workers.css': 'static/css/workers.css',
|
|
'boot.css': 'static/css/boot.css',
|
|
'error.css': 'static/css/error.css',
|
|
'retro-refresh.css': 'static/css/retro-refresh.css',
|
|
'blocks.css': 'static/css/blocks.css',
|
|
'notifications.css': 'static/css/notifications.css',
|
|
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
|
|
|
|
# JS files
|
|
'main.js': 'static/js/main.js',
|
|
'workers.js': 'static/js/workers.js',
|
|
'blocks.js': 'static/js/blocks.js',
|
|
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
|
|
'notifications.js': 'static/js/notifications.js',
|
|
'theme.js': 'static/js/theme.js', # Added theme.js
|
|
|
|
# Template files
|
|
'base.html': 'templates/base.html',
|
|
'dashboard.html': 'templates/dashboard.html',
|
|
'workers.html': 'templates/workers.html',
|
|
'boot.html': 'templates/boot.html',
|
|
'error.html': 'templates/error.html',
|
|
'blocks.html': 'templates/blocks.html',
|
|
'notifications.html': 'templates/notifications.html',
|
|
}
|
|
|
|
# Default configuration
|
|
DEFAULT_CONFIG = {
|
|
"power_cost": 0.0,
|
|
"power_usage": 0.0,
|
|
"wallet": "yourwallethere",
|
|
"timezone": "America/Los_Angeles", # Added default timezone
|
|
"network_fee": 0.0 # Added default network fee
|
|
}
|
|
|
|
def parse_arguments():
|
|
"""Parse command line arguments."""
|
|
parser = argparse.ArgumentParser(description='Setup the Bitcoin Mining Dashboard')
|
|
parser.add_argument('--debug', action='store_true', help='Enable debug logging')
|
|
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-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('--force', action='store_true', help='Force file overwrite')
|
|
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('--theme', choices=['bitcoin', 'deepsea'], help='Set the default UI theme') # Added theme parameter
|
|
return parser.parse_args()
|
|
|
|
def create_directory_structure():
|
|
"""Create the necessary directory structure."""
|
|
logger.info("Creating directory structure...")
|
|
success = True
|
|
|
|
for directory in DIRECTORIES:
|
|
try:
|
|
os.makedirs(directory, exist_ok=True)
|
|
logger.debug(f"Created directory: {directory}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to create directory {directory}: {str(e)}")
|
|
success = False
|
|
|
|
if success:
|
|
logger.info("✓ Directory structure created successfully")
|
|
else:
|
|
logger.warning("⚠ Some directories could not be created")
|
|
|
|
return success
|
|
|
|
def move_files(force=False):
|
|
"""
|
|
Move files to their correct locations.
|
|
|
|
Args:
|
|
force (bool): Force overwriting of existing files
|
|
"""
|
|
logger.info("Moving files to their correct locations...")
|
|
success = True
|
|
moved_count = 0
|
|
skipped_count = 0
|
|
missing_count = 0
|
|
|
|
for source, destination in FILE_MAPPINGS.items():
|
|
if os.path.exists(source):
|
|
# Create the directory if it doesn't exist
|
|
os.makedirs(os.path.dirname(destination), exist_ok=True)
|
|
|
|
# Check if destination exists and handle according to force flag
|
|
if os.path.exists(destination) and not force:
|
|
logger.debug(f"Skipped {source} (destination already exists)")
|
|
skipped_count += 1
|
|
continue
|
|
|
|
try:
|
|
# Copy the file to its destination
|
|
shutil.copy2(source, destination)
|
|
logger.debug(f"Moved {source} to {destination}")
|
|
moved_count += 1
|
|
except Exception as e:
|
|
logger.error(f"Failed to copy {source} to {destination}: {str(e)}")
|
|
success = False
|
|
else:
|
|
logger.warning(f"Source file not found: {source}")
|
|
missing_count += 1
|
|
|
|
if success:
|
|
logger.info(f"✓ File movement completed: {moved_count} moved, {skipped_count} skipped, {missing_count} missing")
|
|
else:
|
|
logger.warning("⚠ Some files could not be moved")
|
|
|
|
return success
|
|
|
|
def minify_js_files():
|
|
"""Minify JavaScript files."""
|
|
logger.info("Minifying JavaScript files...")
|
|
|
|
try:
|
|
import jsmin
|
|
except ImportError:
|
|
logger.error("jsmin package not found. Installing...")
|
|
try:
|
|
subprocess.run([sys.executable, "-m", "pip", "install", "jsmin"], check=True)
|
|
import jsmin
|
|
logger.info("✓ jsmin package installed successfully")
|
|
except Exception as e:
|
|
logger.error(f"Failed to install jsmin: {str(e)}")
|
|
logger.error("Please run: pip install jsmin")
|
|
return False
|
|
|
|
js_dir = 'static/js'
|
|
min_dir = os.path.join(js_dir, 'min')
|
|
os.makedirs(min_dir, exist_ok=True)
|
|
|
|
minified_count = 0
|
|
for js_file in os.listdir(js_dir):
|
|
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
|
|
input_path = os.path.join(js_dir, js_file)
|
|
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
|
|
|
|
try:
|
|
with open(input_path, 'r') as f:
|
|
js_content = f.read()
|
|
|
|
# Minify the content
|
|
minified = jsmin.jsmin(js_content)
|
|
|
|
# Write minified content
|
|
with open(output_path, 'w') as f:
|
|
f.write(minified)
|
|
|
|
minified_count += 1
|
|
logger.debug(f"Minified {js_file}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to minify {js_file}: {str(e)}")
|
|
|
|
logger.info(f"✓ JavaScript minification completed: {minified_count} files processed")
|
|
return True
|
|
|
|
def validate_wallet_address(wallet):
|
|
"""
|
|
Validate Bitcoin wallet address format.
|
|
|
|
Args:
|
|
wallet (str): Bitcoin wallet address
|
|
|
|
Returns:
|
|
bool: True if valid, False otherwise
|
|
"""
|
|
# Basic validation patterns for different Bitcoin address formats
|
|
patterns = [
|
|
r'^1[a-km-zA-HJ-NP-Z1-9]{25,34}$', # Legacy
|
|
r'^3[a-km-zA-HJ-NP-Z1-9]{25,34}$', # P2SH
|
|
r'^bc1[a-zA-Z0-9]{39,59}$', # Bech32
|
|
r'^bc1p[a-zA-Z0-9]{39,59}$', # Taproot
|
|
r'^bc1p[a-z0-9]{73,107}$' # Longform Taproot
|
|
]
|
|
|
|
# Check if the wallet matches any of the patterns
|
|
for pattern in patterns:
|
|
if re.match(pattern, wallet):
|
|
return True
|
|
|
|
return False
|
|
|
|
def create_config(args):
|
|
"""
|
|
Create or update config.json file.
|
|
|
|
Args:
|
|
args: Command line arguments
|
|
"""
|
|
config_file = args.config if args.config else 'config.json'
|
|
config = DEFAULT_CONFIG.copy()
|
|
|
|
# Load existing config if available
|
|
if os.path.exists(config_file):
|
|
try:
|
|
with open(config_file, 'r') as f:
|
|
existing_config = json.load(f)
|
|
config.update(existing_config)
|
|
logger.info(f"Loaded existing configuration from {config_file}")
|
|
except json.JSONDecodeError:
|
|
logger.warning(f"Invalid JSON in {config_file}, using default configuration")
|
|
except Exception as e:
|
|
logger.error(f"Error reading {config_file}: {str(e)}")
|
|
|
|
# Update config from command line arguments
|
|
if args.wallet:
|
|
if validate_wallet_address(args.wallet):
|
|
config["wallet"] = args.wallet
|
|
else:
|
|
logger.warning(f"Invalid wallet address format: {args.wallet}")
|
|
logger.warning("Using default or existing wallet address")
|
|
|
|
if args.power_cost is not None:
|
|
if args.power_cost >= 0:
|
|
config["power_cost"] = args.power_cost
|
|
else:
|
|
logger.warning("Power cost cannot be negative, using default or existing value")
|
|
|
|
if args.power_usage is not None:
|
|
if args.power_usage >= 0:
|
|
config["power_usage"] = args.power_usage
|
|
else:
|
|
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
|
|
try:
|
|
with open(config_file, 'w') as f:
|
|
json.dump(config, f, indent=2, sort_keys=True)
|
|
logger.info(f"✓ Configuration saved to {config_file}")
|
|
except Exception as e:
|
|
logger.error(f"Failed to save configuration: {str(e)}")
|
|
return False
|
|
|
|
# Print current configuration
|
|
logger.info("Current configuration:")
|
|
logger.info(f" ├── Wallet address: {config['wallet']}")
|
|
logger.info(f" ├── Power cost: ${config['power_cost']} per kWh")
|
|
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
|
|
|
|
def check_dependencies(skip=False):
|
|
"""
|
|
Check if required Python dependencies are installed.
|
|
|
|
Args:
|
|
skip (bool): Skip the dependency check
|
|
"""
|
|
if skip:
|
|
logger.info("Skipping dependency check")
|
|
return True
|
|
|
|
logger.info("Checking dependencies...")
|
|
|
|
try:
|
|
# Check if pip is available
|
|
subprocess.run([sys.executable, "-m", "pip", "--version"],
|
|
check=True, capture_output=True, text=True)
|
|
except Exception as e:
|
|
logger.error(f"Pip is not available: {str(e)}")
|
|
logger.error("Please install pip before continuing")
|
|
return False
|
|
|
|
# Check if requirements.txt exists
|
|
if not os.path.exists('requirements.txt'):
|
|
logger.error("requirements.txt not found")
|
|
return False
|
|
|
|
# Check currently installed packages
|
|
try:
|
|
result = subprocess.run(
|
|
[sys.executable, "-m", "pip", "freeze"],
|
|
check=True, capture_output=True, text=True
|
|
)
|
|
installed_output = result.stdout
|
|
installed_packages = {
|
|
line.split('==')[0].lower(): line.split('==')[1] if '==' in line else ''
|
|
for line in installed_output.splitlines()
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to check installed packages: {str(e)}")
|
|
installed_packages = {}
|
|
|
|
# Read requirements
|
|
try:
|
|
with open('requirements.txt', 'r') as f:
|
|
requirements = f.read().splitlines()
|
|
except Exception as e:
|
|
logger.error(f"Failed to read requirements.txt: {str(e)}")
|
|
return False
|
|
|
|
# Check each requirement
|
|
missing_packages = []
|
|
for req in requirements:
|
|
if req and not req.startswith('#'):
|
|
package = req.split('==')[0].lower()
|
|
if package not in installed_packages:
|
|
missing_packages.append(req)
|
|
|
|
if missing_packages:
|
|
logger.warning(f"Missing {len(missing_packages)} required packages")
|
|
logger.info("Installing missing packages...")
|
|
|
|
try:
|
|
subprocess.run(
|
|
[sys.executable, "-m", "pip", "install", "-r", "requirements.txt"],
|
|
check=True, capture_output=True, text=True
|
|
)
|
|
logger.info("✓ Dependencies installed successfully")
|
|
except Exception as e:
|
|
logger.error(f"Failed to install dependencies: {str(e)}")
|
|
logger.error("Please run: pip install -r requirements.txt")
|
|
return False
|
|
else:
|
|
logger.info("✓ All required packages are installed")
|
|
|
|
return True
|
|
|
|
def check_redis():
|
|
"""Check if Redis is available."""
|
|
logger.info("Checking Redis availability...")
|
|
|
|
redis_url = os.environ.get("REDIS_URL")
|
|
if not redis_url:
|
|
logger.info("⚠ Redis URL not configured (REDIS_URL environment variable not set)")
|
|
logger.info(" └── The dashboard will run without persistent state")
|
|
logger.info(" └── Set REDIS_URL for better reliability")
|
|
return True
|
|
|
|
try:
|
|
import redis
|
|
client = redis.Redis.from_url(redis_url)
|
|
client.ping()
|
|
logger.info(f"✓ Successfully connected to Redis at {redis_url}")
|
|
return True
|
|
except ImportError:
|
|
logger.warning("Redis Python package not installed")
|
|
logger.info(" └── Run: pip install redis")
|
|
return False
|
|
except Exception as e:
|
|
logger.warning(f"Failed to connect to Redis: {str(e)}")
|
|
logger.info(f" └── Check that Redis is running and accessible at {redis_url}")
|
|
return False
|
|
|
|
def perform_system_checks():
|
|
"""Perform system checks and provide recommendations."""
|
|
logger.info("Performing system checks...")
|
|
|
|
# Check Python version
|
|
python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
if sys.version_info.major < 3 or (sys.version_info.major == 3 and sys.version_info.minor < 9):
|
|
logger.warning(f"⚠ Python version {python_version} is below recommended (3.9+)")
|
|
else:
|
|
logger.info(f"✓ Python version {python_version} is compatible")
|
|
|
|
# Check available memory
|
|
try:
|
|
import psutil
|
|
memory = psutil.virtual_memory()
|
|
memory_gb = memory.total / (1024**3)
|
|
if memory_gb < 1:
|
|
logger.warning(f"⚠ Low system memory: {memory_gb:.2f} GB (recommended: 1+ GB)")
|
|
else:
|
|
logger.info(f"✓ System memory: {memory_gb:.2f} GB")
|
|
except ImportError:
|
|
logger.debug("psutil not available, skipping memory check")
|
|
|
|
# Check write permissions
|
|
log_dir = 'logs'
|
|
try:
|
|
test_file = os.path.join(log_dir, 'test_write.tmp')
|
|
with open(test_file, 'w') as f:
|
|
f.write('test')
|
|
os.remove(test_file)
|
|
logger.info(f"✓ Write permissions for logs directory")
|
|
except Exception as e:
|
|
logger.warning(f"⚠ Cannot write to logs directory: {str(e)}")
|
|
|
|
# Check port availability
|
|
port = 5000 # Default port
|
|
try:
|
|
import socket
|
|
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
s.bind(('localhost', port))
|
|
s.close()
|
|
logger.info(f"✓ Port {port} is available")
|
|
except Exception:
|
|
logger.warning(f"⚠ Port {port} is already in use")
|
|
|
|
logger.info("System checks completed")
|
|
|
|
def main():
|
|
"""Main setup function."""
|
|
args = parse_arguments()
|
|
|
|
# Set logging level
|
|
if args.debug:
|
|
logger.setLevel(logging.DEBUG)
|
|
logger.debug("Debug logging enabled")
|
|
|
|
logger.info("=== Bitcoin Mining Dashboard Setup ===")
|
|
|
|
# Check dependencies
|
|
if not check_dependencies(args.skip_checks):
|
|
logger.error("Dependency check failed. Please install required packages and retry.")
|
|
return 1
|
|
|
|
# Create directory structure
|
|
if not create_directory_structure():
|
|
logger.error("Failed to create directory structure.")
|
|
return 1
|
|
|
|
# Move files to their correct locations
|
|
if not move_files(args.force):
|
|
logger.warning("Some files could not be moved, but continuing...")
|
|
|
|
# Create or update configuration
|
|
if not create_config(args):
|
|
logger.error("Failed to create configuration file.")
|
|
return 1
|
|
|
|
# Minify JavaScript files if requested
|
|
if args.minify:
|
|
if not minify_js_files():
|
|
logger.warning("JavaScript minification failed, but continuing...")
|
|
|
|
# Check Redis if available
|
|
check_redis()
|
|
|
|
# Perform system checks
|
|
if not args.skip_checks:
|
|
perform_system_checks()
|
|
|
|
logger.info("=== Setup completed successfully ===")
|
|
logger.info("")
|
|
logger.info("Next steps:")
|
|
logger.info("1. Verify configuration in config.json")
|
|
logger.info("2. Start the application with: python App.py")
|
|
logger.info("3. Access the dashboard at: http://localhost:5000")
|
|
|
|
return 0
|
|
|
|
if __name__ == "__main__":
|
|
exit_code = main()
|
|
sys.exit(exit_code)
|