diff --git a/setup.py b/setup.py index 8524868..c845f2f 100644 --- a/setup.py +++ b/setup.py @@ -1,12 +1,43 @@ +#!/usr/bin/env python3 """ -Setup script to prepare project structure and directories. +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 -logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') +# 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 = [ @@ -14,7 +45,8 @@ DIRECTORIES = [ 'static/js', 'static/img', 'templates', - 'logs' + 'logs', + 'data' # For temporary data storage ] # Files to move to their correct locations @@ -26,11 +58,14 @@ FILE_MAPPINGS = { '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', # JS files 'main.js': 'static/js/main.js', 'workers.js': 'static/js/workers.js', - 'retro-refresh.js': 'static/js/retro-refresh.js', + 'blocks.js': 'static/js/blocks.js', + 'block-animation.js': 'static/js/block-animation.js', + 'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js', # Template files 'base.html': 'templates/base.html', @@ -38,48 +73,372 @@ FILE_MAPPINGS = { 'workers.html': 'templates/workers.html', 'boot.html': 'templates/boot.html', 'error.html': 'templates/error.html', + 'blocks.html': 'templates/blocks.html', } +# Default configuration +DEFAULT_CONFIG = { + "power_cost": 0.0, + "power_usage": 0.0, + "wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9" +} + +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('--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') + return parser.parse_args() + def create_directory_structure(): """Create the necessary directory structure.""" + logger.info("Creating directory structure...") + success = True + for directory in DIRECTORIES: - os.makedirs(directory, exist_ok=True) - logging.info(f"Created directory: {directory}") + 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(): - """Move files to their correct locations.""" +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) - # Copy the file to its destination - shutil.copy2(source, destination) - logging.info(f"Moved {source} to {destination}") + # 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: - logging.warning(f"Source file not found: {source}") + 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 create_empty_config(): - """Create an empty config.json file if it doesn't exist.""" - if not os.path.exists('config.json'): - with open('config.json', 'w') as f: - f.write('{\n "power_cost": 0.0,\n "power_usage": 0.0,\n "wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9"\n}') - logging.info("Created empty config.json") +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") + + # 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") + + 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.""" - logging.info("Starting setup process") + 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 - create_directory_structure() + if not create_directory_structure(): + logger.error("Failed to create directory structure.") + return 1 # Move files to their correct locations - move_files() + if not move_files(args.force): + logger.warning("Some files could not be moved, but continuing...") - # Create empty config.json - create_empty_config() + # Create or update configuration + if not create_config(args): + logger.error("Failed to create configuration file.") + return 1 - logging.info("Setup completed successfully") + # 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__": - main() + exit_code = main() + sys.exit(exit_code)