Compare commits

..

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

35 changed files with 1178 additions and 4226 deletions

70
App.py
View File

@ -22,7 +22,6 @@ from config import load_config, save_config
from data_service import MiningDashboardService
from 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 +45,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')
@ -418,8 +417,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,12 +453,12 @@ 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 %H:%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")
return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time)
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %H:%M:%S %p")
return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time)# api/time endpoint
@app.route("/api/metrics")
def api_metrics():
@ -468,30 +467,18 @@ 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("%b %d, %Y, %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
@ -520,8 +507,8 @@ def api_workers():
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()
"server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat()
})
# --- New Config Endpoints ---
@ -599,7 +586,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 +604,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 +637,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
@ -794,7 +781,7 @@ def api_clear_notifications():
@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")
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%b %d, %Y, %I:%M:%S %p")
return render_template("notifications.html", current_time=current_time)
@app.errorhandler(404)
@ -821,42 +808,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

View File

@ -1,4 +1,4 @@
# DeepSea Dashboard
# Ocean.xyz Bitcoin Mining Dashboard
## A Retro Mining Monitoring Solution
@ -6,10 +6,11 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
---
## 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](https://github.com/user-attachments/assets/52d787ab-10d9-4c36-9cba-3ed8878dfa2b)
![dashboard](https://github.com/user-attachments/assets/1042b586-7f02-4514-83f6-1fee50c38b18)
![workers](https://github.com/user-attachments/assets/2d26dbd0-64b7-4f77-921c-c48ad2cb6122)
![blocks](https://github.com/user-attachments/assets/e38d6f17-5e89-4560-aeec-69a349fa12ba)
![notifications](https://github.com/user-attachments/assets/cb191fc5-fa85-49a6-a155-459c68008b8f)
---
@ -24,6 +25,7 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **Payout Monitoring**: View unpaid balance and estimated time to next payout
- **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
- **Status Monitoring**: Real-time status indicators for online and offline devices
@ -40,7 +42,6 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **Backup Polling**: Fallback to traditional polling if real-time connection fails
- **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 +49,14 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
- **System Monitor**: Floating status display with uptime and refresh information
- **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/Djobleezy/Custom-Ocean.xyz-Dashboard.git
cd Custom-Ocean.xyz-Dashboard
```
2. Install dependencies:
@ -73,12 +69,21 @@ 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 in [config.json](https://github.com/Djobleezy/Custom-Ocean.xyz-Dashboard/blob/main/config.json):
```json
{
"power_cost": 0.12,
"power_usage": 3450,
"wallet": "yourwallethere" <--- make sure to replace this value in all project files (boot.html, app.py, config.py, config.json, & setup.py)
}
```
5. Start the application:
```
python App.py
```
5. Open your browser at `http://localhost:5000`
6. Open your browser at `http://localhost:5000`
For detailed deployment instructions with Redis persistence and Gunicorn configuration, see [deployment_steps.md](deployment_steps.md).
@ -108,8 +113,6 @@ You can modify the following environment variables in the `docker-compose.yml` f
- `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.
@ -165,18 +168,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 +182,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 +192,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 +201,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 +226,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 +234,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": "yourwallethere"
}

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": "yourwallethere"
}
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,
@ -161,8 +136,8 @@ class MiningDashboardService:
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
@ -385,7 +360,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}")
@ -663,6 +638,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 +674,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
}
@ -837,8 +814,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")
@ -897,6 +875,7 @@ class MiningDashboardService:
"efficiency": 90.0,
"last_share": "N/A",
"earnings": 0,
"acceptance_rate": 95.0,
"power_consumption": 0,
"temperature": 0
}
@ -983,7 +962,8 @@ class MiningDashboardService:
'workers_online': workers_online,
'workers_offline': workers_offline,
'total_earnings': total_earnings,
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat()
'avg_acceptance_rate': 99.0,
'timestamp': datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
}
logging.info(f"Successfully retrieved {len(workers)} workers across multiple pages")
return result

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

@ -8,11 +8,6 @@ services:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
dashboard:
build: .
@ -21,22 +16,14 @@ services:
- "5000:5000"
environment:
- REDIS_URL=redis://redis:6379
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS
- WALLET=bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9
- 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
- redis
volumes:
redis_data:
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 \

247
minify.py
View File

@ -1,14 +1,6 @@
#!/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."""
@ -17,230 +9,25 @@ def minify_js_files():
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}")
input_path = os.path.join(js_dir, js_file)
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
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
print(f"Minified {js_file}")
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()
print(f"Total files minified: {minified_count}")
if __name__ == "__main__":
main()
minify_js_files()

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,42 +31,6 @@ 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:
@ -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

@ -6,13 +6,6 @@ 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"
@ -47,78 +40,51 @@ class NotificationService:
# 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."""
def _load_notifications(self):
"""Load notifications from persistent storage."""
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")
logging.info(f"Loaded {len(self.notifications)} notifications from storage")
except Exception as e:
logging.error(f"[NotificationService] Error loading notifications: {e}")
self.notifications = [] # Ensure we have a valid list
logging.error(f"Error loading notifications: {e}")
def _load_last_block_height(self) -> None:
def _load_last_block_height(self):
"""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}")
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
# Use Redis if available
last_height = self.state_manager.redis_client.get("last_block_height")
if last_height:
self.last_block_height = last_height.decode('utf-8')
logging.info(f"Loaded last block height from storage: {self.last_block_height}")
else:
logging.info("[NotificationService] No last block height found, starting with None")
logging.info("Redis not available, starting with no last block height")
except Exception as e:
logging.error(f"[NotificationService] Error loading last block height: {e}")
logging.error(f"Error loading last block height: {e}")
def _save_last_block_height(self) -> None:
def _save_last_block_height(self):
"""Save last block height to persistent storage."""
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 hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client and self.last_block_height:
self.state_manager.redis_client.set("last_block_height", str(self.last_block_height))
logging.info(f"Saved last block height to storage: {self.last_block_height}")
except Exception as e:
logging.error(f"Error saving last block height: {e}")
def _save_notifications(self):
"""Save notifications to persistent storage."""
try:
# Prune to max size before saving
if len(self.notifications) > self.max_notifications:
self.notifications.sort(key=lambda n: n.get("timestamp", ""), reverse=True)
self.notifications = self.notifications[:self.max_notifications]
self.notifications = self.notifications[-self.max_notifications:]
self.state_manager.save_notifications(self.notifications)
logging.info(f"[NotificationService] Saved {len(self.notifications)} notifications")
except Exception as e:
logging.error(f"[NotificationService] Error saving notifications: {e}")
logging.error(f"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]:
def add_notification(self, message, level=NotificationLevel.INFO, category=NotificationCategory.SYSTEM, data=None):
"""
Add a new notification.
@ -146,17 +112,12 @@ class NotificationService:
self.notifications.append(notification)
self._save_notifications()
logging.info(f"[NotificationService] Added notification: {message}")
logging.info(f"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]]:
def get_notifications(self, limit=50, offset=0, unread_only=False, category=None, level=None):
"""
Get filtered notifications with optimized filtering.
Get filtered notifications.
Args:
limit (int): Maximum number to return
@ -168,25 +129,31 @@ class NotificationService:
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)
]
filtered = self.notifications
# Apply filters
if unread_only:
filtered = [n for n in filtered if not n.get("read", False)]
if category:
filtered = [n for n in filtered if n.get("category") == category]
if level:
filtered = [n for n in filtered if n.get("level") == level]
# Sort by timestamp (newest first)
filtered = sorted(filtered, key=lambda n: n.get("timestamp", ""), reverse=True)
# Apply pagination
return filtered[offset:offset + limit]
paginated = filtered[offset:offset + limit]
return paginated
def get_unread_count(self) -> int:
def get_unread_count(self):
"""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:
def mark_as_read(self, notification_id=None):
"""
Mark notification(s) as read.
@ -202,18 +169,16 @@ class NotificationService:
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:
def delete_notification(self, notification_id):
"""
Delete a specific notification.
@ -223,19 +188,13 @@ class NotificationService:
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
self._save_notifications()
return True
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None) -> int:
def clear_notifications(self, category=None, older_than_days=None):
"""
Clear notifications with optimized filtering.
Clear notifications.
Args:
category (str, optional): Only clear specific category
@ -246,48 +205,44 @@ class NotificationService:
"""
original_count = len(self.notifications)
cutoff_date = None
if older_than_days:
if category and 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)
]
self.notifications = [
n for n in self.notifications
if n.get("category") != category or
datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date
]
elif category:
self.notifications = [n for n in self.notifications if n.get("category") != category]
elif older_than_days:
cutoff_date = datetime.now() - timedelta(days=older_than_days)
self.notifications = [
n for n in self.notifications
if datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date
]
else:
self.notifications = []
cleared_count = original_count - len(self.notifications)
if cleared_count > 0:
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
self._save_notifications()
return cleared_count
self._save_notifications()
return original_count - len(self.notifications)
def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
def check_and_generate_notifications(self, current_metrics, previous_metrics):
"""
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")
logging.warning("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}")
logging.info(f"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)
@ -318,7 +273,7 @@ class NotificationService:
return new_notifications
except Exception as e:
logging.error(f"[NotificationService] Error generating notifications: {e}")
logging.error(f"Error generating notifications: {e}")
error_notification = self.add_notification(
f"Error generating notifications: {str(e)}",
level=NotificationLevel.ERROR,
@ -326,28 +281,42 @@ class NotificationService:
)
return [error_notification]
def _should_post_daily_stats(self) -> bool:
"""Check if it's time to post daily stats with improved clarity."""
def _should_post_daily_stats(self):
"""Check if it's time to post daily stats (once per day at 12 PM)."""
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]]:
# Target time is 12 PM (noon)
target_hour = 12
current_hour = now.hour
current_minute = now.minute
# If we have a last_daily_stats timestamp
if self.last_daily_stats:
# Check if it's a different day
is_different_day = now.date() > self.last_daily_stats.date()
# Only post if:
# 1. It's a different day AND
# 2. It's the target hour (12 PM) AND
# 3. It's within the first 5 minutes of that hour
if is_different_day and current_hour == target_hour and current_minute < 5:
logging.info(f"Posting daily stats at {current_hour}:{current_minute}")
self.last_daily_stats = now
return True
else:
# First time - post if we're at the target hour
if current_hour == target_hour and current_minute < 5:
logging.info(f"First time posting daily stats at {current_hour}:{current_minute}")
self.last_daily_stats = now
return True
return False
def _generate_daily_stats(self, metrics):
"""Generate daily stats notification."""
try:
if not metrics:
logging.warning("[NotificationService] No metrics available for daily stats")
logging.warning("No metrics available for daily stats")
return None
# Format hashrate with appropriate unit
@ -362,7 +331,7 @@ class NotificationService:
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined (${daily_profit_usd:.2f})"
# Add notification
logging.info(f"[NotificationService] Generating daily stats notification: {message}")
logging.info(f"Generating daily stats notification: {message}")
return self.add_notification(
message,
level=NotificationLevel.INFO,
@ -375,16 +344,16 @@ class NotificationService:
}
)
except Exception as e:
logging.error(f"[NotificationService] Error generating daily stats notification: {e}")
logging.error(f"Error generating daily stats notification: {e}")
return None
def _generate_block_notification(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _generate_block_notification(self, metrics):
"""Generate notification for a new block found."""
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}")
logging.info(f"Generating block notification: height={last_block_height}, earnings={last_block_earnings}")
message = f"New block found by the pool! Block #{last_block_height}, earnings: {last_block_earnings} SATS"
@ -398,95 +367,87 @@ class NotificationService:
}
)
except Exception as e:
logging.error(f"[NotificationService] Error generating block notification: {e}")
logging.error(f"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]]:
def _check_hashrate_change(self, current, previous):
"""Check for significant hashrate changes using 10-minute average."""
try:
# Get 10min hashrate values
# Change from 3hr to 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}")
logging.debug(f"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")
logging.debug("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}")
# Handle strings with units (e.g., "10.5 TH/s")
if isinstance(current_10min, str):
current_10min = float(current_10min.split()[0])
else:
current_10min = float(current_10min)
if isinstance(previous_10min, str):
previous_10min = float(previous_10min.split()[0])
else:
previous_10min = float(previous_10min)
logging.debug(f"Converted 10min hashrates - current: {current_10min}, previous: {previous_10min}")
# Skip if previous was zero (prevents division by zero)
if previous_value == 0:
logging.debug("[NotificationService] Skipping hashrate check - previous was zero")
if previous_10min == 0:
logging.debug("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}%")
percent_change = ((current_10min - previous_10min) / previous_10min) * 100
logging.debug(f"10min hashrate change: {percent_change:.1f}%")
# Significant decrease
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
# Significant decrease (more than 25%)
if percent_change <= -25:
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease"
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
logging.info(f"Generating hashrate notification: {message}")
return self.add_notification(
message,
level=NotificationLevel.WARNING,
category=NotificationCategory.HASHRATE,
data={
"previous": previous_value,
"current": current_value,
"previous": previous_10min,
"current": current_10min,
"change": percent_change,
"timeframe": "10min"
"timeframe": "10min" # Add timeframe to the data
}
)
# Significant increase
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
# Significant increase (more than 25%)
elif percent_change >= 25:
message = f"10min hashrate increase detected: {percent_change:.1f}% increase"
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
logging.info(f"Generating hashrate notification: {message}")
return self.add_notification(
message,
level=NotificationLevel.SUCCESS,
category=NotificationCategory.HASHRATE,
data={
"previous": previous_value,
"current": current_value,
"previous": previous_10min,
"current": current_10min,
"change": percent_change,
"timeframe": "10min"
"timeframe": "10min" # Add timeframe to the data
}
)
return None
except Exception as e:
logging.error(f"[NotificationService] Error checking hashrate change: {e}")
logging.error(f"Error checking hashrate change: {e}")
return None
def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
def _check_earnings_progress(self, current, previous):
"""Check for significant earnings progress or payout approach."""
try:
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0"))
current_unpaid = float(current.get("unpaid_earnings", "0").split()[0]) if isinstance(current.get("unpaid_earnings"), str) else current.get("unpaid_earnings", 0)
# Check if approaching payout
if current.get("est_time_to_payout"):
@ -519,7 +480,7 @@ class NotificationService:
# Check for payout (unpaid balance reset)
if previous.get("unpaid_earnings"):
previous_unpaid = self._parse_numeric_value(previous.get("unpaid_earnings", "0"))
previous_unpaid = float(previous.get("unpaid_earnings", "0").split()[0]) if isinstance(previous.get("unpaid_earnings"), str) else previous.get("unpaid_earnings", 0)
# If balance significantly decreased, likely a payout occurred
if previous_unpaid > 0 and current_unpaid < previous_unpaid * 0.5:
@ -537,12 +498,12 @@ class NotificationService:
return None
except Exception as e:
logging.error(f"[NotificationService] Error checking earnings progress: {e}")
logging.error(f"Error checking earnings progress: {e}")
return None
def _should_send_payout_notification(self) -> bool:
def _should_send_payout_notification(self):
"""Check if enough time has passed since the last payout notification."""
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
return time_since_last_notification.total_seconds() > 86400 # 1 Day

View File

@ -5,29 +5,22 @@ This document provides a comprehensive overview of the Bitcoin Mining Dashboard
## Directory Structure
```
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

@ -60,7 +60,6 @@ FILE_MAPPINGS = {
'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',
@ -68,7 +67,6 @@ FILE_MAPPINGS = {
'blocks.js': 'static/js/blocks.js',
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
'notifications.js': 'static/js/notifications.js',
'theme.js': 'static/js/theme.js', # Added theme.js
# Template files
'base.html': 'templates/base.html',
@ -84,9 +82,7 @@ FILE_MAPPINGS = {
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": "yourwallethere"
}
def parse_arguments():
@ -96,13 +92,10 @@ def parse_arguments():
parser.add_argument('--wallet', type=str, help='Set your Ocean.xyz wallet address')
parser.add_argument('--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():
@ -282,19 +275,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 +288,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

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
@ -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:

View File

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

View File

@ -113,6 +113,7 @@
.metric-value {
color: var(--text-color);
font-weight: bold;
text-shadow: 0 0 6px #32cd32;
}
/* Yellow color family (BTC price, sats metrics, time to payout) */
@ -124,6 +125,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 +134,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) */
@ -150,16 +154,19 @@
#last_block_height,
#pool_fees_percentage {
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 {
@ -218,30 +225,14 @@
}
.datum-label {
color: #ffffff; /* White color */
font-size: 0.95em;
color: #ff9d00; /* Orange color */
font-size: 0.85em;
font-weight: bold;
text-transform: uppercase;
margin-left: 4px;
padding: 2px 5px;
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;
}
letter-spacing: 0.5px;
vertical-align: middle;
}

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

@ -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;

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 */

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,17 +7,10 @@ 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');
@ -48,34 +41,6 @@ $(document).ready(() => {
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;
@ -139,36 +104,14 @@ function refreshNotifications() {
}
}
// This refreshes all timestamps on the page periodically
// Update notification timestamps to relative time
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);
}
const timestamp = new Date(timestampStr);
const relativeTime = formatTimestamp(timestamp);
$(this).find('.notification-time').text(relativeTime);
}
});
}
@ -247,28 +190,14 @@ function createNotificationElement(notification) {
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
// Append "Z" to indicate UTC if not present
let utcTimestampStr = notification.timestamp;
if (!utcTimestampStr.endsWith('Z')) {
utcTimestampStr += 'Z';
}
const utcDate = new Date(utcTimestampStr);
// Format the timestamp using the configured timezone
// Convert UTC date to Los Angeles time with a timezone name for clarity
const options = {
year: 'numeric',
month: 'short',
@ -276,25 +205,16 @@ function createNotificationElement(notification) {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
hour12: true
};
const fullTimestamp = utcDate.toLocaleString('en-US', options);
// 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
// Append the full timestamp to the notification message
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-time').text(formatTimestamp(utcDate));
element.find('.notification-category').text(notification.category);
// Set up action buttons
@ -315,21 +235,10 @@ function createNotificationElement(notification) {
return element;
}
// Format timestamp as relative time
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 diffMs = now - timestamp;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
@ -344,14 +253,17 @@ function formatTimestamp(timestamp) {
} else if (diffDay < 30) {
return `${diffDay}d ago`;
} else {
// Format as date for older notifications using configured timezone
// Format as date for older notifications
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
};
return dateObj.toLocaleDateString('en-US', options);
return timestamp.toLocaleDateString('en-US', options);
}
}

