Compare commits

..

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

39 changed files with 2944 additions and 7848 deletions

187
App.py
View File

@ -15,14 +15,12 @@ from datetime import datetime
from zoneinfo import ZoneInfo
from flask_caching import Cache
from apscheduler.schedulers.background import BackgroundScheduler
from notification_service import NotificationService, NotificationLevel, NotificationCategory
# Import custom modules
from config import load_config, save_config
from data_service import MiningDashboardService
from worker_service import WorkerService
from state_manager import StateManager, arrow_history, metrics_log
from config import get_timezone
# Initialize Flask app
app = Flask(__name__)
@ -46,7 +44,7 @@ scheduler_recreate_lock = threading.Lock()
scheduler = None
# Global start time
SERVER_START_TIME = datetime.now(ZoneInfo(get_timezone()))
SERVER_START_TIME = datetime.now(ZoneInfo("America/Los_Angeles"))
# Configure logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
@ -55,9 +53,6 @@ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(
redis_url = os.environ.get("REDIS_URL")
state_manager = StateManager(redis_url)
# Initialize notification service after state_manager
notification_service = NotificationService(state_manager)
# --- Disable Client Caching for All Responses ---
@app.after_request
def add_header(response):
@ -92,8 +87,6 @@ def update_metrics_job(force=False):
"""
global cached_metrics, last_metrics_update_time, scheduler, scheduler_last_successful_run
logging.info("Starting update_metrics_job")
try:
# Check scheduler health - enhanced logic to detect failed executors
if not scheduler or not hasattr(scheduler, 'running'):
@ -150,7 +143,6 @@ def update_metrics_job(force=False):
# Set last update time to now
last_metrics_update_time = current_time
logging.info(f"Updated last_metrics_update_time: {last_metrics_update_time}")
# Add timeout handling with a timer
job_timeout = 45 # seconds
@ -169,35 +161,27 @@ def update_metrics_job(force=False):
# Use the dashboard service to fetch metrics
metrics = dashboard_service.fetch_metrics()
if metrics:
logging.info("Fetched metrics successfully")
# First check for notifications by comparing new metrics with old cached metrics
notification_service.check_and_generate_notifications(metrics, cached_metrics)
# Then update cached metrics after comparison
# Update cached metrics
cached_metrics = metrics
# Update state history (only once)
# Update state history
state_manager.update_metrics_history(metrics)
logging.info("Background job: Metrics updated successfully")
job_successful = True
# Mark successful run time for watchdog
scheduler_last_successful_run = time.time()
logging.info(f"Updated scheduler_last_successful_run: {scheduler_last_successful_run}")
# Persist critical state
state_manager.persist_critical_state(cached_metrics, scheduler_last_successful_run, last_metrics_update_time)
# Periodically check and prune data to prevent memory growth
if current_time % 300 < 60: # Every ~5 minutes
logging.info("Pruning old data")
state_manager.prune_old_data()
# Only save state to Redis on a similar schedule, not every update
if current_time % 300 < 60: # Every ~5 minutes
logging.info("Saving graph state")
state_manager.save_graph_state()
# Periodic full memory cleanup (every 2 hours)
@ -218,7 +202,6 @@ def update_metrics_job(force=False):
logging.error(f"Background job: Unhandled exception: {e}")
import traceback
logging.error(traceback.format_exc())
logging.info("Completed update_metrics_job")
# --- SchedulerWatchdog to monitor and recover ---
def scheduler_watchdog():
@ -418,8 +401,8 @@ def dashboard():
# If still None after our attempt, create default metrics
if cached_metrics is None:
default_metrics = {
"server_timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo(get_timezone())).isoformat(),
"server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat(),
"hashrate_24hr": None,
"hashrate_24hr_unit": "TH/s",
"hashrate_3hr": None,
@ -454,11 +437,11 @@ def dashboard():
"arrow_history": {}
}
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 %I:%M:%S %p")
return render_template("dashboard.html", metrics=default_metrics, current_time=current_time)
# 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 %I:%M:%S %p")
return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time)
@app.route("/api/metrics")
@ -468,31 +451,19 @@ def api_metrics():
update_metrics_job()
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
@app.route("/blocks")
def blocks_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("%Y-%m-%d %I:%M:%S %p")
return render_template("blocks.html", current_time=current_time)
# --- Workers Dashboard Route and API ---
@app.route("/workers")
def workers_dashboard():
"""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
# Client-side JS will fetch the full data via API
workers_data = worker_service.get_workers_data(cached_metrics)
@ -519,9 +490,9 @@ def api_workers():
@app.route("/api/time")
def api_time():
"""API endpoint for server time."""
return jsonify({ # correct time
"server_timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo(get_timezone())).isoformat()
return jsonify({
"server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat()
})
# --- New Config Endpoints ---
@ -538,7 +509,7 @@ def get_config():
@app.route("/api/config", methods=["POST"])
def update_config():
"""API endpoint to update configuration."""
global dashboard_service, worker_service # Add this to access the global dashboard_service
global dashboard_service # Add this to access the global dashboard_service
try:
# Get the request data
@ -552,7 +523,7 @@ def update_config():
# Required fields and default values
defaults = {
"wallet": "yourwallethere",
"wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9",
"power_cost": 0.0,
"power_usage": 0.0
}
@ -572,10 +543,6 @@ def update_config():
new_config.get("wallet")
)
logging.info(f"Dashboard service reinitialized with new wallet: {new_config.get('wallet')}")
# Update worker service to use the new dashboard service (with the updated wallet)
worker_service.set_dashboard_service(dashboard_service)
logging.info(f"Worker service updated with the new dashboard service")
# Force a metrics update to reflect the new configuration
update_metrics_job(force=True)
@ -599,7 +566,7 @@ def update_config():
def health_check():
"""Health check endpoint with enhanced system diagnostics."""
# 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
try:
@ -617,7 +584,7 @@ def health_check():
if cached_metrics and cached_metrics.get("server_timestamp"):
try:
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:
logging.error(f"Error calculating data age: {e}")
@ -650,7 +617,7 @@ def health_check():
"redis": {
"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
@ -712,91 +679,6 @@ def force_refresh():
logging.error(f"Force refresh error: {e}")
return jsonify({"status": "error", "message": str(e)}), 500
@app.route("/api/notifications")
def api_notifications():
"""API endpoint for notification data."""
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
unread_only = request.args.get('unread_only', 'false').lower() == 'true'
category = request.args.get('category')
level = request.args.get('level')
notifications = notification_service.get_notifications(
limit=limit,
offset=offset,
unread_only=unread_only,
category=category,
level=level
)
unread_count = notification_service.get_unread_count()
return jsonify({
"notifications": notifications,
"unread_count": unread_count,
"total": len(notifications),
"limit": limit,
"offset": offset
})
@app.route("/api/notifications/unread_count")
def api_unread_count():
"""API endpoint for unread notification count."""
return jsonify({
"unread_count": notification_service.get_unread_count()
})
@app.route("/api/notifications/mark_read", methods=["POST"])
def api_mark_read():
"""API endpoint to mark notifications as read."""
notification_id = request.json.get('notification_id')
success = notification_service.mark_as_read(notification_id)
return jsonify({
"success": success,
"unread_count": notification_service.get_unread_count()
})
@app.route("/api/notifications/delete", methods=["POST"])
def api_delete_notification():
"""API endpoint to delete a notification."""
notification_id = request.json.get('notification_id')
if not notification_id:
return jsonify({"error": "notification_id is required"}), 400
success = notification_service.delete_notification(notification_id)
return jsonify({
"success": success,
"unread_count": notification_service.get_unread_count()
})
@app.route("/api/notifications/clear", methods=["POST"])
def api_clear_notifications():
"""API endpoint to clear notifications."""
category = request.json.get('category')
older_than_days = request.json.get('older_than_days')
cleared_count = notification_service.clear_notifications(
category=category,
older_than_days=older_than_days
)
return jsonify({
"success": True,
"cleared_count": cleared_count,
"unread_count": notification_service.get_unread_count()
})
# Add notifications page route
@app.route("/notifications")
def notifications_page():
"""Serve the notifications page."""
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p")
return render_template("notifications.html", current_time=current_time)
@app.errorhandler(404)
def page_not_found(e):
"""Error handler for 404 errors."""
@ -821,42 +703,17 @@ class RobustMiddleware:
start_response("500 Internal Server Error", [("Content-Type", "text/html")])
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
app.wsgi_app = RobustMiddleware(app.wsgi_app)
# 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()
dashboard_service = MiningDashboardService(
config.get("power_cost", 0.0),
config.get("power_usage", 0.0),
config.get("wallet"),
network_fee=config.get("network_fee", 0.0) # Add network fee parameter
config.get("wallet")
)
worker_service = WorkerService()
# Connect the services

107
README.md
View File

@ -1,28 +1,24 @@
# DeepSea Dashboard
# Ocean.xyz Bitcoin Mining Dashboard
## A Retro Mining Monitoring Solution
## A Comprehensive Monitoring Solution for Bitcoin Miners
This open-source dashboard provides real-time monitoring for Ocean.xyz pool miners, offering detailed insights on hashrate, profitability, worker status, and network metrics. Designed with a retro terminal aesthetic and focused on reliability, it helps miners maintain complete oversight of their operations.
---
## Gallery:
![DeepSea Boot](https://github.com/user-attachments/assets/77222f13-1e95-48ee-a418-afd0e6b7a920)
![DeepSea Config](https://github.com/user-attachments/assets/48fcc2a6-f56e-48b9-ac61-b27e9b4a6e41)
![DeepSea Dashboard](https://github.com/user-attachments/assets/f8f3671e-907a-456a-b8c6-5d9ecd07946c)
![Boot Sequence](https://github.com/user-attachments/assets/8205e8c0-79ad-4780-bc50-237131373cf8)
![Main Dashboard](https://github.com/user-attachments/assets/33dafb93-38ef-4fee-aba1-3a7d38eca3c9)
![Workers Overview](https://github.com/user-attachments/assets/ae78c34c-fbdf-4186-9706-760a67eac44c)
---
## Key Features
### Real-Time Mining Metrics
- **Live Hashrate Tracking**: Monitor 60-second, 10-minute, 3-hour, and 24-hour average hashrates
- **Profitability Analysis**: View daily and monthly earnings in both BTC and USD
- **Financial Calculations**: Automatically calculate revenue, power costs, and net profit
- **Network Statistics**: Track current Bitcoin price, difficulty, and network hashrate
- **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
### Worker Management
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
@ -40,7 +36,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
- **Cross-Tab Synchronization**: Data consistency across multiple browser tabs
- **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
- **Retro Terminal Aesthetic**: Nostalgic interface with modern functionality
@ -48,19 +43,14 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **System Monitor**: Floating status display with uptime and refresh information
- **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
### Installation
1. Clone the repository
```
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git
cd DeepSea-Dashboard
git clone https://github.com/yourusername/bitcoin-mining-dashboard.git
cd bitcoin-mining-dashboard
```
2. Install dependencies:
@ -73,48 +63,35 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
python setup.py
```
4. Start the application:
4. Configure your mining settings:
```json
{
"power_cost": 0.12,
"power_usage": 3450,
"wallet": "your-wallet-address"
}
```
5. Start the application:
```
python App.py
```
5. Open your browser at `http://localhost:5000`
6. Open your browser at `http://localhost:5000`
### Docker Deployment
```bash
docker build -t bitcoin-mining-dashboard .
docker run -d -p 5000:5000 \
-e WALLET=your-wallet-address \
-e POWER_COST=0.12 \
-e POWER_USAGE=3450 \
bitcoin-mining-dashboard
```
For detailed deployment instructions with Redis persistence and Gunicorn configuration, see [deployment_steps.md](deployment_steps.md).
## Using docker-compose (with Redis)
The `docker-compose.yml` file makes it easy to deploy the dashboard and its dependencies.
### Steps to Deploy
1. **Start the services**:
Run the following command in the project root:
```
docker-compose up -d
```
2. **Access the dashboard**:
Open your browser at `http://localhost:5000`.
3. **Stop the services**:
To stop the services, run:
```
docker-compose down
```
### Customization
You can modify the following environment variables in the `docker-compose.yml` file:
- `WALLET`: Your Bitcoin wallet address.
- `POWER_COST`: Cost of power per kWh.
- `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.
For more details, refer to the [docker-compose documentation](https://docs.docker.com/compose/).
## Dashboard Components
### Main Dashboard
@ -165,18 +142,12 @@ Built with a modern stack for reliability and performance:
- **Resilience**: Automatic recovery mechanisms and state persistence
- **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
The project follows a modular architecture with clear separation of concerns:
```
DeepSea-Dashboard/
bitcoin-mining-dashboard/
├── App.py # Main application entry point
├── config.py # Configuration management
@ -185,12 +156,9 @@ DeepSea-Dashboard/
├── models.py # Data models
├── state_manager.py # Manager for persistent state
├── 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
│ ├── base.html # Base template with common elements
@ -198,7 +166,6 @@ DeepSea-Dashboard/
│ ├── dashboard.html # Main dashboard template
│ ├── workers.html # Workers dashboard template
│ ├── blocks.html # Bitcoin blocks template
│ ├── notifications.html # Notifications template
│ └── error.html # Error page template
├── static/ # Static assets
@ -208,24 +175,18 @@ DeepSea-Dashboard/
│ │ ├── workers.css # Workers page styles
│ │ ├── boot.css # Boot sequence styles
│ │ ├── blocks.css # Blocks page styles
│ │ ├── notifications.css # Notifications page styles
│ │ ├── error.css # Error page styles
│ │ ├── retro-refresh.css # Floating refresh bar styles
│ │ └── theme-toggle.css # Theme toggle styles
│ │ └── retro-refresh.css # Floating refresh bar styles
│ │
│ └── js/ # JavaScript files
│ ├── main.js # Main dashboard functionality
│ ├── workers.js # Workers page functionality
│ ├── blocks.js # Blocks page functionality
│ ├── notifications.js # Notifications functionality
│ ├── block-animation.js # Block mining animation
│ ├── BitcoinProgressBar.js # System monitor functionality
│ └── theme.js # Theme toggle functionality
│ └── BitcoinProgressBar.js # System monitor functionality
├── deployment_steps.md # Deployment guide
├── project_structure.md # Additional structure documentation
├── LICENSE.md # License information
└── logs/ # Application logs (generated at runtime)
└── project_structure.md # Additional structure documentation
```
For more detailed information on the architecture and component interactions, see [project_structure.md](project_structure.md).
@ -239,7 +200,6 @@ For optimal performance:
3. Use the system monitor to verify connection status
4. Access the health endpoint at `/api/health` for diagnostics
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
@ -248,6 +208,5 @@ Available under the MIT License. This is an independent project not affiliated w
## Acknowledgments
- Ocean.xyz mining pool for their service
- mempool.guide
- The open-source community for their contributions
- Bitcoin protocol developers

View File

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

View File

@ -12,13 +12,14 @@ CONFIG_FILE = "config.json"
def load_config():
"""
Load configuration from file or return defaults if file doesn't exist.
Returns:
dict: Configuration dictionary with settings
"""
default_config = {
"power_cost": 0.0,
"power_usage": 0.0,
"wallet": "yourwallethere",
"timezone": "America/Los_Angeles",
"network_fee": 0.0 # Add default network fee
"wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9"
}
if os.path.exists(CONFIG_FILE):
@ -26,12 +27,6 @@ def load_config():
with open(CONFIG_FILE, "r") as f:
config = json.load(f)
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
except Exception as e:
logging.error(f"Error loading config: {e}")
@ -40,28 +35,6 @@ def load_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):
"""
Save configuration to file.

View File

@ -12,25 +12,22 @@ import requests
from bs4 import BeautifulSoup
from models import OceanData, WorkerData, convert_to_ths
from config import get_timezone
class MiningDashboardService:
"""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.
Args:
power_cost (float): Cost of power in $ per kWh
power_usage (float): Power usage in watts
wallet (str): Bitcoin wallet address for Ocean.xyz
network_fee (float): Additional network fee percentage
"""
self.power_cost = power_cost
self.power_usage = power_usage
self.wallet = wallet
self.network_fee = network_fee
self.cache = {}
self.sats_per_btc = 100_000_000
self.previous_values = {}
@ -39,13 +36,13 @@ class MiningDashboardService:
def fetch_metrics(self):
"""
Fetch metrics from Ocean.xyz and other sources.
Returns:
dict: Mining metrics data
"""
# Add execution time tracking
start_time = time.time()
try:
with ThreadPoolExecutor(max_workers=2) as executor:
future_ocean = executor.submit(self.get_ocean_data)
@ -62,12 +59,12 @@ class MiningDashboardService:
return None
difficulty, network_hashrate, btc_price, block_count = btc_stats
# If we failed to get network hashrate, use a reasonable default to prevent division by zero
if network_hashrate is None:
logging.warning("Using default network hashrate")
network_hashrate = 500e18 # ~500 EH/s as a reasonable fallback
# If we failed to get BTC price, use a reasonable default
if btc_price is None:
logging.warning("Using default BTC price")
@ -82,25 +79,7 @@ class MiningDashboardService:
block_reward = 3.125
blocks_per_day = 86400 / 600
daily_btc_gross = hash_proportion * block_reward * blocks_per_day
# 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_btc_net = daily_btc_gross * (1 - 0.02 - 0.028)
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)
@ -133,11 +112,7 @@ class MiningDashboardService:
'block_number': block_count,
'network_hashrate': (network_hashrate / 1e18) if network_hashrate else None,
'difficulty': difficulty,
'daily_btc_gross': daily_btc_gross,
'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,
'daily_revenue': daily_revenue,
'daily_power_cost': daily_power_cost,
@ -153,16 +128,15 @@ class MiningDashboardService:
'last_block_time': ocean_data.last_block_time,
'total_last_share': ocean_data.total_last_share,
'blocks_found': ocean_data.blocks_found or "0",
'last_block_earnings': ocean_data.last_block_earnings,
'pool_fees_percentage': ocean_data.pool_fees_percentage,
'last_block_earnings': ocean_data.last_block_earnings
}
metrics['estimated_earnings_per_day_sats'] = int(round(estimated_earnings_per_day * self.sats_per_btc))
metrics['estimated_earnings_next_block_sats'] = int(round(estimated_earnings_next_block * 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 ---
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
metrics["server_start_time"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
metrics["server_timestamp"] = datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
metrics["server_start_time"] = datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
# Log execution time
execution_time = time.time() - start_time
@ -239,28 +213,15 @@ class MiningDashboardService:
latest_row = earnings_table.find('tr', class_='table-row')
if latest_row:
cells = latest_row.find_all('td', class_='table-cell')
if len(cells) >= 4: # Ensure there are enough cells for earnings and pool fees
if len(cells) >= 3:
earnings_text = cells[2].get_text(strip=True)
pool_fees_text = cells[3].get_text(strip=True)
# Parse earnings and pool fees
earnings_value = earnings_text.replace('BTC', '').strip()
pool_fees_value = pool_fees_text.replace('BTC', '').strip()
try:
# Convert earnings to BTC and sats
btc_earnings = float(earnings_value)
sats = int(round(btc_earnings * 100_000_000))
sats = int(round(btc_earnings * 100000000))
data.last_block_earnings = str(sats)
# Calculate percentage lost to pool fees
btc_pool_fees = float(pool_fees_value)
percentage_lost = (btc_pool_fees / btc_earnings) * 100 if btc_earnings > 0 else 0
data.pool_fees_percentage = round(percentage_lost, 2)
except Exception as e:
logging.error(f"Error converting earnings or calculating percentage: {e}")
except Exception:
data.last_block_earnings = earnings_value
data.pool_fees_percentage = None
except Exception as e:
logging.error(f"Error parsing earnings data: {e}")
@ -385,7 +346,7 @@ class MiningDashboardService:
try:
naive_dt = datetime.strptime(last_share_str, "%Y-%m-%d %H:%M")
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")
except Exception as e:
logging.error(f"Error converting last share time '{last_share_str}': {e}")
@ -512,48 +473,6 @@ class MiningDashboardService:
return difficulty, network_hashrate, btc_price, block_count
def get_all_worker_rows(self):
"""
Iterate through wpage parameter values to collect all worker table rows.
Limited to 10 pages to balance between showing enough workers and maintaining performance.
Returns:
list: A list of BeautifulSoup row elements containing worker data.
"""
all_rows = []
page_num = 0
max_pages = 10 # Limit to 10 pages of worker data
while page_num < max_pages: # Only fetch up to max_pages
url = f"https://ocean.xyz/stats/{self.wallet}?wpage={page_num}#workers-fulltable"
logging.info(f"Fetching worker data from: {url} (page {page_num+1} of max {max_pages})")
response = self.session.get(url, timeout=15)
if not response.ok:
logging.error(f"Error fetching page {page_num}: status code {response.status_code}")
break
soup = BeautifulSoup(response.text, 'html.parser')
workers_table = soup.find('tbody', id='workers-tablerows')
if not workers_table:
logging.debug(f"No workers table found on page {page_num}")
break
rows = workers_table.find_all("tr", class_="table-row")
if not rows:
logging.debug(f"No worker rows found on page {page_num}, stopping pagination")
break
logging.info(f"Found {len(rows)} worker rows on page {page_num}")
all_rows.extend(rows)
page_num += 1
if page_num >= max_pages:
logging.info(f"Reached maximum page limit ({max_pages}). Collected {len(all_rows)} worker rows total.")
else:
logging.info(f"Completed fetching all available worker data. Collected {len(all_rows)} worker rows from {page_num} pages.")
return all_rows
def get_worker_data(self):
"""
Get worker data from Ocean.xyz using multiple parsing strategies.
@ -663,6 +582,7 @@ class MiningDashboardService:
# Find total worker counts
workers_online = 0
workers_offline = 0
avg_acceptance_rate = 95.0 # Default value
# Iterate through worker rows in the table
for row in workers_table.find_all('tr', class_='table-row'):
@ -698,6 +618,7 @@ class MiningDashboardService:
"efficiency": 90.0, # Default efficiency
"last_share": "N/A",
"earnings": 0,
"acceptance_rate": 95.0, # Default acceptance rate
"power_consumption": 0,
"temperature": 0
}
@ -794,8 +715,8 @@ class MiningDashboardService:
worker["type"] = 'ASIC'
worker["model"] = 'MicroBT Whatsminer'
elif 'bitaxe' in lower_name or 'nerdqaxe' in lower_name:
worker["type"] = 'Bitaxe'
worker["model"] = 'BitAxe Gamma 601'
worker["type"] = 'FPGA'
worker["model"] = 'BitAxe FPGA Miner'
workers.append(worker)
@ -837,8 +758,9 @@ class MiningDashboardService:
'workers_online': workers_online,
'workers_offline': workers_offline,
'total_earnings': total_earnings,
'avg_acceptance_rate': avg_acceptance_rate,
'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")
@ -853,106 +775,204 @@ class MiningDashboardService:
def get_worker_data_alternative(self):
"""
Alternative implementation to get worker data from Ocean.xyz.
This version consolidates worker rows from all pages using the wpage parameter.
Uses a more focused approach to extract worker names and status.
Returns:
dict: Worker data dictionary with stats and list of workers.
dict: Worker data dictionary with stats and list of workers
"""
base_url = "https://ocean.xyz"
stats_url = f"{base_url}/stats/{self.wallet}"
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Cache-Control': 'no-cache'
}
try:
logging.info("Fetching worker data across multiple pages (alternative method)")
# Get all worker rows from every page
rows = self.get_all_worker_rows()
if not rows:
logging.error("No worker rows found across any pages")
logging.info(f"Fetching worker data from {stats_url} (alternative method)")
response = self.session.get(stats_url, headers=headers, timeout=15)
if not response.ok:
logging.error(f"Error fetching ocean worker data: status code {response.status_code}")
return None
soup = BeautifulSoup(response.text, 'html.parser')
# Save the HTML to a file for debugging if needed
try:
with open('debug_ocean_page.html', 'w', encoding='utf-8') as f:
f.write(soup.prettify())
logging.debug("Saved HTML to debug_ocean_page.html for inspection")
except Exception as e:
logging.warning(f"Could not save debug HTML: {e}")
# ---- Specialized Approach ----
# Look specifically for the workers table by characteristic selectors
workers_table = None
# First try to find table with workers-tablerows ID
workers_table = soup.find('tbody', id='workers-tablerows')
# If not found, try alternative selectors
if not workers_table:
# Try to find any table
tables = soup.find_all('table')
logging.debug(f"Found {len(tables)} tables on page")
# Look for a table that contains worker information
for table in tables:
# Look at the header to determine if this is the workers table
thead = table.find('thead')
if thead:
headers = [th.get_text(strip=True).lower() for th in thead.find_all('th')]
logging.debug(f"Table headers: {headers}")
# Check if this looks like a workers table by looking for common headers
worker_headers = ['worker', 'name', 'status', 'hashrate', 'share']
if any(header in ''.join(headers) for header in worker_headers):
logging.info("Found likely workers table by header content")
workers_table = table.find('tbody')
break
if not workers_table:
logging.error("Could not find workers table")
return None
# Debug: Dump all rows in the workers table
rows = workers_table.find_all('tr')
logging.info(f"Found {len(rows)} rows in workers table")
# Debug the first few rows
for i, row in enumerate(rows[:3]):
if i == 0: # First row special handling - likely contains headers or column info
cols = row.find_all(['td', 'th'])
col_texts = [col.get_text(strip=True) for col in cols]
logging.debug(f"First row columns: {col_texts}")
# Find workers by looking at each row in the table
workers = []
total_hashrate = 0
total_earnings = 0
workers_online = 0
workers_offline = 0
# List of invalid worker names (these are likely status labels)
invalid_names = ['online', 'offline', 'status', 'worker', 'total']
# Process each row from all pages
# Process each row in the table
for row_idx, row in enumerate(rows):
# Skip rows that look like headers or total
cells = row.find_all(['td', 'th'])
if not cells or len(cells) < 3:
continue
# Get the first cell text (likely worker name)
first_cell_text = cells[0].get_text(strip=True)
# Skip rows with invalid names or total rows
if first_cell_text.lower() in invalid_names:
continue
try:
worker_name = first_cell_text or f"Worker_{row_idx+1}"
# Extract hashrate and status from row
# --- Generate a valid worker name ---
worker_name = first_cell_text
# If name is empty or invalid, generate a fallback name based on row number
if not worker_name or worker_name.lower() in invalid_names:
worker_name = f"Worker_{row_idx+1}"
# Debug logging for extracted name
logging.debug(f"Extracted worker name: '{worker_name}'")
# This is likely a worker row - extract data
worker = {
"name": worker_name,
"status": "online", # Default assumption
"type": "ASIC",
"status": "online", # Default to online since most workers are online
"type": "ASIC", # Default type
"model": "Unknown",
"hashrate_60sec": 0,
"hashrate_60sec_unit": "TH/s",
"hashrate_3hr": 0,
"hashrate_3hr_unit": "TH/s",
"efficiency": 90.0,
"efficiency": 90.0, # Default
"last_share": "N/A",
"earnings": 0,
"acceptance_rate": 95.0, # Default
"power_consumption": 0,
"temperature": 0
}
# Extract status from second cell if available
# --- Extract status and other data ---
# For most tables, column 1 is status, 2 is last share, 3 is 60sec hashrate, 4 is 3hr hashrate, 5 is earnings
# Get status from second column if available
if len(cells) > 1:
status_text = cells[1].get_text(strip=True).lower()
worker["status"] = "online" if "online" in status_text else "offline"
status_cell = cells[1]
status_text = status_cell.get_text(strip=True).lower()
# Check if this cell actually contains status information
if 'online' in status_text or 'offline' in status_text:
worker["status"] = "online" if "online" in status_text else "offline"
else:
# If the second column doesn't contain status info, check cell contents for clues
for cell in cells:
cell_text = cell.get_text(strip=True).lower()
if 'online' in cell_text:
worker["status"] = "online"
break
elif 'offline' in cell_text:
worker["status"] = "offline"
break
# Update counters based on status
if worker["status"] == "online":
workers_online += 1
else:
workers_offline += 1
# Parse last share from third cell if available
if len(cells) > 2:
worker["last_share"] = cells[2].get_text(strip=True)
# Parse 60sec hashrate from fourth cell if available
if len(cells) > 3:
hashrate_60s_text = cells[3].get_text(strip=True)
try:
parts = hashrate_60s_text.split()
if parts:
worker["hashrate_60sec"] = float(parts[0])
if len(parts) > 1:
worker["hashrate_60sec_unit"] = parts[1]
except ValueError:
logging.warning(f"Could not parse 60-sec hashrate: {hashrate_60s_text}")
# Parse 3hr hashrate from fifth cell if available
if len(cells) > 4:
hashrate_3hr_text = cells[4].get_text(strip=True)
try:
parts = hashrate_3hr_text.split()
if parts:
worker["hashrate_3hr"] = float(parts[0])
if len(parts) > 1:
worker["hashrate_3hr_unit"] = parts[1]
# Normalize and add to total hashrate (using your convert_to_ths helper)
total_hashrate += convert_to_ths(worker["hashrate_3hr"], worker["hashrate_3hr_unit"])
except ValueError:
logging.warning(f"Could not parse 3hr hashrate: {hashrate_3hr_text}")
# Look for earnings in any cell containing 'btc'
# Parse last share time
last_share_idx = 2 # Typical position for last share
if len(cells) > last_share_idx:
last_share_cell = cells[last_share_idx]
worker["last_share"] = last_share_cell.get_text(strip=True)
# Parse hashrates
for i, cell in enumerate(cells):
cell_text = cell.get_text(strip=True)
# Look for hashrate patterns - numbers followed by H/s, TH/s, GH/s, etc.
hashrate_match = re.search(r'([\d\.]+)\s*([KMGTPE]?H/s)', cell_text, re.IGNORECASE)
if hashrate_match:
value = float(hashrate_match.group(1))
unit = hashrate_match.group(2)
# Assign to appropriate hashrate field based on position or content
if i == 3 or "60" in cell_text:
worker["hashrate_60sec"] = value
worker["hashrate_60sec_unit"] = unit
elif i == 4 or "3h" in cell_text:
worker["hashrate_3hr"] = value
worker["hashrate_3hr_unit"] = unit
# Add to total hashrate
total_hashrate += convert_to_ths(value, unit)
# Parse earnings from any cell that might contain BTC values
for cell in cells:
cell_text = cell.get_text(strip=True)
# Look for BTC pattern
if "btc" in cell_text.lower():
try:
# Extract the number part
earnings_match = re.search(r'([\d\.]+)', cell_text)
if earnings_match:
worker["earnings"] = float(earnings_match.group(1))
total_earnings += worker["earnings"]
except Exception:
except ValueError:
pass
# Set worker type based on name
# Set worker type based on name (if it can be inferred)
lower_name = worker["name"].lower()
if 'antminer' in lower_name:
worker["type"] = 'ASIC'
@ -961,33 +981,107 @@ class MiningDashboardService:
worker["type"] = 'ASIC'
worker["model"] = 'MicroBT Whatsminer'
elif 'bitaxe' in lower_name or 'nerdqaxe' in lower_name:
worker["type"] = 'Bitaxe'
worker["model"] = 'BitAxe Gamma 601'
if worker["name"].lower() not in invalid_names:
worker["type"] = 'FPGA'
worker["model"] = 'BitAxe FPGA Miner'
# Only add workers with valid data
if worker["name"] and worker["name"].lower() not in invalid_names:
workers.append(worker)
logging.debug(f"Added worker: {worker['name']}, status: {worker['status']}")
except Exception as e:
logging.error(f"Error parsing worker row: {e}")
import traceback
logging.error(traceback.format_exc())
continue
# If no valid workers were found, try one more approach - generate worker names
if not workers and len(rows) > 0:
logging.warning("No valid workers found, generating worker names based on row indices")
for row_idx, row in enumerate(rows):
# Skip first row (likely header)
if row_idx == 0:
continue
# Skip rows that look like totals
cells = row.find_all(['td', 'th'])
if not cells or len(cells) < 3:
continue
first_cell_text = cells[0].get_text(strip=True)
if first_cell_text.lower() == 'total':
continue
# Generate a worker
worker_name = f"Worker_{row_idx}"
# Basic worker data
worker = {
"name": worker_name,
"status": "online", # Default to online
"type": "ASIC", # Default type
"model": "Unknown",
"hashrate_60sec": 0,
"hashrate_60sec_unit": "TH/s",
"hashrate_3hr": row_idx * 50, # Generate some reasonable value
"hashrate_3hr_unit": "TH/s",
"efficiency": 90.0,
"last_share": "N/A",
"earnings": 0.00001 * row_idx,
"acceptance_rate": 95.0,
"power_consumption": 0,
"temperature": 0
}
workers.append(worker)
workers_online += 1
# Get daily sats from other elements on the page
daily_sats = 0
try:
# Look for earnings per day
earnings_elements = soup.find_all('div', text=lambda t: t and 'earnings per day' in t.lower())
for element in earnings_elements:
# Look for nearest span with a value
value_span = element.find_next('span')
if value_span:
value_text = value_span.get_text(strip=True)
try:
value_parts = value_text.split()
if value_parts:
btc_per_day = float(value_parts[0])
daily_sats = int(btc_per_day * self.sats_per_btc)
break
except (ValueError, IndexError):
pass
except Exception as e:
logging.error(f"Error parsing daily sats: {e}")
# Check if we found any workers
if not workers:
logging.error("No valid worker data parsed")
logging.warning("No workers found in the table")
return None
# Return worker stats dictionary
result = {
'workers': workers,
'total_hashrate': total_hashrate,
'hashrate_unit': 'TH/s',
'hashrate_unit': 'TH/s', # Always use TH/s for consistent display
'workers_total': len(workers),
'workers_online': workers_online,
'workers_offline': workers_offline,
'total_earnings': total_earnings,
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat()
'avg_acceptance_rate': 95.0, # Default value
'daily_sats': daily_sats,
'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 using alternative method")
return result
except Exception as e:
logging.error(f"Error in alternative worker data fetch: {e}")
import traceback
logging.error(traceback.format_exc())
return None

View File

@ -16,8 +16,8 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
1. Clone the repository:
```bash
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git
cd DeepSea-Dashboard
git clone https://github.com/yourusername/bitcoin-mining-dashboard.git
cd bitcoin-mining-dashboard
```
2. Create a virtual environment (recommended):
@ -36,12 +36,21 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
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
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

View File

@ -1,42 +0,0 @@
version: '3'
services:
redis:
image: redis:alpine
restart: unless-stopped
volumes:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
dashboard:
build: .
restart: unless-stopped
ports:
- "5000:5000"
environment:
- REDIS_URL=redis://redis:6379
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS
- POWER_COST=0
- POWER_USAGE=0
- NETWORK_FEE=0
- TIMEZONE=America/Los_Angeles
- LOG_LEVEL=INFO
volumes:
- ./logs:/app/logs
depends_on:
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
redis_data:

View File

@ -1,4 +1,4 @@
FROM python:3.9.18-slim
FROM python:3.9-slim
WORKDIR /app
@ -17,8 +17,8 @@ COPY *.py .
COPY config.json .
COPY setup.py .
# Create all necessary directories in one command
RUN mkdir -p static/css static/js templates logs /app/logs
# Create necessary directories
RUN mkdir -p static/css static/js templates logs
# Copy static files and templates
COPY static/css/*.css static/css/
@ -29,7 +29,7 @@ COPY templates/*.html templates/
RUN python setup.py
# Run the minifier to process HTML templates
RUN python minify.py
# RUN python minify.py
# Create a non-root user for better security
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
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
USER appuser
@ -46,6 +49,7 @@ EXPOSE 5000
# Set environment variables
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
ENV PYTHON_UNBUFFERED=1
# Add healthcheck
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \

246
minify.py
View File

@ -1,246 +0,0 @@
#!/usr/bin/env python3
import os
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():
"""Minify JavaScript files."""
js_dir = 'static/js'
min_dir = os.path.join(js_dir, 'min')
os.makedirs(min_dir, exist_ok=True)
minified_count = 0
skipped_count = 0
for js_file in os.listdir(js_dir):
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
try:
input_path = os.path.join(js_dir, js_file)
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
# 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 {js_file} (already up to date)")
skipped_count += 1
continue
with open(input_path, 'r', encoding='utf-8') as f:
js_content = f.read()
# Minify the content
minified = jsmin.jsmin(js_content)
# Write minified content
with open(output_path, 'w', encoding='utf-8') as f:
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
except Exception as e:
logger.error(f"Error processing {js_file}: {e}")
logger.info(f"JavaScript minification: {minified_count} files minified, {skipped_count} files skipped")
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__":
main()

View File

@ -2,13 +2,11 @@
Data models for the Bitcoin Mining Dashboard.
"""
from dataclasses import dataclass
from typing import Optional, Dict, List, Union, Any
import logging
@dataclass
class OceanData:
"""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_unit: str = None
hashrate_24hr: float = None
@ -33,49 +31,13 @@ class OceanData:
blocks_found: str = None
total_last_share: str = "N/A"
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
class WorkerData:
"""Data structure for individual worker information."""
name: str = None
status: str = "offline"
type: str = "ASIC" # ASIC or Bitaxe
type: str = "ASIC" # ASIC or FPGA
model: str = "Unknown"
hashrate_60sec: float = 0
hashrate_60sec_unit: str = "TH/s"
@ -88,61 +50,6 @@ class WorkerData:
power_consumption: 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):
"""
Convert any hashrate unit to TH/s equivalent.

View File

@ -1,548 +0,0 @@
# notification_service.py
import logging
import json
import time
import uuid
from datetime import datetime, timedelta
from enum import Enum
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):
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
class NotificationCategory(Enum):
HASHRATE = "hashrate"
BLOCK = "block"
WORKER = "worker"
EARNINGS = "earnings"
SYSTEM = "system"
class NotificationService:
"""Service for managing mining dashboard notifications."""
def __init__(self, state_manager):
"""Initialize with state manager for persistence."""
self.state_manager = state_manager
self.notifications = []
self.daily_stats_time = "00:00:00" # When to post daily stats (midnight)
self.last_daily_stats = None
self.max_notifications = 100 # Maximum number to store
self.last_block_height = None # Track the last seen block height
self.last_payout_notification_time = None # Track the last payout notification time
self.last_estimated_payout_time = None # Track the last estimated payout time
# Load existing notifications from state
self._load_notifications()
# Load last block height from state
self._load_last_block_height()
def _get_redis_value(self, key: str, default: Any = None) -> Any:
"""Generic method to retrieve values from Redis."""
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:
stored_notifications = self.state_manager.get_notifications()
if stored_notifications:
self.notifications = stored_notifications
logging.info(f"[NotificationService] 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:
logging.error(f"[NotificationService] Error loading notifications: {e}")
self.notifications = [] # Ensure we have a valid list
def _load_last_block_height(self) -> None:
"""Load last block height from persistent storage."""
try:
self.last_block_height = self._get_redis_value("last_block_height")
if self.last_block_height:
logging.info(f"[NotificationService] Loaded last block height from storage: {self.last_block_height}")
else:
logging.info("[NotificationService] No last block height found, starting with None")
except Exception as e:
logging.error(f"[NotificationService] Error loading last block height: {e}")
def _save_last_block_height(self) -> None:
"""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:
# Sort by timestamp before pruning to ensure we keep the most recent
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.state_manager.save_notifications(self.notifications)
logging.info(f"[NotificationService] Saved {len(self.notifications)} notifications")
except Exception as e:
logging.error(f"[NotificationService] Error saving notifications: {e}")
def add_notification(self,
message: str,
level: NotificationLevel = NotificationLevel.INFO,
category: NotificationCategory = NotificationCategory.SYSTEM,
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
"""
Add a new notification.
Args:
message (str): Notification message text
level (NotificationLevel): Severity level
category (NotificationCategory): Classification category
data (dict, optional): Additional data for the notification
Returns:
dict: The created notification
"""
notification = {
"id": str(uuid.uuid4()),
"timestamp": datetime.now().isoformat(),
"message": message,
"level": level.value,
"category": category.value,
"read": False
}
if data:
notification["data"] = data
self.notifications.append(notification)
self._save_notifications()
logging.info(f"[NotificationService] Added notification: {message}")
return notification
def get_notifications(self,
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.
Args:
limit (int): Maximum number to return
offset (int): Starting offset for pagination
unread_only (bool): Only return unread notifications
category (str): Filter by category
level (str): Filter by level
Returns:
list: Filtered notifications
"""
# Apply all filters in a single pass
filtered = [
n for n in self.notifications
if (not unread_only or not n.get("read", False)) and
(not category or n.get("category") == category) and
(not level or n.get("level") == level)
]
# Sort by timestamp (newest first)
filtered = sorted(filtered, key=lambda n: n.get("timestamp", ""), reverse=True)
# Apply pagination
return filtered[offset:offset + limit]
def get_unread_count(self) -> int:
"""Get count of unread notifications."""
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:
"""
Mark notification(s) as read.
Args:
notification_id (str, optional): ID of specific notification to mark read,
or None to mark all as read
Returns:
bool: True if successful
"""
if notification_id:
# Mark specific notification as read
for n in self.notifications:
if n.get("id") == notification_id:
n["read"] = True
logging.info(f"[NotificationService] Marked notification {notification_id} as read")
break
else:
# Mark all as read
for n in self.notifications:
n["read"] = True
logging.info(f"[NotificationService] Marked all {len(self.notifications)} notifications as read")
self._save_notifications()
return True
def delete_notification(self, notification_id: str) -> bool:
"""
Delete a specific notification.
Args:
notification_id (str): ID of notification to delete
Returns:
bool: True if successful
"""
original_count = len(self.notifications)
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()
return deleted > 0
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None) -> int:
"""
Clear notifications with optimized filtering.
Args:
category (str, optional): Only clear specific category
older_than_days (int, optional): Only clear notifications older than this
Returns:
int: Number of notifications cleared
"""
original_count = len(self.notifications)
cutoff_date = None
if older_than_days:
cutoff_date = datetime.now() - timedelta(days=older_than_days)
# Apply filters in a single pass
self.notifications = [
n for n in self.notifications
if (not category or n.get("category") != category) and
(not cutoff_date or datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date)
]
cleared_count = original_count - len(self.notifications)
if cleared_count > 0:
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
self._save_notifications()
return cleared_count
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.
Args:
current_metrics: Current system metrics
previous_metrics: Previous system metrics for comparison
Returns:
list: Newly created notifications
"""
new_notifications = []
try:
# Skip if no metrics
if not current_metrics:
logging.warning("[NotificationService] No current metrics available, skipping notification checks")
return new_notifications
# Check for block updates (using persistent storage)
last_block_height = current_metrics.get("last_block_height")
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:
logging.info(f"[NotificationService] Block change detected: {self.last_block_height} -> {last_block_height}")
block_notification = self._generate_block_notification(current_metrics)
if block_notification:
new_notifications.append(block_notification)
# Always update the stored last block height when it changes
if self.last_block_height != last_block_height:
self.last_block_height = last_block_height
self._save_last_block_height()
# Regular comparison with previous metrics
if previous_metrics:
# Check for daily stats
if self._should_post_daily_stats():
stats_notification = self._generate_daily_stats(current_metrics)
if stats_notification:
new_notifications.append(stats_notification)
# Check for significant hashrate drop
hashrate_notification = self._check_hashrate_change(current_metrics, previous_metrics)
if hashrate_notification:
new_notifications.append(hashrate_notification)
# Check for earnings and payout progress
earnings_notification = self._check_earnings_progress(current_metrics, previous_metrics)
if earnings_notification:
new_notifications.append(earnings_notification)
return new_notifications
except Exception as e:
logging.error(f"[NotificationService] Error generating notifications: {e}")
error_notification = self.add_notification(
f"Error generating notifications: {str(e)}",
level=NotificationLevel.ERROR,
category=NotificationCategory.SYSTEM
)
return [error_notification]
def _should_post_daily_stats(self) -> bool:
"""Check if it's time to post daily stats with improved clarity."""
now = datetime.now()
# Only proceed if we're in the target hour and within first 5 minutes
if now.hour != DEFAULT_TARGET_HOUR or now.minute >= NOTIFICATION_WINDOW_MINUTES:
return False
# If we have a last_daily_stats timestamp, check if it's a different day
if self.last_daily_stats and now.date() <= self.last_daily_stats.date():
return False
# All conditions met, update timestamp and return True
logging.info(f"[NotificationService] Posting daily stats at {now.hour}:{now.minute}")
self.last_daily_stats = now
return True
def _generate_daily_stats(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Generate daily stats notification."""
try:
if not metrics:
logging.warning("[NotificationService] No metrics available for daily stats")
return None
# Format hashrate with appropriate unit
hashrate_24hr = metrics.get("hashrate_24hr", 0)
hashrate_unit = metrics.get("hashrate_24hr_unit", "TH/s")
# Format daily earnings
daily_mined_sats = metrics.get("daily_mined_sats", 0)
daily_profit_usd = metrics.get("daily_profit_usd", 0)
# Build message
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined (${daily_profit_usd:.2f})"
# Add notification
logging.info(f"[NotificationService] Generating daily stats notification: {message}")
return self.add_notification(
message,
level=NotificationLevel.INFO,
category=NotificationCategory.HASHRATE,
data={
"hashrate": hashrate_24hr,
"unit": hashrate_unit,
"daily_sats": daily_mined_sats,
"daily_profit": daily_profit_usd
}
)
except Exception as e:
logging.error(f"[NotificationService] Error generating daily stats notification: {e}")
return None
def _generate_block_notification(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Generate notification for a new block found."""
try:
last_block_height = metrics.get("last_block_height", "Unknown")
last_block_earnings = metrics.get("last_block_earnings", "0")
logging.info(f"[NotificationService] 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"
return self.add_notification(
message,
level=NotificationLevel.SUCCESS,
category=NotificationCategory.BLOCK,
data={
"block_height": last_block_height,
"earnings": last_block_earnings
}
)
except Exception as e:
logging.error(f"[NotificationService] Error generating block notification: {e}")
return None
def _parse_numeric_value(self, value_str: Any) -> float:
"""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."""
try:
# Get 10min hashrate values
current_10min = current.get("hashrate_10min", 0)
previous_10min = previous.get("hashrate_10min", 0)
# Log what we're comparing
logging.debug(f"[NotificationService] Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}")
# Skip if values are missing
if not current_10min or not previous_10min:
logging.debug("[NotificationService] Skipping hashrate check - missing values")
return None
# Parse values consistently
current_value = self._parse_numeric_value(current_10min)
previous_value = self._parse_numeric_value(previous_10min)
logging.debug(f"[NotificationService] Converted 10min hashrates - current: {current_value}, previous: {previous_value}")
# Skip if previous was zero (prevents division by zero)
if previous_value == 0:
logging.debug("[NotificationService] Skipping hashrate check - previous was zero")
return None
# Calculate percentage change
percent_change = ((current_value - previous_value) / previous_value) * 100
logging.debug(f"[NotificationService] 10min hashrate change: {percent_change:.1f}%")
# Significant decrease
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease"
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
return self.add_notification(
message,
level=NotificationLevel.WARNING,
category=NotificationCategory.HASHRATE,
data={
"previous": previous_value,
"current": current_value,
"change": percent_change,
"timeframe": "10min"
}
)
# Significant increase
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
message = f"10min hashrate increase detected: {percent_change:.1f}% increase"
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
return self.add_notification(
message,
level=NotificationLevel.SUCCESS,
category=NotificationCategory.HASHRATE,
data={
"previous": previous_value,
"current": current_value,
"change": percent_change,
"timeframe": "10min"
}
)
return None
except Exception as e:
logging.error(f"[NotificationService] Error checking hashrate change: {e}")
return None
def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""Check for significant earnings progress or payout approach."""
try:
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0"))
# Check if approaching payout
if current.get("est_time_to_payout"):
est_time = current.get("est_time_to_payout")
# If estimated time is a number of days
if est_time.isdigit() or (est_time[0] == '-' and est_time[1:].isdigit()):
days = int(est_time)
if 0 < days <= 1:
if self._should_send_payout_notification():
message = f"Payout approaching! Estimated within 1 day"
self.last_payout_notification_time = datetime.now()
return self.add_notification(
message,
level=NotificationLevel.SUCCESS,
category=NotificationCategory.EARNINGS,
data={"days_to_payout": days}
)
# If it says "next block"
elif "next block" in est_time.lower():
if self._should_send_payout_notification():
message = f"Payout expected with next block!"
self.last_payout_notification_time = datetime.now()
return self.add_notification(
message,
level=NotificationLevel.SUCCESS,
category=NotificationCategory.EARNINGS,
data={"payout_imminent": True}
)
# Check for payout (unpaid balance reset)
if previous.get("unpaid_earnings"):
previous_unpaid = self._parse_numeric_value(previous.get("unpaid_earnings", "0"))
# If balance significantly decreased, likely a payout occurred
if previous_unpaid > 0 and current_unpaid < previous_unpaid * 0.5:
message = f"Payout received! Unpaid balance reset from {previous_unpaid} to {current_unpaid} BTC"
return self.add_notification(
message,
level=NotificationLevel.SUCCESS,
category=NotificationCategory.EARNINGS,
data={
"previous_balance": previous_unpaid,
"current_balance": current_unpaid,
"payout_amount": previous_unpaid - current_unpaid
}
)
return None
except Exception as e:
logging.error(f"[NotificationService] Error checking earnings progress: {e}")
return None
def _should_send_payout_notification(self) -> bool:
"""Check if enough time has passed since the last payout notification."""
if self.last_payout_notification_time is None:
return True
time_since_last_notification = datetime.now() - self.last_payout_notification_time
return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS

View File

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

View File

@ -19,4 +19,3 @@ urllib3==2.0.7
idna==3.4
certifi==2023.7.22
six==1.16.0
jsmin==3.0.1

View File

@ -43,7 +43,7 @@ except ImportError:
DIRECTORIES = [
'static/css',
'static/js',
'static/js/min', # For minified JS files
'static/img',
'templates',
'logs',
'data' # For temporary data storage
@ -59,16 +59,13 @@ FILE_MAPPINGS = {
'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',
'block-animation.js': 'static/js/block-animation.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',
@ -77,16 +74,13 @@ FILE_MAPPINGS = {
'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
"wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9"
}
def parse_arguments():
@ -96,13 +90,9 @@ def parse_arguments():
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():
@ -168,52 +158,6 @@ def move_files(force=False):
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.
@ -282,19 +226,6 @@ def create_config(args):
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:
@ -308,9 +239,7 @@ def create_config(args):
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']}")
logger.info(f" └── Power usage: {config['power_usage']} watts")
return True
@ -494,11 +423,6 @@ def main():
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()

View File

@ -7,7 +7,6 @@ import time
import gc
import threading
import redis
from config import get_timezone
# Global variables for arrow history, legacy hashrate history, and a log of full metrics snapshots.
arrow_history = {} # stored per second
@ -94,7 +93,7 @@ class StateManager:
arrow_history[key] = [
{"time": entry.get("t", ""),
"value": entry.get("v", 0),
"arrow": entry.get("a", "")} # Use saved arrow value
"arrow": ""} # Default empty arrow
for entry in values
]
@ -146,10 +145,10 @@ class StateManager:
for key, values in arrow_history.items():
if isinstance(values, list) and values:
# Only store recent history (last 2 hours)
recent_values = values[-180:] if len(values) > 180 else values
# Use shorter field names and preserve arrow directions
recent_values = values[-120:] if len(values) > 120 else values
# Use shorter field names and remove unnecessary fields
compact_arrow_history[key] = [
{"t": entry["time"], "v": entry["value"], "a": entry["arrow"]}
{"t": entry["time"], "v": entry["value"]}
for entry in recent_values
]
@ -328,7 +327,7 @@ class StateManager:
from datetime import datetime
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:
for key in arrow_keys:
@ -344,7 +343,6 @@ class StateManager:
try:
previous_val = arrow_history[key][-1]["value"]
previous_unit = arrow_history[key][-1].get("unit", "")
previous_arrow = arrow_history[key][-1].get("arrow", "") # Get previous arrow
# Use the convert_to_ths function to normalize both values before comparison
if key.startswith("hashrate") and current_unit:
@ -352,44 +350,23 @@ class StateManager:
norm_curr_val = convert_to_ths(float(current_val), current_unit)
norm_prev_val = convert_to_ths(float(previous_val), previous_unit if previous_unit else "th/s")
# Lower the threshold to 0.05% for more sensitivity
if norm_curr_val > norm_prev_val * 1.0001:
if norm_curr_val > norm_prev_val * 1.01: # 1% threshold to avoid minor fluctuations
arrow = ""
elif norm_curr_val < norm_prev_val * 0.9999:
elif norm_curr_val < norm_prev_val * 0.99: # 1% threshold
arrow = ""
else:
arrow = previous_arrow # Preserve previous arrow if change is insignificant
else:
# For non-hashrate values or when units are missing
# Try to convert to float for comparison
try:
curr_float = float(current_val)
prev_float = float(previous_val)
# Lower the threshold to 0.05% for more sensitivity
if curr_float > prev_float * 1.0001:
arrow = ""
elif curr_float < prev_float * 0.9999:
arrow = ""
else:
arrow = previous_arrow # Preserve previous arrow
except (ValueError, TypeError):
# If values can't be converted to float, compare directly
if current_val != previous_val:
arrow = "" if current_val > previous_val else ""
else:
arrow = previous_arrow # Preserve previous arrow
if float(current_val) > float(previous_val) * 1.01:
arrow = ""
elif float(current_val) < float(previous_val) * 0.99:
arrow = ""
except Exception as e:
logging.error(f"Error calculating arrow for {key}: {e}")
# Keep previous arrow on error instead of empty string
if arrow_history[key] and arrow_history[key][-1].get("arrow"):
arrow = arrow_history[key][-1]["arrow"]
if key not in arrow_history:
arrow_history[key] = []
if not arrow_history[key] or arrow_history[key][-1]["time"] != current_second:
# Create new entry
entry = {
"time": current_second,
"value": current_val,
@ -401,11 +378,8 @@ class StateManager:
arrow_history[key].append(entry)
else:
# Update existing entry
arrow_history[key][-1]["value"] = current_val
# Only update arrow if it's not empty - this preserves arrows between changes
if arrow:
arrow_history[key][-1]["arrow"] = arrow
arrow_history[key][-1]["arrow"] = arrow
# Update unit if available
if current_unit:
arrow_history[key][-1]["unit"] = current_unit
@ -425,9 +399,6 @@ class StateManager:
# Sort by time to ensure chronological order
aggregated_history[key] = sorted(list(minute_groups.values()),
key=lambda x: x["time"])
# Only keep the most recent 60 data points for the graph display
aggregated_history[key] = aggregated_history[key][-60:] if len(aggregated_history[key]) > 60 else aggregated_history[key]
metrics["arrow_history"] = aggregated_history
metrics["history"] = hashrate_history
@ -437,33 +408,3 @@ class StateManager:
# Cap the metrics log to three hours worth (180 entries)
if len(metrics_log) > MAX_HISTORY_ENTRIES:
metrics_log = metrics_log[-MAX_HISTORY_ENTRIES:]
def save_notifications(self, notifications):
"""Save notifications to persistent storage."""
try:
# If we have Redis, use it
if self.redis_client:
notifications_json = json.dumps(notifications)
self.redis_client.set("dashboard_notifications", notifications_json)
return True
else:
# Otherwise just keep in memory
return True
except Exception as e:
logging.error(f"Error saving notifications: {e}")
return False
def get_notifications(self):
"""Retrieve notifications from persistent storage."""
try:
# If we have Redis, use it
if self.redis_client:
notifications_json = self.redis_client.get("dashboard_notifications")
if notifications_json:
return json.loads(notifications_json)
# Return empty list if not found or no Redis
return []
except Exception as e:
logging.error(f"Error retrieving notifications: {e}")
return []

View File

@ -57,10 +57,6 @@
flex-direction: column;
}
.stat-item strong {
color: #f7931a; /* Use the Bitcoin orange color for labels */
}
/* Blocks grid */
.blocks-container {
overflow-x: auto;
@ -112,6 +108,7 @@
font-size: 1.2rem;
font-weight: bold;
color: var(--primary-color);
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
}
.block-time {
@ -141,22 +138,27 @@
.block-info-value.yellow {
color: #ffd700;
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
}
.block-info-value.green {
color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
}
.block-info-value.blue {
color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
}
.block-info-value.white {
color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
.block-info-value.red {
color: #ff5555;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
}
/* Loader */
@ -215,6 +217,7 @@
padding: 0.5rem 1rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite;
font-family: var(--header-font);
display: flex;
@ -235,6 +238,7 @@
.block-modal-close:hover,
.block-modal-close:focus {
color: #ffa500;
text-shadow: 0 0 10px rgba(255, 165, 0, 0.8);
}
.block-modal-body {
@ -257,6 +261,7 @@
font-size: 1.1rem;
color: var(--primary-color);
margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
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 */
body {
background: linear-gradient(135deg, #121212, #000000);
@ -447,6 +8,7 @@ body {
margin: 0;
padding: 10px;
overflow-x: hidden;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.4);
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
@ -517,26 +79,32 @@ body::before {
/* Neon-inspired color classes */
.green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.blue {
color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
}
.yellow {
color: #ffd700 !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
}
.white {
color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
}
.red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.magenta {
color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
}
/* Bitcoin Logo styling with extra neon border */
@ -575,7 +143,6 @@ body::before {
font-size: 16px;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
transition: all 0.2s ease;
z-index: 50; /* Lower z-index value */
}
#skip-button:hover {
@ -583,24 +150,6 @@ body::before {
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-container {
display: none;
@ -610,6 +159,7 @@ body::before {
#prompt-text {
color: #f7931a;
margin-right: 5px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline;
}
@ -625,6 +175,7 @@ body::before {
height: 33px;
padding: 0;
margin: 0;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline-block;
vertical-align: top;
}
@ -652,6 +203,7 @@ body::before {
#loading-message {
text-align: center;
margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
}
#debug-info {

View File

@ -1,85 +1,14 @@
.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 */
:root {
--bg-color: #0a0a0a;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #1a1a1a);
--primary-color: #f7931a;
--accent-color: #00ffff;
--text-color: #ffffff;
--card-padding: 0.5rem;
--text-size-base: 16px;
--terminal-font: 'VT323', monospace;
--header-font: 'Orbitron', sans-serif;
--text-transform: uppercase;
--bg-color: #0a0a0a;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #1a1a1a);
--primary-color: #f7931a;
--accent-color: #00ffff;
--text-color: #ffffff;
--card-padding: 0.5rem;
--text-size-base: 16px;
--terminal-font: 'VT323', monospace;
--header-font: 'Orbitron', sans-serif;
}
@media (min-width: 768px) {
@ -117,12 +46,11 @@ body::before {
}
body {
background: var(--bg-gradient);
color: var(--text-color);
padding-top: 0.5rem;
font-size: var(--text-size-base);
font-family: var(--terminal-font);
text-transform: uppercase;
background: var(--bg-gradient);
color: var(--text-color);
padding-top: 0.5rem;
font-size: var(--text-size-base);
font-family: var(--terminal-font);
}
h1 {
@ -131,6 +59,7 @@ h1 {
color: var(--primary-color);
font-family: var(--header-font);
letter-spacing: 1px;
text-shadow: 0 0 10px var(--primary-color);
animation: flicker 4s infinite;
}
@ -178,23 +107,16 @@ h1 {
right: 10px;
color: grey;
text-decoration: none;
font-size: 0.7rem; /* Decreased font size */
padding: 5px 10px; /* Add padding for a larger clickable area */
transition: background-color 0.3s ease; /* Optional: Add hover effect */
font-size: 0.9rem;
text-shadow: 0 0 5px grey;
}
#topRightLink:hover {
background-color: rgba(255, 255, 255, 0.1); /* Optional: Highlight on hover */
}
/* Card styles */
.card,
.card-header,
.card-body,
.card-footer {
border-radius: 0 !important;
text-transform: uppercase;
}
/* Enhanced card with scanlines */
@ -235,6 +157,7 @@ h1 {
padding: 0.3rem 0.5rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite;
font-family: var(--header-font);
}
@ -256,6 +179,7 @@ h1 {
border-radius: 5px;
z-index: 9999;
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);
}
@ -300,8 +224,8 @@ h1 {
border-radius: 50%;
margin-left: 0.5em;
position: relative;
top: -1px;
animation: glow 3s infinite;
top: -2px;
animation: glow 1s infinite;
box-shadow: 0 0 10px #32CD32, 0 0 20px #32CD32;
}
@ -311,16 +235,14 @@ h1 {
}
.offline-dot {
display: inline-block;
width: 8px;
height: 8px;
background: red;
border-radius: 50%;
margin-left: 0.5em;
position: relative;
top: -1px;
animation: glow 3s infinite;
box-shadow: 0 0 10px red, 0 0 20px red !important;
display: inline-block;
width: 8px;
height: 8px;
background: red;
border-radius: 50%;
margin-left: 0.5em;
animation: glowRed 1s infinite;
box-shadow: 0 0 10px red, 0 0 20px red;
}
@keyframes glowRed {
@ -331,48 +253,58 @@ h1 {
/* Color utility classes */
.green-glow, .status-green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.red-glow, .status-red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.yellow-glow {
color: #ffd700 !important;
text-shadow: 0 0 6px #ffd700, 0 0 12px #ffd700;
}
.blue-glow {
color: #00dfff !important;
text-shadow: 0 0 6px #00dfff, 0 0 12px #00dfff;
}
.white-glow {
color: #ffffff !important;
text-shadow: 0 0 6px #ffffff, 0 0 12px #ffffff;
}
/* Basic color classes for backward compatibility */
.green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.blue {
color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
}
.yellow {
color: #ffd700 !important;
font-weight: normal !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
}
.white {
color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
}
.red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.magenta {
color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
}
/* Bitcoin Progress Bar Styles */
@ -455,6 +387,7 @@ h1 {
font-size: 1rem;
color: var(--primary-color);
margin-top: 0.3rem;
text-shadow: 0 0 5px var(--primary-color);
text-align: center;
width: 100%;
}
@ -485,17 +418,3 @@ h1 {
margin-bottom: 0.5rem;
}
}
/* Navigation badges for notifications */
.nav-badge {
background-color: var(--primary-color);
color: var(--bg-color);
border-radius: 10px;
font-size: 0.7rem;
padding: 1px 5px;
min-width: 16px;
text-align: center;
display: none;
margin-left: 5px;
vertical-align: middle;
}

View File

@ -85,6 +85,7 @@
.chevron {
font-size: 0.8rem;
position: relative;
top: 3px;
}
/* Refresh timer container */
@ -113,6 +114,7 @@
.metric-value {
color: var(--text-color);
font-weight: bold;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
/* Yellow color family (BTC price, sats metrics, time to payout) */
@ -124,6 +126,7 @@
#estimated_rewards_in_window_sats,
#est_time_to_payout {
color: #ffd700;
text-shadow: 0 0 6px rgba(255, 215, 0, 0.6);
}
/* Green color family (profits, earnings) */
@ -132,11 +135,13 @@
#daily_profit_usd,
#monthly_profit_usd {
color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
}
/* Red color family (costs) */
#daily_power_cost {
color: #ff5555 !important;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
}
/* White metrics (general stats) */
@ -147,19 +152,21 @@
#workers_hashing,
#last_share,
#blocks_found,
#last_block_height,
#pool_fees_percentage {
color: #ffffff;
#last_block_height {
color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
/* Blue metrics (time data) */
#last_block_time {
color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
}
.card-body strong {
color: var(--primary-color);
margin-right: 0.25rem;
text-shadow: 0 0 2px var(--primary-color);
}
.card-body p {
@ -180,68 +187,13 @@
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);
text-transform: uppercase;
}
/* Add bottom padding to accommodate minimized system monitor */
.container-fluid {
padding-bottom: 60px !important; /* Enough space for minimized monitor */
}
/* Add these styles to dashboard.css */
@keyframes pulse-block-marker {
0% {
transform: translate(-50%, -50%) rotate(45deg) scale(1);
opacity: 1;
}
50% {
transform: translate(-50%, -50%) rotate(45deg) scale(1.3);
opacity: 0.8;
}
100% {
transform: translate(-50%, -50%) rotate(45deg) scale(1);
opacity: 1;
}
}
.chart-container-relative {
position: relative;
}
/* Styling for optimal fee indicator */
.fee-star {
color: gold;
margin-left: 4px;
font-size: 1.2em;
vertical-align: middle;
}
.datum-label {
color: #ffffff; /* White color */
font-size: 0.95em;
font-weight: bold;
text-transform: uppercase;
margin-left: 4px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 3px;
letter-spacing: 2px;
}
/* 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;
/* Ensure last card has proper margin to avoid being hidden */
#payoutMiscCard {
margin-bottom: 60px;
}

View File

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

View File

@ -1,327 +0,0 @@
/* notifications.css */
/* Notification Controls */
.notification-controls {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 10px;
}
.full-timestamp {
font-size: 0.8em;
color: #888;
}
.filter-buttons {
display: flex;
flex-wrap: wrap;
gap: 5px;
}
.filter-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 5px 10px;
font-family: var(--terminal-font);
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
}
.filter-button:hover {
background-color: rgba(247, 147, 26, 0.2);
}
.filter-button.active {
background-color: var(--primary-color);
color: var(--bg-color);
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
.notification-actions {
display: flex;
gap: 5px;
align-items: center;
text-transform: uppercase;
}
.action-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 6px 12px;
font-family: var(--terminal-font);
cursor: pointer;
transition: all 0.2s ease;
min-width: 80px; /* Set a minimum width to prevent text cutoff */
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 0.9rem; /* Slightly smaller font */
line-height: 1;
text-transform: uppercase;
}
.action-button:hover {
background-color: rgba(247, 147, 26, 0.2);
}
.action-button.danger {
border-color: #ff5555;
color: #ff5555;
}
.action-button.danger:hover {
background-color: rgba(255, 85, 85, 0.2);
}
/* Card header with unread badge */
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.unread-badge {
background-color: var(--primary-color);
color: var(--bg-color);
padding: 2px 8px;
border-radius: 10px;
font-size: 0.8rem;
min-width: 25px;
text-align: center;
}
.unread-badge:empty {
display: none;
}
/* Notifications Container */
#notifications-container {
min-height: 200px;
position: relative;
}
.loading-message {
text-align: center;
padding: 20px;
color: #888;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #888;
}
.empty-state i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.5;
}
/* Notification Item */
.notification-item {
display: flex;
padding: 12px;
border-bottom: 1px solid rgba(247, 147, 26, 0.2);
transition: background-color 0.2s ease;
position: relative;
background-color: rgba(0, 0, 0, 0.15);
}
.notification-item:hover {
background-color: rgba(247, 147, 26, 0.05);
}
.notification-item[data-read="true"] {
opacity: 0.6;
}
.notification-item[data-level="success"] {
border-left: 3px solid #32CD32;
}
.notification-item[data-level="info"] {
border-left: 3px solid #00dfff;
}
.notification-item[data-level="warning"] {
border-left: 3px solid #ffd700;
}
.notification-item[data-level="error"] {
border-left: 3px solid #ff5555;
}
.notification-icon {
flex: 0 0 40px;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
}
.notification-item[data-level="success"] .notification-icon i {
color: #32CD32;
}
.notification-item[data-level="info"] .notification-icon i {
color: #00dfff;
}
.notification-item[data-level="warning"] .notification-icon i {
color: #ffd700;
}
.notification-item[data-level="error"] .notification-icon i {
color: #ff5555;
}
.notification-content {
flex: 1;
padding: 0 15px;
}
.notification-message {
margin-bottom: 5px;
word-break: break-word;
color: white;
}
.notification-meta {
font-size: 0.8rem;
color: #888;
display: flex;
gap: 15px;
}
.notification-category {
text-transform: uppercase;
font-size: 0.7rem;
color: #aaa;
}
.notification-actions {
flex: 0 0 80px;
display: flex;
align-items: center;
justify-content: flex-end;
gap: 5px;
}
.notification-actions button {
background: none;
border: none;
color: #888;
cursor: pointer;
transition: color 0.2s ease;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 3px;
}
.mark-read-button:hover {
color: #32CD32;
background-color: rgba(50, 205, 50, 0.1);
}
.delete-button:hover {
color: #ff5555;
background-color: rgba(255, 85, 85, 0.1);
}
/* Pagination */
.pagination-controls {
margin-top: 15px;
text-align: center;
}
.load-more-button {
background-color: var(--bg-color);
border: 1px solid var(--primary-color);
color: var(--primary-color);
padding: 5px 15px;
font-family: var(--terminal-font);
cursor: pointer;
transition: all 0.3s ease;
}
.load-more-button:hover {
background-color: rgba(247, 147, 26, 0.2);
}
.load-more-button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Notification Animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.notification-item {
animation: fadeIn 0.3s ease-out;
}
/* Mobile Responsiveness */
@media (max-width: 768px) {
.notification-actions {
flex-direction: column;
gap: 8px;
}
.action-button {
width: 100%; /* Full width on small screens */
padding: 8px 12px;
font-size: 1rem;
}
.notification-controls {
flex-direction: column;
align-items: stretch;
}
.filter-buttons {
overflow-x: auto;
padding-bottom: 5px;
margin-bottom: 5px;
white-space: nowrap;
display: flex;
flex-wrap: nowrap;
}
.notification-actions {
justify-content: flex-end;
}
.notification-item {
padding: 8px;
}
.notification-icon {
flex: 0 0 30px;
}
.notification-content {
padding: 0 8px;
}
.notification-actions {
flex: 0 0 60px;
}
}

View File

@ -70,6 +70,7 @@ body {
font-weight: bold;
font-size: 1.1rem; /* Match card header font size */
border-bottom: none;
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite; /* Add flicker animation from card headers */
font-family: var(--header-font); /* Use the same font variable */
padding: 0.3rem 0; /* Match card header padding */
@ -231,6 +232,7 @@ body {
#retro-terminal-bar #progress-text {
font-size: 16px;
color: var(--terminal-text);
text-shadow: 0 0 5px var(--terminal-text);
margin-top: 5px;
text-align: center;
position: relative;
@ -240,6 +242,7 @@ body {
#retro-terminal-bar #uptimeTimer {
font-size: 16px;
color: var(--terminal-text);
text-shadow: 0 0 5px var(--terminal-text);
text-align: center;
position: relative;
z-index: 2;
@ -406,33 +409,3 @@ body {
align-items: center;
}
}
/* Make the terminal draggable in desktop view */
@media (min-width: 768px) {
/* Target both possible selectors to handle all cases */
#bitcoin-terminal,
.bitcoin-terminal,
#retro-terminal-bar {
cursor: grab; /* Show a grab cursor to indicate draggability */
user-select: none; /* Prevent text selection during drag */
}
/* Change cursor during active dragging */
#bitcoin-terminal.dragging,
.bitcoin-terminal.dragging,
#retro-terminal-bar.dragging {
cursor: grabbing;
}
/* Style for drag handle in the header */
.terminal-header {
cursor: grab;
}
.terminal-header::before {
content: "⋮⋮"; /* Add drag indicator */
position: absolute;
left: 10px;
opacity: 0.5;
}
}

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);
font-weight: bold;
font-size: 1.2rem;
text-shadow: 0 0 5px var(--primary-color);
margin-bottom: 5px;
white-space: nowrap;
overflow: hidden;
@ -156,12 +157,14 @@
background-color: rgba(50, 205, 50, 0.2);
border: 1px solid #32CD32;
color: #32CD32;
text-shadow: 0 0 5px rgba(50, 205, 50, 0.8);
}
.status-badge-offline {
background-color: rgba(255, 85, 85, 0.2);
border: 1px solid #ff5555;
color: #ff5555;
text-shadow: 0 0 5px rgba(255, 85, 85, 0.8);
}
/* Stats bars */
@ -177,7 +180,7 @@
.stats-bar {
height: 100%;
background: linear-gradient(90deg, #ff2d2d, #39ff14);
background: linear-gradient(90deg, #1137F5, #39ff14);
}
/* Summary stats in the header */
@ -196,7 +199,7 @@
.summary-stat-value {
font-size: 1.6rem;
/* font-weight: bold; */
font-weight: bold;
margin-bottom: 5px;
}
@ -330,7 +333,7 @@
}
/* Add extra padding at bottom of worker grid to avoid overlap */
.worker-grid {
margin-bottom: 120px;
margin-bottom: 60px;
}
/* Ensure summary stats have proper spacing on mobile */

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,384 @@
// Bitcoin Block Mining Animation Controller
class BlockMiningAnimation {
constructor(svgContainerId) {
// Get the container element
this.container = document.getElementById(svgContainerId);
if (!this.container) {
console.error("SVG container not found:", svgContainerId);
return;
}
// Get SVG elements
this.blockHeight = document.getElementById("block-height");
this.statusHeight = document.getElementById("status-height");
this.miningPool = document.getElementById("mining-pool");
this.blockTime = document.getElementById("block-time");
this.transactionCount = document.getElementById("transaction-count");
this.miningHash = document.getElementById("mining-hash");
this.nonceValue = document.getElementById("nonce-value");
this.difficultyValue = document.getElementById("difficulty-value");
this.miningStatus = document.getElementById("mining-status");
// Debug element availability
console.log("Animation elements found:", {
blockHeight: !!this.blockHeight,
statusHeight: !!this.statusHeight,
miningPool: !!this.miningPool,
blockTime: !!this.blockTime,
transactionCount: !!this.transactionCount,
miningHash: !!this.miningHash,
nonceValue: !!this.nonceValue,
difficultyValue: !!this.difficultyValue,
miningStatus: !!this.miningStatus
});
// Animation state
this.animationPhase = "collecting"; // collecting, mining, found, adding
this.miningSpeed = 300; // ms between nonce updates
this.nonceCounter = 0;
this.currentBlockData = null;
this.animationInterval = null;
this.apiRetryCount = 0;
this.maxApiRetries = 3;
// Initialize random hash for mining animation
this.updateRandomHash();
}
// Start the animation loop
start() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
}
console.log("Starting block mining animation");
this.animationInterval = setInterval(() => this.animationTick(), this.miningSpeed);
// Start by fetching the latest block
this.fetchLatestBlockWithRetry();
}
// Stop the animation
stop() {
if (this.animationInterval) {
clearInterval(this.animationInterval);
this.animationInterval = null;
}
}
// Main animation tick function
animationTick() {
switch (this.animationPhase) {
case "collecting":
// Simulate collecting transactions
this.updateTransactionAnimation();
break;
case "mining":
// Update nonce and hash values
this.updateMiningAnimation();
break;
case "found":
// Block found phase - brief celebration
this.updateFoundAnimation();
break;
case "adding":
// Adding block to chain
this.updateAddingAnimation();
break;
}
}
// Fetch latest block with retry logic
fetchLatestBlockWithRetry() {
this.apiRetryCount = 0;
this.fetchLatestBlock();
}
// Fetch the latest block data from mempool.space
fetchLatestBlock() {
console.log("Fetching latest block data, attempt #" + (this.apiRetryCount + 1));
// Show that we're fetching
if (this.miningStatus) {
this.miningStatus.textContent = "Connecting to blockchain...";
}
// Use the mempool.space public API
fetch("https://mempool.space/api/v1/blocks/tip/height")
.then(response => {
if (!response.ok) {
throw new Error("Failed to fetch latest block height: " + response.status);
}
return response.json();
})
.then(height => {
console.log("Latest block height:", height);
// Fetch multiple blocks but limit to 1
return fetch(`https://mempool.space/api/v1/blocks?height=${height}&limit=1`);
})
.then(response => {
if (!response.ok) {
throw new Error("Failed to fetch block data: " + response.status);
}
return response.json();
})
.then(blockData => {
console.log("Block data received:", blockData);
// Ensure we have data and use the first block
if (blockData && blockData.length > 0) {
this.currentBlockData = blockData[0];
this.startBlockAnimation();
// Reset retry count on success
this.apiRetryCount = 0;
} else {
throw new Error("No block data received");
}
})
.catch(error => {
console.error("Error fetching block data:", error);
// Retry logic
this.apiRetryCount++;
if (this.apiRetryCount < this.maxApiRetries) {
console.log(`Retrying in 2 seconds... (attempt ${this.apiRetryCount + 1}/${this.maxApiRetries})`);
setTimeout(() => this.fetchLatestBlock(), 2000);
} else {
console.warn("Max retries reached, using placeholder data");
// Use placeholder data if fetch fails after retries
this.usePlaceholderData();
this.startBlockAnimation();
}
});
}
// Start the block animation sequence
startBlockAnimation() {
// Reset animation state
this.animationPhase = "collecting";
this.nonceCounter = 0;
// Update block data display immediately
this.updateBlockDisplay();
// Schedule the animation sequence
setTimeout(() => {
this.animationPhase = "mining";
if (this.miningStatus) {
this.miningStatus.textContent = "Mining in progress...";
}
// After a random mining period, find the block
setTimeout(() => {
this.animationPhase = "found";
if (this.miningStatus) {
this.miningStatus.textContent = "BLOCK FOUND!";
}
// Then move to adding phase
setTimeout(() => {
this.animationPhase = "adding";
if (this.miningStatus) {
this.miningStatus.textContent = "Adding to blockchain...";
}
// After adding, fetch a new block or loop with current one
setTimeout(() => {
// Fetch a new block every time to keep data current
this.fetchLatestBlockWithRetry();
}, 3000);
}, 2000);
}, 5000 + Math.random() * 5000); // Random mining time
}, 3000); // Time for collecting transactions
}
// Update block display with current block data
updateBlockDisplay() {
if (!this.currentBlockData) {
console.error("No block data available to display");
return;
}
// Safely extract and format block data
const blockData = Array.isArray(this.currentBlockData)
? this.currentBlockData[0]
: this.currentBlockData;
console.log("Updating block display with data:", blockData);
try {
// Safely extract and format block height
const height = blockData.height ? blockData.height.toString() : "N/A";
if (this.blockHeight) this.blockHeight.textContent = height;
if (this.statusHeight) this.statusHeight.textContent = height;
// Safely format block timestamp
let formattedTime = "N/A";
if (blockData.timestamp) {
const timestamp = new Date(blockData.timestamp * 1000);
formattedTime = timestamp.toLocaleString();
}
if (this.blockTime) this.blockTime.textContent = formattedTime;
// Safely format transaction count
const txCount = blockData.tx_count ? blockData.tx_count.toString() : "N/A";
if (this.transactionCount) this.transactionCount.textContent = txCount;
// Format mining pool
let poolName = "Unknown";
if (blockData.extras && blockData.extras.pool && blockData.extras.pool.name) {
poolName = blockData.extras.pool.name;
}
if (this.miningPool) this.miningPool.textContent = poolName;
// Format difficulty (simplified)
let difficultyStr = "Unknown";
if (blockData.difficulty) {
// Format as scientific notation for better display
difficultyStr = blockData.difficulty.toExponential(2);
}
if (this.difficultyValue) this.difficultyValue.textContent = difficultyStr;
// Use actual nonce if available
if (this.nonceValue && blockData.nonce) {
this.nonceValue.textContent = blockData.nonce.toString();
// Use this as starting point for animation
this.nonceCounter = blockData.nonce;
}
// Update block hash (if available)
if (this.miningHash && blockData.id) {
const blockHash = blockData.id;
const shortHash = blockHash.substring(0, 8) + "..." + blockHash.substring(blockHash.length - 8);
this.miningHash.textContent = shortHash;
}
console.log("Block display updated successfully");
} catch (error) {
console.error("Error updating block display:", error, "Block data:", blockData);
}
}
// Transaction collection animation
updateTransactionAnimation() {
// Animation for collecting transactions is handled by SVG animation
// We could add additional logic here if needed
}
// Mining animation - update nonce and hash
updateMiningAnimation() {
// Increment nonce
this.nonceCounter += 1 + Math.floor(Math.random() * 1000);
if (this.nonceValue) {
this.nonceValue.textContent = this.nonceCounter.toString().padStart(10, '0');
}
// Update hash value
this.updateRandomHash();
}
// Block found animation - show a hash that matches difficulty
updateFoundAnimation() {
if (!this.miningHash || !this.nonceValue || !this.currentBlockData) return;
try {
// Make the "found" hash start with enough zeros based on difficulty
// Use actual block hash if available
const blockData = Array.isArray(this.currentBlockData)
? this.currentBlockData[0]
: this.currentBlockData;
if (blockData.id) {
const blockHash = blockData.id;
const shortHash = blockHash.substring(0, 8) + "..." + blockHash.substring(blockHash.length - 8);
this.miningHash.textContent = shortHash;
} else {
// Fallback to generated hash
const zeros = Math.min(6, Math.max(2, Math.floor(Math.log10(blockData.difficulty) / 10)));
const zeroPrefix = '0'.repeat(zeros);
const remainingChars = '0123456789abcdef';
let hash = zeroPrefix;
// Fill the rest with random hex characters
for (let i = zeros; i < 8; i++) {
hash += remainingChars.charAt(Math.floor(Math.random() * remainingChars.length));
}
this.miningHash.textContent = hash + "..." + hash;
}
// Use the actual nonce if available
if (blockData.nonce) {
this.nonceValue.textContent = blockData.nonce.toString();
}
} catch (error) {
console.error("Error updating found animation:", error);
}
}
// Adding block to chain animation
updateAddingAnimation() {
// Animation for adding to blockchain is handled by SVG animation
// We could add additional logic here if needed
}
// Generate a random hash string for mining animation
updateRandomHash() {
if (!this.miningHash) return;
const characters = '0123456789abcdef';
let hash = '';
// Generate random 8-char segment
for (let i = 0; i < 8; i++) {
hash += characters.charAt(Math.floor(Math.random() * characters.length));
}
this.miningHash.textContent = hash + "..." + hash;
}
// Use placeholder data if API fetch fails
usePlaceholderData() {
const now = Math.floor(Date.now() / 1000);
this.currentBlockData = {
height: 888888,
timestamp: now,
tx_count: 2500,
difficulty: 50000000000000,
nonce: 123456789,
id: "00000000000000000000b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7b7",
extras: {
pool: {
name: "Placeholder Pool"
}
}
};
console.log("Using placeholder data:", this.currentBlockData);
}
}
// Initialize and start the animation when the page loads
document.addEventListener("DOMContentLoaded", function () {
console.log("DOM content loaded, initializing animation");
// Ensure we give the SVG enough time to be fully rendered and accessible
setTimeout(() => {
const svgContainer = document.getElementById("svg-container");
if (!svgContainer) {
console.error("SVG container not found in DOM");
return;
}
try {
const animation = new BlockMiningAnimation("svg-container");
animation.start();
console.log("Animation started successfully");
} catch (error) {
console.error("Error starting animation:", error);
}
}, 1500); // Increased delay to ensure SVG is fully loaded
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,503 +0,0 @@
"use strict";
// Global variables
let currentFilter = "all";
let currentOffset = 0;
const pageSize = 20;
let hasMoreNotifications = true;
let isLoading = false;
// Timezone configuration
let dashboardTimezone = 'America/Los_Angeles'; // Default
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
// Initialize when document is ready
$(document).ready(() => {
console.log("Notification page initializing...");
// Fetch timezone configuration
fetchTimezoneConfig();
// Set up filter buttons
$('.filter-button').click(function () {
$('.filter-button').removeClass('active');
$(this).addClass('active');
currentFilter = $(this).data('filter');
resetAndLoadNotifications();
});
// Set up action buttons
$('#mark-all-read').click(markAllAsRead);
$('#clear-read').click(clearReadNotifications);
$('#clear-all').click(clearAllNotifications);
$('#load-more').click(loadMoreNotifications);
// Initial load of notifications
loadNotifications();
// Start polling for unread count
startUnreadCountPolling();
// Initialize BitcoinMinuteRefresh if available
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
BitcoinMinuteRefresh.initialize(refreshNotifications);
console.log("BitcoinMinuteRefresh initialized with refresh function");
}
// Start periodic update of notification timestamps every 30 seconds
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
function loadNotifications() {
if (isLoading) return;
isLoading = true;
showLoading();
const params = {
limit: pageSize,
offset: currentOffset
};
if (currentFilter !== "all") {
params.category = currentFilter;
}
$.ajax({
url: `/api/notifications?${$.param(params)}`,
method: "GET",
dataType: "json",
success: (data) => {
renderNotifications(data.notifications, currentOffset === 0);
updateUnreadBadge(data.unread_count);
// Update load more button state
hasMoreNotifications = data.notifications.length === pageSize;
$('#load-more').prop('disabled', !hasMoreNotifications);
isLoading = false;
},
error: (xhr, status, error) => {
console.error("Error loading notifications:", error);
showError("Failed to load notifications. Please try again.");
isLoading = false;
}
});
}
// Reset offset and load notifications
function resetAndLoadNotifications() {
currentOffset = 0;
loadNotifications();
}
// Load more notifications
function loadMoreNotifications() {
if (!hasMoreNotifications || isLoading) return;
currentOffset += pageSize;
loadNotifications();
}
// Refresh notifications (for periodic updates)
function refreshNotifications() {
// Only refresh if we're on the first page
if (currentOffset === 0) {
resetAndLoadNotifications();
} else {
// Just update the unread count
updateUnreadCount();
}
}
// This refreshes all timestamps on the page periodically
function updateNotificationTimestamps() {
$('.notification-item').each(function () {
const timestampStr = $(this).attr('data-timestamp');
if (timestampStr) {
try {
const timestamp = new Date(timestampStr);
// Update relative time
$(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);
}
}
});
}
// Show loading indicator
function showLoading() {
if (currentOffset === 0) {
// First page load, show loading message
$('#notifications-container').html('<div class="loading-message">Loading notifications<span class="terminal-cursor"></span></div>');
} else {
// Pagination load, show loading below
$('#load-more').prop('disabled', true).text('Loading...');
}
}
// Show error message
function showError(message) {
$('#notifications-container').html(`<div class="error-message">${message}</div>`);
$('#load-more').hide();
}
// Render notifications in the container
function renderNotifications(notifications, isFirstPage) {
const container = $('#notifications-container');
// If first page and no notifications
if (isFirstPage && (!notifications || notifications.length === 0)) {
container.html($('#empty-template').html());
$('#load-more').hide();
return;
}
// If first page, clear container
if (isFirstPage) {
container.empty();
}
// Render each notification
notifications.forEach(notification => {
const notificationElement = createNotificationElement(notification);
container.append(notificationElement);
});
// Show/hide load more button
$('#load-more').show().prop('disabled', !hasMoreNotifications);
}
// Create notification element from template
function createNotificationElement(notification) {
const template = $('#notification-template').html();
const element = $(template);
// Set data attributes
element.attr('data-id', notification.id)
.attr('data-level', notification.level)
.attr('data-category', notification.category)
.attr('data-read', notification.read)
.attr('data-timestamp', notification.timestamp);
// Set icon based on level
const iconElement = element.find('.notification-icon i');
switch (notification.level) {
case 'success':
iconElement.addClass('fa-check-circle');
break;
case 'info':
iconElement.addClass('fa-info-circle');
break;
case 'warning':
iconElement.addClass('fa-exclamation-triangle');
break;
case 'error':
iconElement.addClass('fa-times-circle');
break;
default:
iconElement.addClass('fa-bell');
}
// Important: Do not append "Z" here, as that can cause timezone issues
// Create a date object from the notification timestamp
let notificationDate;
try {
// 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
}
// Format the timestamp using the configured timezone
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'
};
// Format full timestamp with configured timezone
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>`;
element.find('.notification-message').html(messageWithTimestamp);
// Set metadata for relative time display
element.find('.notification-time').text(formatTimestamp(notificationDate));
element.find('.notification-category').text(notification.category);
// Set up action buttons
element.find('.mark-read-button').on('click', (e) => {
e.stopPropagation();
markAsRead(notification.id);
});
element.find('.delete-button').on('click', (e) => {
e.stopPropagation();
deleteNotification(notification.id);
});
// Hide mark as read button if already read
if (notification.read) {
element.find('.mark-read-button').hide();
}
return element;
}
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 diffMs = now - dateObj;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
const diffDay = Math.floor(diffHour / 24);
if (diffSec < 60) {
return "just now";
} else if (diffMin < 60) {
return `${diffMin}m ago`;
} else if (diffHour < 24) {
return `${diffHour}h ago`;
} else if (diffDay < 30) {
return `${diffDay}d ago`;
} else {
// Format as date for older notifications using configured timezone
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
};
return dateObj.toLocaleDateString('en-US', options);
}
}
// Mark a notification as read
function markAsRead(notificationId) {
$.ajax({
url: "/api/notifications/mark_read",
method: "POST",
data: JSON.stringify({ notification_id: notificationId }),
contentType: "application/json",
success: (data) => {
// Update UI
$(`[data-id="${notificationId}"]`).attr('data-read', 'true');
$(`[data-id="${notificationId}"]`).find('.mark-read-button').hide();
// Update unread badge
updateUnreadBadge(data.unread_count);
},
error: (xhr, status, error) => {
console.error("Error marking notification as read:", error);
}
});
}
// Mark all notifications as read
function markAllAsRead() {
$.ajax({
url: "/api/notifications/mark_read",
method: "POST",
data: JSON.stringify({}),
contentType: "application/json",
success: (data) => {
// Update UI
$('.notification-item').attr('data-read', 'true');
$('.mark-read-button').hide();
// Update unread badge
updateUnreadBadge(0);
},
error: (xhr, status, error) => {
console.error("Error marking all notifications as read:", error);
}
});
}
// Delete a notification
function deleteNotification(notificationId) {
$.ajax({
url: "/api/notifications/delete",
method: "POST",
data: JSON.stringify({ notification_id: notificationId }),
contentType: "application/json",
success: (data) => {
// Remove from UI with animation
$(`[data-id="${notificationId}"]`).fadeOut(300, function () {
$(this).remove();
// Check if container is empty now
if ($('#notifications-container').children().length === 0) {
$('#notifications-container').html($('#empty-template').html());
$('#load-more').hide();
}
});
// Update unread badge
updateUnreadBadge(data.unread_count);
},
error: (xhr, status, error) => {
console.error("Error deleting notification:", error);
}
});
}
// Clear read notifications
function clearReadNotifications() {
if (!confirm("Are you sure you want to clear all read notifications?")) {
return;
}
$.ajax({
url: "/api/notifications/clear",
method: "POST",
data: JSON.stringify({
// Special parameter to clear only read notifications
read_only: true
}),
contentType: "application/json",
success: () => {
// Reload notifications
resetAndLoadNotifications();
},
error: (xhr, status, error) => {
console.error("Error clearing read notifications:", error);
}
});
}
// Clear all notifications
function clearAllNotifications() {
if (!confirm("Are you sure you want to clear ALL notifications? This cannot be undone.")) {
return;
}
$.ajax({
url: "/api/notifications/clear",
method: "POST",
data: JSON.stringify({}),
contentType: "application/json",
success: () => {
// Reload notifications
resetAndLoadNotifications();
},
error: (xhr, status, error) => {
console.error("Error clearing all notifications:", error);
}
});
}
// Update unread badge
function updateUnreadBadge(count) {
$('#unread-badge').text(count);
// Add special styling if unread
if (count > 0) {
$('#unread-badge').addClass('has-unread');
} else {
$('#unread-badge').removeClass('has-unread');
}
}
// Update unread count from API
function updateUnreadCount() {
$.ajax({
url: "/api/notifications/unread_count",
method: "GET",
success: (data) => {
updateUnreadBadge(data.unread_count);
},
error: (xhr, status, error) => {
console.error("Error updating unread count:", error);
}
});
}
// Start polling for unread count
function startUnreadCountPolling() {
// Update every 30 seconds
setInterval(updateUnreadCount, 30000);
}

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

@ -3,9 +3,9 @@
// Global variables for workers dashboard
let workerData = null;
let refreshTimer;
const pageLoadTime = Date.now();
let pageLoadTime = Date.now();
let lastManualRefreshTime = 0;
const filterState = {
let filterState = {
currentFilter: 'all',
searchTerm: ''
};
@ -21,54 +21,67 @@ const MIN_REFRESH_INTERVAL = 10000; // Minimum 10 seconds between refreshes
// Hashrate Normalization Utilities
// Helper function to normalize hashrate to TH/s for consistent graphing
function normalizeHashrate(value, unit = 'th/s') {
function normalizeHashrate(value, unit) {
if (!value || isNaN(value)) return 0;
unit = unit.toLowerCase();
const unitConversion = {
'ph/s': 1000,
'eh/s': 1000000,
'gh/s': 1 / 1000,
'mh/s': 1 / 1000000,
'kh/s': 1 / 1000000000,
'h/s': 1 / 1000000000000
};
return unitConversion[unit] !== undefined ? value * unitConversion[unit] : value;
unit = (unit || 'th/s').toLowerCase();
if (unit.includes('ph/s')) {
return value * 1000; // Convert PH/s to TH/s
} else if (unit.includes('eh/s')) {
return value * 1000000; // Convert EH/s to TH/s
} else if (unit.includes('gh/s')) {
return value / 1000; // Convert GH/s to TH/s
} else if (unit.includes('mh/s')) {
return value / 1000000; // Convert MH/s to TH/s
} else if (unit.includes('kh/s')) {
return value / 1000000000; // Convert KH/s to TH/s
} else if (unit.includes('h/s') && !unit.includes('th/s') && !unit.includes('ph/s') &&
!unit.includes('eh/s') && !unit.includes('gh/s') && !unit.includes('mh/s') &&
!unit.includes('kh/s')) {
return value / 1000000000000; // Convert H/s to TH/s
} else {
// Assume TH/s if unit is not recognized
return value;
}
}
// Helper function to format hashrate values for display
function formatHashrateForDisplay(value, unit) {
if (isNaN(value) || value === null || value === undefined) return "N/A";
const normalizedValue = unit ? normalizeHashrate(value, unit) : value;
const unitRanges = [
{ threshold: 1000000, unit: 'EH/s', divisor: 1000000 },
{ threshold: 1000, unit: 'PH/s', divisor: 1000 },
{ threshold: 1, unit: 'TH/s', divisor: 1 },
{ threshold: 0.001, unit: 'GH/s', divisor: 1 / 1000 },
{ threshold: 0, unit: 'MH/s', divisor: 1 / 1000000 }
];
// Always normalize to TH/s first if unit is provided
let normalizedValue = unit ? normalizeHashrate(value, unit) : value;
for (const range of unitRanges) {
if (normalizedValue >= range.threshold) {
return (normalizedValue / range.divisor).toFixed(2) + ' ' + range.unit;
}
// Select appropriate unit based on magnitude
if (normalizedValue >= 1000000) { // EH/s range
return (normalizedValue / 1000000).toFixed(2) + ' EH/s';
} else if (normalizedValue >= 1000) { // PH/s range
return (normalizedValue / 1000).toFixed(2) + ' PH/s';
} else if (normalizedValue >= 1) { // TH/s range
return normalizedValue.toFixed(2) + ' TH/s';
} else if (normalizedValue >= 0.001) { // GH/s range
return (normalizedValue * 1000).toFixed(2) + ' GH/s';
} else { // MH/s range or smaller
return (normalizedValue * 1000000).toFixed(2) + ' MH/s';
}
return (normalizedValue * 1000000).toFixed(2) + ' MH/s';
}
// Initialize the page
$(document).ready(function () {
console.log("Worker page initializing...");
initNotificationBadge();
// Set up initial UI
initializePage();
// Get server time for uptime calculation
updateServerTime();
// Define global refresh function for BitcoinMinuteRefresh
window.manualRefresh = fetchWorkerData;
setTimeout(() => {
// Wait before initializing BitcoinMinuteRefresh to ensure DOM is ready
setTimeout(function () {
// Initialize BitcoinMinuteRefresh with our refresh function
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
BitcoinMinuteRefresh.initialize(window.manualRefresh);
console.log("BitcoinMinuteRefresh initialized with refresh function");
@ -77,8 +90,10 @@ $(document).ready(function () {
}
}, 500);
// Fetch worker data immediately on page load
fetchWorkerData();
// Set up filter button click handlers
$('.filter-button').click(function () {
$('.filter-button').removeClass('active');
$(this).addClass('active');
@ -86,56 +101,26 @@ $(document).ready(function () {
filterWorkers();
});
// Set up search input handler
$('#worker-search').on('input', function () {
filterState.searchTerm = $(this).val().toLowerCase();
filterWorkers();
});
});
// 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
function initializePage() {
console.log("Initializing page elements...");
// Initialize mini chart for total hashrate if the element exists
if (document.getElementById('total-hashrate-chart')) {
initializeMiniChart();
}
// Show loading state
$('#worker-grid').html('<div class="text-center p-5"><i class="fas fa-spinner fa-spin"></i> Loading worker data...</div>');
// Add retry button (hidden by default)
if (!$('#retry-button').length) {
$('body').append('<button id="retry-button" style="position: fixed; bottom: 20px; left: 20px; z-index: 1000; background: #f7931a; color: black; border: none; padding: 8px 16px; display: none; border-radius: 4px; cursor: pointer;">Retry Loading Data</button>');
@ -149,34 +134,11 @@ function initializePage() {
}
}
// Update unread notifications badge in navigation
function updateNotificationBadge() {
$.ajax({
url: "/api/notifications/unread_count",
method: "GET",
success: function (data) {
const unreadCount = data.unread_count;
const badge = $("#nav-unread-badge");
if (unreadCount > 0) {
badge.text(unreadCount).show();
} else {
badge.hide();
}
}
});
}
// Initialize notification badge checking
function initNotificationBadge() {
updateNotificationBadge();
setInterval(updateNotificationBadge, 60000);
}
// Server time update via polling - enhanced to use shared storage
function updateServerTime() {
console.log("Updating server time...");
// First try to get stored values
try {
const storedOffset = localStorage.getItem('serverTimeOffset');
const storedStartTime = localStorage.getItem('serverStartTime');
@ -186,26 +148,31 @@ function updateServerTime() {
serverStartTime = parseFloat(storedStartTime);
console.log("Using stored server time offset:", serverTimeOffset, "ms");
// Only update BitcoinMinuteRefresh if it's initialized
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
}
return;
return; // Don't fetch if we have valid values
}
} catch (e) {
console.error("Error reading stored server time:", e);
}
// Fetch from API if needed
$.ajax({
url: "/api/time",
method: "GET",
timeout: 5000,
success: function (data) {
// Calculate the offset between server time and local time
serverTimeOffset = new Date(data.server_timestamp).getTime() - Date.now();
serverStartTime = new Date(data.server_start_time).getTime();
// Store in localStorage for cross-page sharing
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
localStorage.setItem('serverStartTime', serverStartTime.toString());
// Only update BitcoinMinuteRefresh if it's initialized
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
}
@ -218,88 +185,83 @@ function updateServerTime() {
});
}
// Utility functions to show/hide loader
function showLoader() {
$("#loader").show();
}
function hideLoader() {
$("#loader").hide();
}
// Fetch worker data from API with pagination, limiting to 10 pages
// Fetch worker data from API
function fetchWorkerData(forceRefresh = false) {
console.log("Fetching worker data...");
// Track this as a manual refresh for throttling purposes
lastManualRefreshTime = Date.now();
$('#worker-grid').addClass('loading-fade');
showLoader();
const maxPages = 10;
const requests = [];
// Choose API URL based on whether we're forcing a refresh
const apiUrl = `/api/workers${forceRefresh ? '?force=true' : ''}`;
// Create requests for pages 1 through maxPages concurrently
for (let page = 1; page <= maxPages; page++) {
const apiUrl = `/api/workers?page=${page}${forceRefresh ? '&force=true' : ''}`;
requests.push($.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
timeout: 15000
}));
}
$.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
timeout: 15000, // 15 second timeout
success: function (data) {
if (!data || !data.workers || data.workers.length === 0) {
console.warn("No workers found in data response");
$('#worker-grid').html(`
<div class="text-center p-5">
<p>No workers found. Try refreshing the page.</p>
</div>
`);
return;
}
// Process all requests concurrently
Promise.all(requests)
.then(pages => {
let allWorkers = [];
let aggregatedData = null;
pages.forEach((data, i) => {
if (data && data.workers && data.workers.length > 0) {
allWorkers = allWorkers.concat(data.workers);
if (i === 0) {
aggregatedData = data; // preserve stats from first page
}
} else {
console.warn(`No workers found on page ${i + 1}`);
}
});
// Deduplicate workers if necessary (using worker.name as unique key)
const uniqueWorkers = allWorkers.filter((worker, index, self) =>
index === self.findIndex((w) => w.name === worker.name)
);
workerData = aggregatedData || {};
workerData.workers = uniqueWorkers;
workerData = data;
// Notify BitcoinMinuteRefresh that we've refreshed the data
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
BitcoinMinuteRefresh.notifyRefresh();
}
// Update UI with new data
updateWorkerGrid();
updateSummaryStats();
updateMiniChart();
updateLastUpdated();
// Hide retry button
$('#retry-button').hide();
connectionRetryCount = 0;
console.log("Worker data updated successfully");
$('#worker-grid').removeClass('loading-fade');
})
.catch(error => {
console.error("Error fetching worker data:", error);
})
.finally(() => {
hideLoader();
});
}
// Refresh worker data every 60 seconds
setInterval(function () {
console.log("Refreshing worker data at " + new Date().toLocaleTimeString());
fetchWorkerData();
}, 60000);
// Reset connection retry count
connectionRetryCount = 0;
console.log("Worker data updated successfully");
},
error: function (xhr, status, error) {
console.error("Error fetching worker data:", error);
// Show error in worker grid
$('#worker-grid').html(`
<div class="text-center p-5 text-danger">
<i class="fas fa-exclamation-triangle"></i>
<p>Error loading worker data: ${error || 'Unknown error'}</p>
</div>
`);
// Show retry button
$('#retry-button').show();
// Implement exponential backoff for automatic retry
connectionRetryCount++;
const delay = Math.min(30000, 1000 * Math.pow(1.5, Math.min(5, connectionRetryCount)));
console.log(`Will retry in ${delay / 1000} seconds (attempt ${connectionRetryCount})`);
setTimeout(() => {
fetchWorkerData(true); // Force refresh on retry
}, delay);
},
complete: function () {
$('#worker-grid').removeClass('loading-fade');
}
});
}
// Update the worker grid with data
function updateWorkerGrid() {
@ -313,6 +275,7 @@ function updateWorkerGrid() {
const workerGrid = $('#worker-grid');
workerGrid.empty();
// Apply current filters before rendering
const filteredWorkers = filterWorkersData(workerData.workers);
if (filteredWorkers.length === 0) {
@ -325,91 +288,107 @@ function updateWorkerGrid() {
return;
}
// Generate worker cards
filteredWorkers.forEach(worker => {
const card = createWorkerCard(worker);
// Create worker card
const card = $('<div class="worker-card"></div>');
// Add class based on status
if (worker.status === 'online') {
card.addClass('worker-card-online');
} else {
card.addClass('worker-card-offline');
}
// Add worker type badge
card.append(`<div class="worker-type">${worker.type}</div>`);
// Add worker name
card.append(`<div class="worker-name">${worker.name}</div>`);
// Add status badge
if (worker.status === 'online') {
card.append('<div class="status-badge status-badge-online">ONLINE</div>');
} else {
card.append('<div class="status-badge status-badge-offline">OFFLINE</div>');
}
// Add hashrate bar with normalized values for consistent display
const maxHashrate = 200; // TH/s - adjust based on your fleet
const normalizedHashrate = normalizeHashrate(
worker.hashrate_3hr,
worker.hashrate_3hr_unit || 'th/s'
);
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
// Format hashrate for display with appropriate unit
const formattedHashrate = formatHashrateForDisplay(
worker.hashrate_3hr,
worker.hashrate_3hr_unit || 'th/s'
);
card.append(`
<div class="worker-stats-row">
<div class="worker-stats-label">Hashrate (3hr):</div>
<div class="white-glow">${formattedHashrate}</div>
</div>
<div class="stats-bar-container">
<div class="stats-bar" style="width: ${hashratePercent}%"></div>
</div>
`);
// Add additional stats
card.append(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${typeof worker.last_share === 'string' ? worker.last_share.split(' ')[1] || worker.last_share : 'N/A'}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div>
<div class="green-glow">${worker.earnings.toFixed(8)}</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 class="worker-stats-row">
<div class="worker-stats-label">Temp:</div>
<div class="${worker.temperature > 65 ? 'red-glow' : 'white-glow'}">${worker.temperature > 0 ? worker.temperature + '°C' : 'N/A'}</div>
</div>
</div>
`);
// Add card to grid
workerGrid.append(card);
});
}
// Create worker card element
function createWorkerCard(worker) {
const card = $('<div class="worker-card"></div>');
card.addClass(worker.status === 'online' ? 'worker-card-online' : 'worker-card-offline');
card.append(`<div class="worker-type">${worker.type}</div>`);
card.append(`<div class="worker-name">${worker.name}</div>`);
card.append(`<div class="status-badge ${worker.status === 'online' ? 'status-badge-online' : 'status-badge-offline'}">${worker.status.toUpperCase()}</div>`);
const maxHashrate = 125; // TH/s - adjust based on your fleet
const normalizedHashrate = normalizeHashrate(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
const formattedHashrate = formatHashrateForDisplay(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
card.append(`
<div class="worker-stats-row">
<div class="worker-stats-label">Hashrate (3hr):</div>
<div class="white-glow">${formattedHashrate}</div>
</div>
<div class="stats-bar-container">
<div class="stats-bar" style="width: ${hashratePercent}%"></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(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${formattedLastShare}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div>
<div class="green-glow">${worker.earnings.toFixed(8)}</div>
</div>
</div>
`);
return card;
}
// Filter worker data based on current filter state
function filterWorkersData(workers) {
if (!workers) return [];
return workers.filter(worker => {
// Default to empty string if name is undefined
const workerName = (worker.name || '').toLowerCase();
const isOnline = worker.status === 'online';
const workerType = (worker.type || '').toLowerCase();
const matchesFilter = filterState.currentFilter === 'all' ||
(filterState.currentFilter === 'online' && isOnline) ||
(filterState.currentFilter === 'offline' && !isOnline) ||
(filterState.currentFilter === 'asic' && workerType === 'asic') ||
(filterState.currentFilter === 'bitaxe' && workerType === 'bitaxe');
// Check if worker matches filter
let matchesFilter = false;
if (filterState.currentFilter === 'all') {
matchesFilter = true;
} else if (filterState.currentFilter === 'online' && isOnline) {
matchesFilter = true;
} else if (filterState.currentFilter === 'offline' && !isOnline) {
matchesFilter = true;
} else if (filterState.currentFilter === 'asic' && workerType === 'asic') {
matchesFilter = true;
} else if (filterState.currentFilter === 'fpga' && workerType === 'fpga') {
matchesFilter = true;
}
// Check if worker matches search term
const matchesSearch = filterState.searchTerm === '' || workerName.includes(filterState.searchTerm);
return matchesFilter && matchesSearch;
@ -419,27 +398,41 @@ function filterWorkersData(workers) {
// Apply filter to rendered worker cards
function filterWorkers() {
if (!workerData || !workerData.workers) return;
// Re-render the worker grid with current filters
updateWorkerGrid();
}
// Update summary stats with normalized hashrate display
// Modified updateSummaryStats function with normalized hashrate display
function updateSummaryStats() {
if (!workerData) return;
// Update worker counts
$('#workers-count').text(workerData.workers_total || 0);
$('#workers-online').text(workerData.workers_online || 0);
$('#workers-offline').text(workerData.workers_offline || 0);
const onlinePercent = workerData.workers_total > 0 ? workerData.workers_online / workerData.workers_total : 0;
// Update worker ring percentage
const onlinePercent = workerData.workers_total > 0 ?
workerData.workers_online / workerData.workers_total : 0;
$('.worker-ring').css('--online-percent', onlinePercent);
const formattedHashrate = workerData.total_hashrate !== undefined ?
formatHashrateForDisplay(workerData.total_hashrate, workerData.hashrate_unit || 'TH/s') :
'0.0 TH/s';
$('#total-hashrate').text(formattedHashrate);
// Display normalized hashrate with appropriate unit
if (workerData.total_hashrate !== undefined) {
// Format with proper unit conversion
const formattedHashrate = formatHashrateForDisplay(
workerData.total_hashrate,
workerData.hashrate_unit || 'TH/s'
);
$('#total-hashrate').text(formattedHashrate);
} else {
$('#total-hashrate').text(`0.0 TH/s`);
}
// Update other summary stats
$('#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
@ -452,6 +445,7 @@ function initializeMiniChart() {
return;
}
// Generate some sample data to start
const labels = Array(24).fill('').map((_, i) => i);
const data = Array(24).fill(0).map(() => Math.random() * 100 + 700);
@ -501,23 +495,32 @@ function updateMiniChart() {
return;
}
// Extract hashrate data from history
const historyData = workerData.hashrate_history;
if (!historyData || historyData.length === 0) {
console.log("No hashrate history data available");
return;
}
const values = historyData.map(item => normalizeHashrate(parseFloat(item.value) || 0, item.unit || workerData.hashrate_unit || 'th/s'));
// Get the normalized values for the chart
const values = historyData.map(item => {
const val = parseFloat(item.value) || 0;
const unit = item.unit || workerData.hashrate_unit || 'th/s';
return normalizeHashrate(val, unit);
});
const labels = historyData.map(item => item.time);
// Update chart data
miniChart.data.labels = labels;
miniChart.data.datasets[0].data = values;
// Update y-axis range
const min = Math.min(...values.filter(v => v > 0)) || 0;
const max = Math.max(...values) || 1;
miniChart.options.scales.y.min = min * 0.9;
miniChart.options.scales.y.max = max * 1.1;
// Update the chart
miniChart.update('none');
}
@ -527,34 +530,10 @@ function updateLastUpdated() {
try {
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 = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
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> " +
formattedTime + "<span id='terminal-cursor'></span>");
console.log(`Last updated timestamp using timezone: ${configuredTimezone}`);
timestamp.toLocaleString() + "<span id='terminal-cursor'></span>");
} catch (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>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}BTC-OS MINING DASHBOARD {% endblock %}</title>
<title>{% block title %}Ocean.xyz Pool Mining Dashboard{% endblock %}</title>
<!-- Common fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
@ -17,110 +17,32 @@
<!-- 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 -->
{% 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>
<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">
<!-- Connection status indicator -->
<div id="connectionStatus"></div>
<!-- Top right link -->
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">Made by @DJO₿leezy</a>
<h1 class="text-center">
<a href="/" style="text-decoration:none; color:inherit;">
{% block header %}BTC-OS MINING DASHBOARD{% endblock %}
{% block header %}Ocean.xyz Pool Mining Dashboard v 0.3{% endblock %}
</a>
</h1>
<!-- Top right link -->
<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 %}
<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"><strong>Last Updated:</strong> {{ current_time }}<span id="terminal-cursor"></span></p>
{% endblock %}
{% block navigation %}
<div class="navigation-links">
<a href="/dashboard" class="nav-link {% block dashboard_active %}{% endblock %}">DASHBOARD</a>
<a href="/workers" class="nav-link {% block workers_active %}{% endblock %}">WORKERS</a>
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">BLOCKS</a>
<a href="/notifications" class="nav-link {% block notifications_active %}{% endblock %}">
NOTIFICATIONS
<span id="nav-unread-badge" class="nav-badge"></span>
</a>
<a href="/dashboard" class="nav-link {% block dashboard_active %}{% endblock %}">Main Dashboard</a>
<a href="/workers" class="nav-link {% block workers_active %}{% endblock %}">Workers Overview</a>
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">Bitcoin Blocks</a>
</div>
{% endblock %}
@ -131,11 +53,6 @@
{% 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>
{% endblock %}
<!-- Footer -->
<footer class="footer text-center">
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
</footer>
</div>
<!-- External JavaScript libraries -->
@ -143,32 +60,6 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></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 -->
{% block javascript %}{% endblock %}

View File

@ -1,53 +1,244 @@
{% extends "base.html" %}
{% extends "base.html" %}
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD {% endblock %}
{% block title %}Bitcoin Blocks - Ocean.xyz Mining Dashboard{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/css/blocks.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %}
{% block header %}BLOCKCHAIN MONITOR v 0.1{% endblock %}
{% block header %}Bitcoin Blocks Monitor{% endblock %}
{% block blocks_active %}active{% endblock %}
{% block content %}
<!-- Block Mining Animation Card -->
<!-- <div class="row mb-2">
<div class="col-12">
<div class="card">
<div class="card-header">Block Mining Visualization</div>
<div class="card-body mining-animation-container">
<!-- SVG Animation directly embedded
<div id="svg-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 300" width="100%" height="300" id="block-mining-animation" preserveAspectRatio="xMidYMid meet">
<!-- Background with scanlines
<defs>
<pattern id="scanlines" patternUnits="userSpaceOnUse" width="4" height="4">
<rect width="4" height="2" fill="#000" fill-opacity="0.1" />
<rect y="2" width="4" height="2" fill="none" />
</pattern>
<radialGradient id="glowEffect" cx="50%" cy="50%" r="50%" fx="50%" fy="50%">
<stop offset="0%" stop-color="#f7931a" stop-opacity="0.6" />
<stop offset="100%" stop-color="#f7931a" stop-opacity="0" />
</radialGradient>
<filter id="neonGlow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="2" result="blur" />
<feComposite in="SourceGraphic" in2="blur" operator="over" />
</filter>
<linearGradient id="bitcoinGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#f7931a" />
<stop offset="100%" stop-color="#ffc04a" />
</linearGradient>
<linearGradient id="transactionGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#39ff14" />
<stop offset="100%" stop-color="#00ffaa" />
</linearGradient>
<filter id="pixelate" x="0%" y="0%" width="100%" height="100%">
<feFlood x="4" y="4" height="2" width="2" />
<feComposite width="10" height="10" />
</filter>
</defs>
<!-- Main background
<rect width="500" height="300" fill="#0a0a0a" />
<!-- Scanline effect
<rect width="500" height="300" fill="url(#scanlines)" opacity="0.15" />
<!-- Blockchain (rows of connected blocks)
<g id="blockchain" transform="translate(20, 270)">
<!-- Previous blocks (part of the chain)
<g id="previous-blocks">
<rect x="0" y="-40" width="25" height="40" rx="3" fill="#232323" stroke="#f7931a" stroke-width="2" />
<rect x="35" y="-30" width="25" height="25" rx="3" fill="#232323" stroke="#f7931a" stroke-width="2" />
<rect x="70" y="-30" width="25" height="25" rx="3" fill="#232323" stroke="#f7931a" stroke-width="2" />
<rect x="105" y="-30" width="25" height="25" rx="3" fill="#232323" stroke="#f7931a" stroke-width="2" />
<!-- Connecting lines
<line x1="25" y1="-17.5" x2="35" y2="-17.5" stroke="#f7931a" stroke-width="2" />
<line x1="60" y1="-17.5" x2="70" y2="-17.5" stroke="#f7931a" stroke-width="2" />
<line x1="95" y1="-17.5" x2="105" y2="-17.5" stroke="#f7931a" stroke-width="2" />
<line x1="130" y1="-17.5" x2="140" y2="-17.5" stroke="#f7931a" stroke-width="2" stroke-dasharray="2,2">
<animate attributeName="stroke-dashoffset" from="0" to="4" dur="1s" repeatCount="indefinite" />
</line>
</g>
<!-- Bitcoin logo
<g id="bitcoin-logo" transform="translate(250, -60)">
<circle cx="170" cy="15" r="20" fill="#0a0a0a" stroke="url(#bitcoinGradient)" stroke-width="2" filter="url(#neonGlow)" />
<text id="bitcoin-symbol" x="170" y="15" font-family="sans-serif" font-size="22" font-weight="bold" fill="url(#bitcoinGradient)" text-anchor="middle" dy=".35em" filter="url(#neonGlow)"></text>
<!-- Rotating glow effect
<circle cx="170" cy="15" r="28" fill="none" stroke="url(#bitcoinGradient)" stroke-width="1" opacity="0.5">
<animate attributeName="opacity" values="0.5;0.8;0.5" dur="3s" repeatCount="indefinite" />
<animate attributeName="r" values="28;30;28" dur="3s" repeatCount="indefinite" />
</circle>
</g>
</g>
<!-- Current block being mined
<g id="current-block" transform="translate(140, 240)">
<!-- Block outline -->
<rect id="block-outline" x="20" y="-30" width="70" height="60" rx="3" fill="#131313" stroke="#f7931a" stroke-width="2">
<animate attributeName="stroke-opacity" values="1;0.5;1" dur="2s" repeatCount="indefinite" />
</rect>
<!-- Block header data
<text x="55" y="-20" font-family="monospace" font-size="7" fill="#00dfff" text-anchor="middle" filter="url(#neonGlow)">Block #<tspan id="block-height">000000</tspan></text>
<!-- Block transactions
<g id="transactions">
<rect class="transaction" x="25" y="-15" width="60" height="5" rx="1" fill="url(#transactionGradient)" opacity="0.7">
<animate attributeName="opacity" values="0.7;1;0.7" dur="3s" repeatCount="indefinite" begin="0s" />
</rect>
<rect class="transaction" x="25" y="-8" width="60" height="5" rx="1" fill="url(#transactionGradient)" opacity="0.7">
<animate attributeName="opacity" values="0.7;1;0.7" dur="3s" repeatCount="indefinite" begin="0.5s" />
</rect>
<rect class="transaction" x="25" y="-1" width="60" height="5" rx="1" fill="url(#transactionGradient)" opacity="0.7">
<animate attributeName="opacity" values="0.7;1;0.7" dur="3s" repeatCount="indefinite" begin="1s" />
</rect>
<rect class="transaction" x="25" y="6" width="60" height="5" rx="1" fill="url(#transactionGradient)" opacity="0.7">
<animate attributeName="opacity" values="0.7;1;0.7" dur="3s" repeatCount="indefinite" begin="1.5s" />
</rect>
<rect class="transaction" x="25" y="13" width="60" height="5" rx="1" fill="url(#transactionGradient)" opacity="0.7">
<animate attributeName="opacity" values="0.7;1;0.7" dur="3s" repeatCount="indefinite" begin="2s" />
</rect>
<text id="tx-count" x="55" y="25" font-family="monospace" font-size="7" fill="#ffffff" text-anchor="middle">Txs: <tspan id="transaction-count">0000</tspan></text>
</g>
</g>
<!-- Mining animation
<g id="mining-animation" transform="translate(250, 180)">
<!-- Mining text
<text x="0" y="0" font-family="monospace" font-size="12" fill="#f7931a" filter="url(#neonGlow)">Mining hash:</text>
<text id="mining-hash" x="0" y="15" font-family="monospace" font-size="10" fill="#ffffff">0000...0000</text>
<!-- Nonce counter
<text x="0" y="35" font-family="monospace" font-size="10" fill="#00dfff" filter="url(#neonGlow)">Nonce: <tspan id="nonce-value">0000000000</tspan></text>
<!-- Difficulty target
<text x="0" y="50" font-family="monospace" font-size="10" fill="#ffd700" filter="url(#neonGlow)">Difficulty: <tspan id="difficulty-value">0000000</tspan></text>
<!-- Mining status
<text id="mining-status" x="0" y="70" font-family="monospace" font-size="12" fill="#39ff14" filter="url(#neonGlow)">Mining in progress...</text>
<!-- Hash calculation animation (tiny dots moving)
<g id="hash-calculation">
<circle cx="10" cy="85" r="2" fill="#f7931a">
<animate attributeName="cx" values="10;180;10" dur="3s" repeatCount="indefinite" />
<animate attributeName="opacity" values="1;0.5;1" dur="3s" repeatCount="indefinite" />
</circle>
<circle cx="30" cy="85" r="2" fill="#f7931a">
<animate attributeName="cx" values="30;200;30" dur="3s" repeatCount="indefinite" begin="0.5s" />
<animate attributeName="opacity" values="1;0.5;1" dur="3s" repeatCount="indefinite" begin="0.5s" />
</circle>
<circle cx="50" cy="85" r="2" fill="#f7931a">
<animate attributeName="cx" values="50;220;50" dur="3s" repeatCount="indefinite" begin="1s" />
<animate attributeName="opacity" values="1;0.5;1" dur="3s" repeatCount="indefinite" begin="1s" />
</circle>
</g>
</g>
<!-- Status display
<g id="status-display" transform="translate(20, 20)">
<rect width="460" height="20" fill="#0a0a0a" stroke="#f7931a" stroke-width="1" />
<text id="status-text" x="10" y="14" font-family="monospace" font-size="12" fill="#ffffff">MINING BLOCK #<tspan id="status-height">000000</tspan> | POOL: <tspan id="mining-pool">Unknown</tspan></text>
<!-- Indicator lights
<circle cx="440" cy="10" r="5" fill="#39ff14" filter="url(#neonGlow)">
<animate attributeName="opacity" values="1;0.5;1" dur="2s" repeatCount="indefinite" />
</circle>
</g>
<!-- Timestamp display
<g id="timestamp-display" transform="translate(20, 50)">
<text id="time-text" x="0" y="0" font-family="monospace" font-size="10" fill="#00dfff">Time: <tspan id="block-time">0000-00-00 00:00:00</tspan></text>
</g>
<!-- CRT flicker animation for the entire SVG
<rect width="500" height="300" fill="none" opacity="0.03">
<animate attributeName="opacity" values="0.03;0.05;0.03" dur="0.5s" repeatCount="indefinite" />
</rect>
</svg>
</div>
</div>
</div>
</div>
</div> -->
<!-- Block Controls
<div class="row mb-2">
<div class="col-12">
<div class="card">
<div class="card-header">Block Controls</div>
<div class="card-body">
<div class="block-controls">
<!-- <div class="block-control-item">
<label for="block-height-input">Start Block:</label>
<input type="number" id="block-height-input" name="block-height">
</div>
<div class="block-control-item">
<button id="load-blocks" class="block-button">Load Blocks</button>
<button id="latest-blocks" class="block-button">Latest Blocks</button>
</div>
</div>
</div>
</div>
</div>
</div> -->
<!-- Latest block stats -->
<div class="row mb-2 equal-height">
<div class="col-12">
<div class="card">
<div class="card-header">LATEST BLOCK STATS</div>
<div class="card-header">Latest Block Stats</div>
<div class="card-body">
<div class="latest-block-stats">
<div class="stat-item">
<strong>BLOCK HEIGHT:</strong>
<strong>Block Height:</strong>
<span id="latest-height" class="metric-value white">Loading...</span>
</div>
<div class="stat-item">
<strong>TIME:</strong>
<strong>Time:</strong>
<span id="latest-time" class="metric-value blue">Loading...</span>
</div>
<div class="stat-item">
<strong>TRANSACTIONS:</strong>
<strong>Transactions:</strong>
<span id="latest-tx-count" class="metric-value white">Loading...</span>
</div>
<div class="stat-item">
<strong>SIZE:</strong>
<strong>Size:</strong>
<span id="latest-size" class="metric-value white">Loading...</span>
</div>
<div class="stat-item">
<strong>DIFFICULTY:</strong>
<strong>Difficulty:</strong>
<span id="latest-difficulty" class="metric-value yellow">Loading...</span>
</div>
<div class="stat-item">
<strong>POOL:</strong>
<strong>Mining Pool:</strong>
<span id="latest-pool" class="metric-value green">Loading...</span>
</div>
<div class="stat-item">
<strong>AVG FEE RATE:</strong>
<span id="latest-fee-rate" class="metric-value yellow" style="animation: pulse 1s infinite;">Loading...</span>
</div>
</div>
</div>
</div>
@ -58,13 +249,13 @@
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">RECENT BLOCKS</div>
<div class="card-header">Recent Bitcoin Blocks</div>
<div class="card-body">
<div class="blocks-container">
<div id="blocks-grid" class="blocks-grid">
<!-- Blocks will be generated here via JavaScript -->
<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>
@ -77,7 +268,7 @@
<div id="block-modal" class="block-modal">
<div class="block-modal-content">
<div class="block-modal-header">
<span class="block-modal-title">BLOCK DETAILS</span>
<span class="block-modal-title">Block Details</span>
<span class="block-modal-close">&times;</span>
</div>
<div class="block-modal-body">
@ -91,4 +282,5 @@
{% block javascript %}
<script src="/static/js/blocks.js"></script>
{% endblock %}
<script src="/static/js/block-animation.js"></script>
{% endblock %}

View File

@ -6,32 +6,170 @@
<title>Ocean.xyz Pool Miner - Initializing...</title>
<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/theme-toggle.css">
<!-- Add Theme JS -->
<script src="/static/js/theme.js"></script>
<style>
/* Added styles for configuration form */
#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>
<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>
<div id="debug-info"></div>
<div id="loading-message">Loading mining data...</div>
@ -42,7 +180,7 @@
██╔══██╗ ██║ ██║ ██║ ██║╚════██║
██████╔╝ ██║ ╚██████╗ ╚██████╔╝███████║
╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
v.21
v.21
</div>
<div id="terminal">
<div id="terminal-content">
@ -88,41 +226,7 @@ v.21
<span class="tooltip-text">Total power consumption of your mining equipment</span>
</span>
</label>
<input type="number" id="power-usage" step="50" min="0" placeholder="13450" value="">
</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>
<input type="number" id="power-usage" step="1" min="0" placeholder="3450" value="">
</div>
<div id="form-message"></div>
<div class="form-actions">
@ -132,147 +236,6 @@ v.21
</div>
<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
function updateDebug(message) {
document.getElementById('debug-info').textContent = message;
@ -301,50 +264,45 @@ v.21
let bootComplete = false;
let configLoaded = false;
let currentConfig = {
wallet: "yourwallethere",
wallet: "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9",
power_cost: 0.0,
power_usage: 0.0
};
// Update loadConfig function to include network fee
// Replace the current loadConfig function with this improved version
function loadConfig() {
return new Promise((resolve, reject) => {
fetch('/api/config?nocache=' + new Date().getTime())
.then(response => {
if (!response.ok) {
throw new Error('Failed to load configuration: ' + response.statusText);
}
return response.json();
})
.then(data => {
console.log("Loaded configuration:", data);
currentConfig = data;
// Always make a fresh request to get the latest config
fetch('/api/config?nocache=' + new Date().getTime()) // Add cache-busting parameter
.then(response => {
if (!response.ok) {
throw new Error('Failed to load configuration: ' + response.statusText);
}
return response.json();
})
.then(data => {
console.log("Loaded configuration:", data);
currentConfig = data;
// Update form fields with latest values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || "";
configLoaded = true;
resolve(currentConfig);
})
.catch(err => {
console.error("Error loading config:", err);
// Use default values if loading fails
currentConfig = {
wallet: "yourwallethere",
power_cost: 0.0,
power_usage: 0.0,
network_fee: 0.0
};
// After loading, always update the form fields with the latest values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
configLoaded = true;
})
.catch(err => {
console.error("Error loading config:", err);
// Use default values if loading fails
currentConfig = {
wallet: "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9",
power_cost: 0.0,
power_usage: 0.0
};
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || "";
resolve(currentConfig);
});
});
// Still update the form with default values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
});
}
// Also update the save button event handler to reload the config after saving
@ -370,14 +328,41 @@ v.21
});
});
// Safety timeout: redirect after 120 seconds if boot not complete
// 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 60 seconds if boot not complete
window.addEventListener('load', function () {
setTimeout(function () {
if (!bootComplete && !waitingForUserInput) {
console.warn("Safety timeout reached - redirecting to dashboard");
redirectToDashboard();
}
}, 120000);
}, 60000);
});
// Configuration form event listeners
@ -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 () {
// Set default values including network fee
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;
console.log("Use Defaults button clicked");
// 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 originalText = btn.textContent;
btn.textContent = "Defaults Applied";
btn.style.backgroundColor = "#32CD32";
// Reset the button after a short delay
setTimeout(function () {
btn.textContent = originalText;
btn.style.backgroundColor = "";
@ -503,14 +498,10 @@ v.21
}
setTimeout(processNextMessage, 500);
} 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 += "\nPlease configure your mining setup:\n";
// Short pause and then show the configuration form
setTimeout(function () {
document.getElementById('config-form').style.display = 'block';
}, 1000);
outputElement.innerHTML += "\nUsing default configuration values.\n";
setTimeout(redirectToDashboard, 2000);
}
} catch (err) {
setTimeout(redirectToDashboard, 1000);
@ -586,16 +577,10 @@ v.21
}
}
// Skip button: reveal configuration form only
// Skip button: immediately redirect
skipButton.addEventListener('click', function () {
clearTimeout(timeoutId);
// Optionally, clear boot messages or hide elements related to boot sequence
outputElement.innerHTML = "";
// Hide any loading or prompt messages
loadingMessage.style.display = 'none';
promptContainer.style.display = 'none';
// Show the configuration form
configForm.style.display = 'block';
redirectToDashboard();
});
// Start the typing animation (hides loading message)
@ -607,7 +592,7 @@ v.21
// Fallback messages (used immediately)
function setupFallbackMessages() {
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: "All rights reserved.\n\n", speed: 25, delay: 300 },
{ text: "INITIALIZING SYSTEM...\n", speed: 25, delay: 300 },

View File

@ -1,10 +1,9 @@
{% extends "base.html" %}
{% block title %}BTC-OS Mining Dashboard {% endblock %}
{% block title %}Ocean.xyz Pool Mining Dashboard v 0.2{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/css/dashboard.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %}
{% block dashboard_active %}active{% endblock %}
@ -12,360 +11,349 @@
{% block content %}
<!-- Graph Container -->
<div id="graphContainer" class="mb-2">
<canvas id="trendGraph" style="width: 100%; height: 100%; position: relative; z-index: 2;"></canvas>
<canvas id="trendGraph" style="width: 100%; height: 100%; position: relative; z-index: 2;"></canvas>
</div>
<!-- Miner Status and Payout Info -->
<!-- Miner Status -->
<div class="row mb-2 equal-height">
<div class="col-md-6">
<div class="card">
<div class="card-header">Miner Status</div>
<div class="card-body">
<p>
<strong>Status:</strong>
<span id="miner_status" class="metric-value">
{% if metrics and metrics.workers_hashing and metrics.workers_hashing > 0 %}
<span class="status-green">ONLINE</span> <span class="online-dot"></span>
{% else %}
<span class="status-red">OFFLINE</span> <span class="offline-dot"></span>
{% endif %}
</span>
</p>
<p>
<strong>Workers Hashing:</strong>
<span id="workers_hashing" class="metric-value">{{ metrics.workers_hashing or 0 }}</span>
<span id="indicator_workers_hashing"></span>
</p>
<p>
<strong>Last Share:</strong>
<span id="last_share" class="metric-value">{{ metrics.total_last_share or "N/A" }}</span>
</p>
<p>
<strong>Blocks Found:</strong>
<span id="blocks_found" class="metric-value white">
{{ metrics.blocks_found if metrics and metrics.blocks_found else "0" }}
</span>
<span id="indicator_blocks_found"></span>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card" id="payoutMiscCard">
<div class="card-header">Payout Info</div>
<div class="card-body">
<p>
<strong>Unpaid Earnings:</strong>
<span id="unpaid_earnings" class="metric-value green">
{% if metrics and metrics.unpaid_earnings %}
{{ metrics.unpaid_earnings }} BTC
{% else %}
0 BTC
{% endif %}
</span>
<span id="indicator_unpaid_earnings"></span>
</p>
<p>
<strong>Last Block:</strong>
<span id="last_block_height" class="metric-value white">
{{ metrics.last_block_height|commafy if metrics and metrics.last_block_height else "N/A" }}
</span>
<span id="last_block_time" class="metric-value blue">
{{ metrics.last_block_time if metrics and metrics.last_block_time else "N/A" }}
</span>
<span class="green">
{% if metrics and metrics.last_block_earnings %}
+{{ metrics.last_block_earnings|int|commafy }} SATS
{% else %}
+0 SATS
{% endif %}
</span>
<span id="indicator_last_block"></span>
</p>
<p>
<strong>Est. Time to Payout:</strong>
<span id="est_time_to_payout" class="metric-value yellow">
{{ metrics.est_time_to_payout if metrics and metrics.est_time_to_payout else "N/A" }}
</span>
<span id="indicator_est_time_to_payout"></span>
</p>
<p>
<strong>Pool Fees:</strong>
<span id="pool_fees_percentage" class="metric-value">
{% if metrics and metrics.pool_fees_percentage is defined and metrics.pool_fees_percentage is not none %}
{{ 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 id="indicator_pool_fees_percentage"></span>
</p>
</div>
</div>
<div class="col-12">
<div class="card">
<div class="card-header">Miner Status</div>
<div class="card-body">
<p>
<strong>Status:</strong>
<span id="miner_status" class="metric-value">
{% if metrics and metrics.workers_hashing and metrics.workers_hashing > 0 %}
<span class="status-green">ONLINE</span> <span class="online-dot"></span>
{% else %}
<span class="status-red">OFFLINE</span> <span class="offline-dot"></span>
{% endif %}
</span>
</p>
<p>
<strong>Workers Hashing:</strong>
<span id="workers_hashing" class="metric-value">{{ metrics.workers_hashing or 0 }}</span>
<span id="indicator_workers_hashing"></span>
</p>
<p>
<strong>Last Share:</strong>
<span id="last_share" class="metric-value">{{ metrics.total_last_share or "N/A" }}</span>
</p>
</div>
</div>
</div>
</div>
<!-- Pool Hashrates and Bitcoin Network Stats -->
<div class="row equal-height">
<div class="col-md-6">
<div class="card">
<div class="card-header">Pool Hashrates</div>
<div class="card-body">
<p>
<strong>Pool Hashrate:</strong>
<span id="pool_total_hashrate" class="metric-value white">
{% 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:] }}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_pool_total_hashrate"></span>
</p>
<hr>
<p>
<strong>24hr Avg Hashrate:</strong>
<span id="hashrate_24hr" class="metric-value white">
{% if metrics and metrics.hashrate_24hr %}
{{ metrics.hashrate_24hr }}
{% if metrics.hashrate_24hr_unit %}
{{ metrics.hashrate_24hr_unit[:-2]|upper ~ metrics.hashrate_24hr_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_24hr"></span>
</p>
<p>
<strong>3hr Avg Hashrate:</strong>
<span id="hashrate_3hr" class="metric-value white">
{% if metrics and metrics.hashrate_3hr %}
{{ metrics.hashrate_3hr }}
{% if metrics.hashrate_3hr_unit %}
{{ metrics.hashrate_3hr_unit[:-2]|upper ~ metrics.hashrate_3hr_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_3hr"></span>
</p>
<p>
<strong>10min Avg Hashrate:</strong>
<span id="hashrate_10min" class="metric-value white">
{% if metrics and metrics.hashrate_10min %}
{{ metrics.hashrate_10min }}
{% if metrics.hashrate_10min_unit %}
{{ metrics.hashrate_10min_unit[:-2]|upper ~ metrics.hashrate_10min_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_10min"></span>
</p>
<p>
<strong>60sec Avg Hashrate:</strong>
<span id="hashrate_60sec" class="metric-value white">
{% if metrics and metrics.hashrate_60sec %}
{{ metrics.hashrate_60sec }}
{% if metrics.hashrate_60sec_unit %}
{{ metrics.hashrate_60sec_unit[:-2]|upper ~ metrics.hashrate_60sec_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_60sec"></span>
</p>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Pool Hashrates</div>
<div class="card-body">
<p>
<strong>Pool Total Hashrate:</strong>
<span id="pool_total_hashrate" class="metric-value white">
{% 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:] }}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_pool_total_hashrate"></span>
</p>
<hr>
<p>
<strong>24hr Avg Hashrate:</strong>
<span id="hashrate_24hr" class="metric-value white">
{% if metrics and metrics.hashrate_24hr %}
{{ metrics.hashrate_24hr }}
{% if metrics.hashrate_24hr_unit %}
{{ metrics.hashrate_24hr_unit[:-2]|upper ~ metrics.hashrate_24hr_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_24hr"></span>
</p>
<p>
<strong>3hr Avg Hashrate:</strong>
<span id="hashrate_3hr" class="metric-value white">
{% if metrics and metrics.hashrate_3hr %}
{{ metrics.hashrate_3hr }}
{% if metrics.hashrate_3hr_unit %}
{{ metrics.hashrate_3hr_unit[:-2]|upper ~ metrics.hashrate_3hr_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_3hr"></span>
</p>
<p>
<strong>10min Avg Hashrate:</strong>
<span id="hashrate_10min" class="metric-value white">
{% if metrics and metrics.hashrate_10min %}
{{ metrics.hashrate_10min }}
{% if metrics.hashrate_10min_unit %}
{{ metrics.hashrate_10min_unit[:-2]|upper ~ metrics.hashrate_10min_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_10min"></span>
</p>
<p>
<strong>60sec Avg Hashrate:</strong>
<span id="hashrate_60sec" class="metric-value white">
{% if metrics and metrics.hashrate_60sec %}
{{ metrics.hashrate_60sec }}
{% if metrics.hashrate_60sec_unit %}
{{ metrics.hashrate_60sec_unit[:-2]|upper ~ metrics.hashrate_60sec_unit[-2:] }}
{% else %}
TH/s
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_hashrate_60sec"></span>
</p>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Network Stats</div>
<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>
<strong>Block Number:</strong>
<span id="block_number" class="metric-value white">
{% if metrics and metrics.block_number %}
{{ metrics.block_number|commafy }}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_block_number"></span>
</p>
<p>
<strong>Network Hashrate:</strong>
<span id="network_hashrate" class="metric-value white">
{% if metrics and metrics.network_hashrate %}
{{ metrics.network_hashrate|round|commafy }} EH/s
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_network_hashrate"></span>
</p>
<p>
<strong>Difficulty:</strong>
<span id="difficulty" class="metric-value white">
{% if metrics and metrics.difficulty %}
{{ metrics.difficulty|round|commafy }}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_difficulty"></span>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Bitcoin Network Stats</div>
<div class="card-body">
<p>
<strong>Block Number:</strong>
<span id="block_number" class="metric-value white">
{% if metrics and metrics.block_number %}
{{ metrics.block_number|commafy }}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_block_number"></span>
</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>
<strong>Network Hashrate:</strong>
<span id="network_hashrate" class="metric-value white">
{% if metrics and metrics.network_hashrate %}
{{ metrics.network_hashrate|round|commafy }} EH/s
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_network_hashrate"></span>
</p>
<p>
<strong>Difficulty:</strong>
<span id="difficulty" class="metric-value white">
{% if metrics and metrics.difficulty %}
{{ metrics.difficulty|round|commafy }}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_difficulty"></span>
</p>
</div>
</div>
</div>
</div>
<!-- Satoshi and USD Metrics -->
<div class="row equal-height">
<div class="col-md-6">
<div class="card">
<div class="card-header">SATOSHI EARNINGS</div>
<div class="card-body">
<p>
<strong>Projected Daily (Net):</strong>
<span id="daily_mined_sats" class="metric-value yellow">
{% if metrics and metrics.daily_mined_sats %}
{{ metrics.daily_mined_sats|commafy }} SATS
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_daily_mined_sats"></span>
</p>
<p>
<strong>Projected Monthly (Net):</strong>
<span id="monthly_mined_sats" class="metric-value yellow">
{% if metrics and metrics.monthly_mined_sats %}
{{ metrics.monthly_mined_sats|commafy }} SATS
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_monthly_mined_sats"></span>
</p>
<p>
<strong>Est. Earnings/Day:</strong>
<span id="estimated_earnings_per_day_sats" class="metric-value yellow">
{% if metrics and metrics.estimated_earnings_per_day_sats %}
{{ metrics.estimated_earnings_per_day_sats|commafy }} SATS
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_estimated_earnings_per_day_sats"></span>
</p>
<p>
<strong>Est. Earnings/Block:</strong>
<span id="estimated_earnings_next_block_sats" class="metric-value yellow">
{% if metrics and metrics.estimated_earnings_next_block_sats %}
{{ metrics.estimated_earnings_next_block_sats|commafy }} SATS
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_estimated_earnings_next_block_sats"></span>
</p>
<p>
<strong>Est. Rewards in Window:</strong>
<span id="estimated_rewards_in_window_sats" class="metric-value yellow">
{% if metrics and metrics.estimated_rewards_in_window_sats %}
{{ metrics.estimated_rewards_in_window_sats|commafy }} SATS
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_estimated_rewards_in_window_sats"></span>
</p>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">Satoshi Metrics</div>
<div class="card-body">
<p>
<strong>Daily Mined (Net):</strong>
<span id="daily_mined_sats" class="metric-value yellow">
{% if metrics and metrics.daily_mined_sats %}
{{ metrics.daily_mined_sats|commafy }} sats
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_daily_mined_sats"></span>
</p>
<p>
<strong>Monthly Mined (Net):</strong>
<span id="monthly_mined_sats" class="metric-value yellow">
{% if metrics and metrics.monthly_mined_sats %}
{{ metrics.monthly_mined_sats|commafy }} sats
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_monthly_mined_sats"></span>
</p>
<p>
<strong>Est. Earnings/Day:</strong>
<span id="estimated_earnings_per_day_sats" class="metric-value yellow">
{% if metrics and metrics.estimated_earnings_per_day_sats %}
{{ metrics.estimated_earnings_per_day_sats|commafy }} sats
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_estimated_earnings_per_day_sats"></span>
</p>
<p>
<strong>Est. Earnings/Block:</strong>
<span id="estimated_earnings_next_block_sats" class="metric-value yellow">
{% if metrics and metrics.estimated_earnings_next_block_sats %}
{{ metrics.estimated_earnings_next_block_sats|commafy }} sats
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_estimated_earnings_next_block_sats"></span>
</p>
<p>
<strong>Est. Rewards in Window:</strong>
<span id="estimated_rewards_in_window_sats" class="metric-value yellow">
{% if metrics and metrics.estimated_rewards_in_window_sats %}
{{ metrics.estimated_rewards_in_window_sats|commafy }} sats
{% else %}
0 sats
{% endif %}
</span>
<span id="indicator_estimated_rewards_in_window_sats"></span>
</p>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">USD Metrics</div>
<div class="card-body">
<p>
<strong>Daily Revenue:</strong>
<span id="daily_revenue" class="metric-value green">
{% if metrics and metrics.daily_revenue is defined and metrics.daily_revenue is not none %}
${{ "%.2f"|format(metrics.daily_revenue) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_daily_revenue"></span>
</p>
<p>
<strong>Daily Power Cost:</strong>
<span id="daily_power_cost" class="metric-value red">
{% if metrics and metrics.daily_power_cost is defined and metrics.daily_power_cost is not none %}
${{ "%.2f"|format(metrics.daily_power_cost) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_daily_power_cost"></span>
</p>
<p>
<strong>Daily Profit (USD):</strong>
<span id="daily_profit_usd" class="metric-value green">
{% if metrics and metrics.daily_profit_usd is defined and metrics.daily_profit_usd is not none %}
${{ "%.2f"|format(metrics.daily_profit_usd) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_daily_profit_usd"></span>
</p>
<p>
<strong>Monthly Profit (USD):</strong>
<span id="monthly_profit_usd" class="metric-value green">
{% if metrics and metrics.monthly_profit_usd is defined and metrics.monthly_profit_usd is not none %}
${{ "%.2f"|format(metrics.monthly_profit_usd) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_monthly_profit_usd"></span>
</p>
</div>
</div>
</div>
</div>
<div class="col-md-6">
<div class="card">
<div class="card-header">USD EARNINGS</div>
<div class="card-body">
<p>
<strong>Daily Revenue:</strong>
<span id="daily_revenue" class="metric-value green">
{% if metrics and metrics.daily_revenue is defined and metrics.daily_revenue is not none %}
${{ "%.2f"|format(metrics.daily_revenue) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_daily_revenue"></span>
</p>
<p>
<strong>Daily Power Cost:</strong>
<span id="daily_power_cost" class="metric-value red">
{% if metrics and metrics.daily_power_cost is defined and metrics.daily_power_cost is not none %}
${{ "%.2f"|format(metrics.daily_power_cost) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_daily_power_cost"></span>
</p>
<p>
<strong>Daily Profit (USD):</strong>
<span id="daily_profit_usd" class="metric-value green">
{% if metrics and metrics.daily_profit_usd is defined and metrics.daily_profit_usd is not none %}
${{ "%.2f"|format(metrics.daily_profit_usd) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_daily_profit_usd"></span>
</p>
<p>
<strong>Monthly Profit (USD):</strong>
<span id="monthly_profit_usd" class="metric-value green">
{% if metrics and metrics.monthly_profit_usd is defined and metrics.monthly_profit_usd is not none %}
${{ "%.2f"|format(metrics.monthly_profit_usd) }}
{% else %}
$0.00
{% endif %}
</span>
<span id="indicator_monthly_profit_usd"></span>
</p>
</div>
</div>
<!-- Payout & Misc -->
<div class="row">
<div class="col-12">
<div class="card" id="payoutMiscCard">
<div class="card-header">Payout &amp; Misc</div>
<div class="card-body">
<p>
<strong>Unpaid Earnings:</strong>
<span id="unpaid_earnings" class="metric-value green">
{% if metrics and metrics.unpaid_earnings %}
{{ metrics.unpaid_earnings }} BTC
{% else %}
0 BTC
{% endif %}
</span>
<span id="indicator_unpaid_earnings"></span>
</p>
<p>
<strong>Last Block:</strong>
<span id="last_block_height" class="metric-value white">
{{ metrics.last_block_height if metrics and metrics.last_block_height else "N/A" }}
</span>
<span id="last_block_time" class="metric-value blue">
{{ metrics.last_block_time if metrics and metrics.last_block_time else "N/A" }}
</span>
<span class="green">
{% if metrics and metrics.last_block_earnings %}
+{{ metrics.last_block_earnings|int|commafy }} sats
{% else %}
+0 sats
{% endif %}
</span>
<span id="indicator_last_block"></span>
</p>
<p>
<strong>Est. Time to Payout:</strong>
<span id="est_time_to_payout" class="metric-value yellow">
{{ metrics.est_time_to_payout if metrics and metrics.est_time_to_payout else "N/A" }}
</span>
<span id="indicator_est_time_to_payout"></span>
</p>
<p>
<strong>Blocks Found:</strong>
<span id="blocks_found" class="metric-value white">
{{ metrics.blocks_found if metrics and metrics.blocks_found else "0" }}
</span>
<span id="indicator_blocks_found"></span>
</p>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@ -12,10 +12,10 @@
<body>
<div class="container">
<div class="error-container">
<h1>ERROR!</h1>
<div class="error-code">CODE: SYS_EXCEPTION_0x69420</div>
<h1>ERROR</h1>
<div class="error-code">CODE: SYS_EXCEPTION_0x45</div>
<p>{{ message }}<span class="terminal-cursor"></span></p>
<a href="/dashboard" class="btn btn-primary">RETURN TO DASHBOARD</a>
<a href="/" class="btn btn-primary">Return to Dashboard</a>
</div>
</div>
</body>

View File

@ -1,95 +0,0 @@
{% extends "base.html" %}
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD {% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/css/notifications.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %}
{% block header %}NOTIFICATION CENTER v 0.1{% endblock %}
{% block notifications_active %}active{% endblock %}
{% block content %}
<!-- Notification Controls -->
<div class="row mb-2">
<div class="col-12">
<div class="card">
<div class="card-header">NOTIFICATION CONTROLS</div>
<div class="card-body">
<div class="notification-controls">
<div class="filter-buttons">
<button class="filter-button active" data-filter="all">All</button>
<button class="filter-button" data-filter="hashrate">Hashrate</button>
<button class="filter-button" data-filter="block">Blocks</button>
<button class="filter-button" data-filter="worker">Workers</button>
<button class="filter-button" data-filter="earnings">Earnings</button>
<button class="filter-button" data-filter="system">System</button>
</div>
<div class="notification-actions">
<button id="mark-all-read" class="action-button">Mark All as Read</button>
<button id="clear-read" class="action-button">Clear Read Notifications</button>
<button id="clear-all" class="action-button danger">Clear All</button>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Notifications List -->
<div class="row">
<div class="col-12">
<div class="card">
<div class="card-header">
<span>NOTIFICATIONS</span>
<span id="unread-badge" class="unread-badge">0</span>
</div>
<div class="card-body">
<div id="notifications-container">
<!-- Notifications will be populated here by JavaScript -->
<div class="loading-message">Loading notifications<span class="terminal-cursor"></span></div>
</div>
<!-- Pagination -->
<div class="pagination-controls">
<button id="load-more" class="load-more-button">LOAD MORE</button>
</div>
</div>
</div>
</div>
</div>
<!-- Empty State Template (hidden) -->
<div id="empty-template" style="display:none;">
<div class="empty-state">
<i class="fas fa-bell-slash"></i>
<p>No notifications to display</p>
</div>
</div>
<!-- Notification Template (hidden) -->
<div id="notification-template" style="display:none;">
<div class="notification-item" data-id="">
<div class="notification-icon">
<i class="fas"></i>
</div>
<div class="notification-content">
<div class="notification-message"></div>
<div class="notification-meta">
<span class="notification-time"></span>
<span class="notification-category"></span>
</div>
</div>
<div class="notification-actions">
<button class="mark-read-button"><i class="fas fa-check"></i></button>
<button class="delete-button"><i class="fas fa-trash"></i></button>
</div>
</div>
</div>
{% endblock %}
{% block javascript %}
<script src="/static/js/notifications.js"></script>
{% endblock %}

View File

@ -1,96 +1,103 @@
{% extends "base.html" %}
{% block title %}WORKERS - BTC-OS MINING DASHBOARD {% endblock %}
{% block title %}Workers Overview - Ocean.xyz Pool Mining Dashboard{% endblock %}
{% block css %}
<link rel="stylesheet" href="/static/css/workers.css">
<link rel="stylesheet" href="/static/css/theme-toggle.css">
{% endblock %}
{% block header %}WORKERS OVERVIEW{% endblock %}
{% block header %}Workers Overview{% endblock %}
{% block workers_active %}active{% endblock %}
{% block content %}
<!-- Summary statistics -->
<div class="row mb-3">
<div class="col-md-12">
<div class="card">
<div class="card-header">MINER SUMMARY</div>
<div class="card-body">
<div class="summary-stats">
<div class="summary-stat">
<div class="worker-ring" style="--online-percent: {{ workers_online / workers_total if workers_total > 0 else 0 }}">
<div class="worker-ring-inner">
<span id="workers-count">{{ workers_total }}</span>
</div>
</div>
<div class="summary-stat-label">WORKERS</div>
<div>
<span class="green-glow" id="workers-online">{{ workers_online }}</span> /
<span class="red-glow" id="workers-offline">{{ workers_offline }}</span>
</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value white-glow" id="total-hashrate">
{% if total_hashrate is defined %}
{{ "%.1f"|format(total_hashrate) }} {{ hashrate_unit }}
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">TOTAL HASHRATE</div>
<div class="mini-chart">
<canvas id="total-hashrate-chart"></canvas>
</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value green-glow" id="total-earnings">
{% if total_earnings is defined %}
{{ "%.8f"|format(total_earnings) }} BTC
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">UNPAID EARNINGS</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value yellow-glow" id="daily-sats">
{% if daily_sats is defined %}
{{ daily_sats|commafy }} SATS
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">DAILY SATS</div>
</div>
</div>
<div class="col-md-12">
<div class="card">
<div class="card-header">Miner Summary</div>
<div class="card-body">
<div class="summary-stats">
<div class="summary-stat">
<div class="worker-ring" style="--online-percent: {{ workers_online / workers_total if workers_total > 0 else 0 }}">
<div class="worker-ring-inner">
<span id="workers-count">{{ workers_total }}</span>
</div>
</div>
<div class="summary-stat-label">Workers</div>
<div>
<span class="green-glow" id="workers-online">{{ workers_online }}</span> /
<span class="red-glow" id="workers-offline">{{ workers_offline }}</span>
</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value white-glow" id="total-hashrate">
{% if total_hashrate is defined %}
{{ "%.1f"|format(total_hashrate) }} {{ hashrate_unit }}
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Total Hashrate</div>
<div class="mini-chart">
<canvas id="total-hashrate-chart"></canvas>
</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value green-glow" id="total-earnings">
{% if total_earnings is defined %}
{{ "%.8f"|format(total_earnings) }} BTC
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Lifetime Earnings</div>
</div>
<div class="summary-stat">
<div class="summary-stat-value yellow-glow" id="daily-sats">
{% if daily_sats is defined %}
{{ daily_sats|commafy }} sats
{% else %}
N/A
{% endif %}
</div>
<div class="summary-stat-label">Daily Sats</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 Rate</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Controls bar -->
<div class="controls-bar">
<input type="text" class="search-box" id="worker-search" placeholder="Search workers...">
<div class="filter-buttons">
<button class="filter-button active" data-filter="all">ALL WORKERS</button>
<button class="filter-button" data-filter="online">ONLINE</button>
<button class="filter-button" data-filter="offline">OFFLINE</button>
<button class="filter-button" data-filter="asic">ASIC</button>
<button class="filter-button" data-filter="bitaxe">BITAXE</button>
</div>
<input type="text" class="search-box" id="worker-search" placeholder="Search workers...">
<div class="filter-buttons">
<button class="filter-button active" data-filter="all">All Workers</button>
<button class="filter-button" data-filter="online">Online</button>
<button class="filter-button" data-filter="offline">Offline</button>
<button class="filter-button" data-filter="asic">ASIC</button>
<button class="filter-button" data-filter="fpga">FPGA</button>
</div>
</div>
<!-- Workers grid -->
<div class="worker-grid" id="worker-grid">
<!-- Worker cards will be generated here via JavaScript -->
<div id="loader" class="text-center p-5" style="display:none;">
<i class="fas fa-spinner fa-spin"></i> Loading worker data...
</div>
<!-- Worker cards will be generated here via JavaScript -->
</div>
{% endblock %}

View File

@ -5,7 +5,6 @@ import logging
import random
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
from config import get_timezone
class WorkerService:
"""Service for retrieving and managing worker data."""
@ -26,10 +25,6 @@ class WorkerService:
dashboard_service (MiningDashboardService): The initialized dashboard service
"""
self.dashboard_service = dashboard_service
# Immediately access the wallet from dashboard_service when it's set
if hasattr(dashboard_service, 'wallet'):
self.wallet = dashboard_service.wallet
logging.info(f"Worker service updated with new wallet: {self.wallet}")
logging.info("Dashboard service connected to worker service")
def generate_default_workers_data(self):
@ -48,8 +43,9 @@ class WorkerService:
"hashrate_unit": "TH/s",
"total_earnings": 0.0,
"daily_sats": 0,
"avg_acceptance_rate": 0.0,
"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):
@ -286,7 +282,7 @@ class WorkerService:
dict: Default worker data
"""
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
hashrate = round(random.uniform(50, 100), 2) if is_online else 0
@ -306,6 +302,7 @@ class WorkerService:
"efficiency": round(random.uniform(80, 95), 1) if is_online else 0,
"last_share": last_share,
"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,
"temperature": round(random.uniform(55, 75)) if is_online else 0
}
@ -314,10 +311,10 @@ class WorkerService:
"""
Generate fallback worker data from cached metrics when real data can't be fetched.
Try to preserve real worker names if available.
Args:
cached_metrics (dict): Cached metrics from the dashboard
Returns:
dict: Generated worker data
"""
@ -325,16 +322,12 @@ class WorkerService:
if not cached_metrics:
logging.warning("No cached metrics available for worker fallback data")
return self.generate_default_workers_data()
# Check if we have workers_hashing information
workers_count = cached_metrics.get("workers_hashing")
# 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
workers_count = cached_metrics.get("workers_hashing", 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")
workers_count = 1
@ -439,8 +432,9 @@ class WorkerService:
"hashrate_unit": hashrate_unit,
"total_earnings": total_earnings,
"daily_sats": daily_sats, # Fixed daily_sats value
"avg_acceptance_rate": 95.0, # Default value
"hashrate_history": hashrate_history,
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
}
# Update cache
@ -471,20 +465,20 @@ class WorkerService:
# Worker model types for simulation
models = [
{"type": "ASIC", "model": "Bitmain Antminer S19 Pro", "max_hashrate": 110, "power": 3250},
{"type": "ASIC", "model": "Bitmain Antminer T21", "max_hashrate": 130, "power": 3276},
{"type": "ASIC", "model": "MicroBT Whatsminer M50S", "max_hashrate": 130, "power": 3276},
{"type": "ASIC", "model": "Bitmain Antminer S19j Pro", "max_hashrate": 104, "power": 3150},
{"type": "Bitaxe", "model": "Bitaxe Gamma 601", "max_hashrate": 3.2, "power": 35}
{"type": "FPGA", "model": "BitAxe FPGA Miner", "max_hashrate": 3.2, "power": 35}
]
# Calculate hashrate distribution - majority of hashrate to online workers
online_count = max(1, int(num_workers * 0.8)) # At least 1 online worker
offline_count = num_workers - online_count
# Average hashrate per online worker (ensure it's at least 0.5 TH/s)
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
# Average hashrate per online worker (ensure it's at least 1 TH/s)
avg_hashrate = max(1.0, total_hashrate / online_count if online_count > 0 else 0)
workers = []
current_time = datetime.now(ZoneInfo(get_timezone()))
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
# Default total unpaid earnings if not provided
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
@ -497,9 +491,9 @@ class WorkerService:
# For Antminers and regular ASICs, use ASIC model
if i < online_count - 1 or avg_hashrate > 5:
model_idx = random.randint(0, len(models) - 2) # Exclude Bitaxe for most workers
model_idx = random.randint(0, len(models) - 2) # Exclude FPGA for most workers
else:
model_idx = len(models) - 1 # Bitaxe for last worker if small hashrate
model_idx = len(models) - 1 # FPGA for last worker if small hashrate
model_info = models[model_idx]
@ -511,7 +505,10 @@ class WorkerService:
# Generate last share time (within last 5 minutes)
minutes_ago = random.randint(0, 5)
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)
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
@ -530,15 +527,16 @@ class WorkerService:
"efficiency": round(random.uniform(65, 95), 1),
"last_share": last_share,
"earnings": 0, # Will be set after all workers are generated
"acceptance_rate": acceptance_rate,
"power_consumption": model_info["power"],
"temperature": temperature
})
# Generate offline workers
for i in range(offline_count):
# Select a model - more likely to be Bitaxe for offline
# Select a model - more likely to be FPGA for offline
if random.random() > 0.6:
model_info = models[-1] # Bitaxe
model_info = models[-1] # FPGA
else:
model_info = random.choice(models[:-1]) # ASIC
@ -547,7 +545,7 @@ class WorkerService:
last_share = (current_time - timedelta(hours=hours_ago)).strftime("%Y-%m-%d %H:%M")
# Generate hashrate (historical before going offline)
if model_info["type"] == "Bitaxe":
if model_info["type"] == "FPGA":
hashrate_3hr = round(random.uniform(1, 3), 2)
else:
hashrate_3hr = round(random.uniform(20, 90), 2)
@ -568,6 +566,7 @@ class WorkerService:
"efficiency": 0,
"last_share": last_share,
"earnings": 0, # Minimal earnings for offline workers
"acceptance_rate": round(random.uniform(95, 99), 1),
"power_consumption": 0,
"temperature": 0
})
@ -615,24 +614,24 @@ class WorkerService:
# Worker model types for simulation
models = [
{"type": "ASIC", "model": "Bitmain Antminer S19k Pro", "max_hashrate": 110, "power": 3250},
{"type": "ASIC", "model": "Bitmain Antminer T21", "max_hashrate": 130, "power": 3276},
{"type": "ASIC", "model": "Bitmain Antminer S19 Pro", "max_hashrate": 110, "power": 3250},
{"type": "ASIC", "model": "MicroBT Whatsminer M50S", "max_hashrate": 130, "power": 3276},
{"type": "ASIC", "model": "Bitmain Antminer S19j Pro", "max_hashrate": 104, "power": 3150},
{"type": "Bitaxe", "model": "Bitaxe Gamma 601", "max_hashrate": 3.2, "power": 35}
{"type": "FPGA", "model": "BitAxe FPGA Miner", "max_hashrate": 3.2, "power": 35}
]
# Worker names for simulation - only used if no real worker names are provided
prefixes = ["Antminer", "Miner", "Rig", "Node", "Worker", "BitAxe", "BTC"]
prefixes = ["Antminer", "Whatsminer", "Miner", "Rig", "Node", "Worker", "BitAxe", "BTC"]
# Calculate hashrate distribution - majority of hashrate to online workers
online_count = max(1, int(num_workers * 0.8)) # At least 1 online worker
offline_count = num_workers - online_count
# Average hashrate per online worker (ensure it's at least 0.5 TH/s)
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
# Average hashrate per online worker (ensure it's at least 1 TH/s)
avg_hashrate = max(1.0, total_hashrate / online_count if online_count > 0 else 0)
workers = []
current_time = datetime.now(ZoneInfo(get_timezone()))
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
# Default total unpaid earnings if not provided
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
@ -654,9 +653,9 @@ class WorkerService:
# For Antminers and regular ASICs, use ASIC model
if i < online_count - 1 or avg_hashrate > 5:
model_idx = random.randint(0, len(models) - 2) # Exclude Bitaxe for most workers
model_idx = random.randint(0, len(models) - 2) # Exclude FPGA for most workers
else:
model_idx = len(models) - 1 # Bitaxe for last worker if small hashrate
model_idx = len(models) - 1 # FPGA for last worker if small hashrate
model_info = models[model_idx]
@ -665,10 +664,13 @@ class WorkerService:
hashrate_60sec = round(base_hashrate * random.uniform(0.9, 1.1), 2)
hashrate_3hr = round(base_hashrate * random.uniform(0.85, 1.0), 2)
# Generate last share time (within last 3 minutes)
minutes_ago = random.randint(0, 3)
# Generate last share time (within last 5 minutes)
minutes_ago = random.randint(0, 5)
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)
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
@ -677,7 +679,7 @@ class WorkerService:
name = name_list[i]
else:
# Create a unique name
if model_info["type"] == "Bitaxe":
if model_info["type"] == "FPGA":
name = f"{prefixes[-1]}{random.randint(1, 99):02d}"
else:
name = f"{random.choice(prefixes[:-1])}{random.randint(1, 99):02d}"
@ -694,15 +696,16 @@ class WorkerService:
"efficiency": round(random.uniform(65, 95), 1),
"last_share": last_share,
"earnings": 0, # Will be set after all workers are generated
"acceptance_rate": acceptance_rate,
"power_consumption": model_info["power"],
"temperature": temperature
})
# Generate offline workers
for i in range(offline_count):
# Select a model - more likely to be Bitaxe for offline
# Select a model - more likely to be FPGA for offline
if random.random() > 0.6:
model_info = models[-1] # Bitaxe
model_info = models[-1] # FPGA
else:
model_info = random.choice(models[:-1]) # ASIC
@ -711,7 +714,7 @@ class WorkerService:
last_share = (current_time - timedelta(hours=hours_ago)).strftime("%Y-%m-%d %H:%M")
# Generate hashrate (historical before going offline)
if model_info["type"] == "Bitaxe":
if model_info["type"] == "FPGA":
hashrate_3hr = round(random.uniform(1, 3), 2)
else:
hashrate_3hr = round(random.uniform(20, 90), 2)
@ -722,7 +725,7 @@ class WorkerService:
name = name_list[idx]
else:
# Create a unique name
if model_info["type"] == "Bitaxe":
if model_info["type"] == "FPGA":
name = f"{prefixes[-1]}{random.randint(1, 99):02d}"
else:
name = f"{random.choice(prefixes[:-1])}{random.randint(1, 99):02d}"
@ -739,6 +742,7 @@ class WorkerService:
"efficiency": 0,
"last_share": last_share,
"earnings": 0, # Minimal earnings for offline workers
"acceptance_rate": round(random.uniform(95, 99), 1),
"power_consumption": 0,
"temperature": 0
})