View File

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

View File

@ -92,40 +92,6 @@ $(document).ready(function () {
});
});
// Load timezone setting early
(function loadTimezoneEarly() {
// First try to get from localStorage for instant access
try {
const storedTimezone = localStorage.getItem('dashboardTimezone');
if (storedTimezone) {
window.dashboardTimezone = storedTimezone;
console.log(`Using cached timezone: ${storedTimezone}`);
}
} catch (e) {
console.error("Error reading timezone from localStorage:", e);
}
// Then fetch from server to ensure we have the latest setting
fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
window.dashboardTimezone = data.timezone;
console.log(`Set timezone from server: ${data.timezone}`);
// Cache for future use
try {
localStorage.setItem('dashboardTimezone', data.timezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
}
})
.catch(error => {
console.error("Error fetching timezone:", error);
});
})();
// Initialize page elements
function initializePage() {
console.log("Initializing page elements...");
@ -355,40 +321,20 @@ function createWorkerCard(worker) {
</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 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>
`);
@ -440,6 +386,7 @@ function updateSummaryStats() {
$('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`);
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`);
$('#avg-acceptance-rate').text(`${(workerData.avg_acceptance_rate || 0).toFixed(2)}%`);
}
// Initialize mini chart
@ -527,11 +474,6 @@ 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',
@ -539,22 +481,12 @@ function updateLastUpdated() {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZone: configuredTimezone // Explicitly use the configured timezone
hour12: true
};
// 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('en-US', options) + "<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 %}BTC-OS MINING DASHBOARD v 0.3{% endblock %}</title>
<!-- Common fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
@ -17,79 +17,10 @@
<!-- 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>
@ -103,11 +34,6 @@
<!-- 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>
{% endblock %}
@ -131,11 +57,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 +64,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,10 +1,9 @@
{% extends "base.html" %}
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD {% endblock %}
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD v 0.3{% 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 %}
@ -64,7 +63,7 @@
<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>

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">
@ -90,40 +228,6 @@ v.21
</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>
</div>
<div id="form-message"></div>
<div class="form-actions">
<button class="btn btn-secondary" id="use-defaults">Use Defaults</button>
@ -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;
@ -306,45 +269,40 @@ v.21
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: "yourwallethere",
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,6 +328,33 @@ v.21
});
});
// Save configuration
function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const updatedConfig = {
wallet: wallet || currentConfig.wallet,
power_cost: powerCost,
power_usage: powerUsage
};
return fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConfig)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to save configuration');
}
return response.json();
});
}
// Safety timeout: redirect after 120 seconds if boot not complete
window.addEventListener('load', function () {
setTimeout(function () {
@ -399,19 +384,29 @@ v.21
});
});
// Update Use Defaults button handler
// Replace the current Use Defaults button event listener with this fixed version
document.getElementById('use-defaults').addEventListener('click', function () {
// 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);
@ -607,7 +598,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 %}BTC-OS Mining Dashboard v 0.3{% 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 %}
@ -41,11 +40,18 @@
<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" }}
<strong>Pool Fees:</strong>
<span id="pool_fees_percentage" class="metric-value">
{% if metrics and metrics.pool_fees_percentage is defined %}
{{ metrics.pool_fees_percentage }}%
{% if metrics.pool_fees_percentage >= 0.9 and metrics.pool_fees_percentage <= 1.3 %}
<span class="fee-star"></span> <span class="datum-label">DATUM</span>
{% endif %}
{% else %}
N/A
{% endif %}
</span>
<span id="indicator_blocks_found"></span>
<span id="indicator_pool_fees_percentage"></span>
</p>
</div>
</div>
@ -93,18 +99,11 @@
<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 %}
<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_pool_fees_percentage"></span>
<span id="indicator_blocks_found"></span>
</p>
</div>
</div>
@ -118,7 +117,7 @@
<div class="card-header">Pool Hashrates</div>
<div class="card-body">
<p>
<strong>Pool Hashrate:</strong>
<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:] }}
@ -201,17 +200,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">
@ -223,6 +211,17 @@
</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">
@ -257,7 +256,7 @@
<div class="card-header">SATOSHI EARNINGS</div>
<div class="card-body">
<p>
<strong>Projected Daily (Net):</strong>
<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
@ -268,7 +267,7 @@
<span id="indicator_daily_mined_sats"></span>
</p>
<p>
<strong>Projected Monthly (Net):</strong>
<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

View File

@ -1,10 +1,9 @@
{% extends "base.html" %}
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD {% endblock %}
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD v 0.3{% 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 %}

View File

@ -1,10 +1,9 @@
{% extends "base.html" %}
{% block title %}WORKERS - BTC-OS MINING DASHBOARD {% endblock %}
{% block title %}WORKERS - BTC-OS MINING DASHBOARD v 0.3{% 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 %}
@ -67,6 +66,17 @@
</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</div>
</div>
</div>
</div>
</div>

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."""
@ -48,8 +47,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 +286,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 +306,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 +315,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 +326,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 +436,9 @@ class WorkerService:
"hashrate_unit": hashrate_unit,
"total_earnings": total_earnings,
"daily_sats": daily_sats, # Fixed daily_sats value
"avg_acceptance_rate": 98.8, # Default value
"hashrate_history": hashrate_history,
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
}
# Update cache
@ -484,7 +482,7 @@ class WorkerService:
avg_hashrate = max(0.5, 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:
@ -511,7 +509,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,6 +531,7 @@ 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
})
@ -568,6 +570,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
})
@ -632,7 +635,7 @@ class WorkerService:
avg_hashrate = max(0.5, 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:
@ -669,6 +672,9 @@ class WorkerService:
minutes_ago = random.randint(0, 3)
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)
@ -694,6 +700,7 @@ 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
})
@ -739,6 +746,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
})