mirror of
https://github.com/Retropex/custom-ocean.xyz-dashboard.git
synced 2025-05-12 19:20:45 +02:00
Compare commits
55 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b9c04a39e8 | ||
![]() |
6ba7545278 | ||
![]() |
50b5241812 | ||
![]() |
54957babc3 | ||
![]() |
4c4750cb24 | ||
![]() |
2021583951 | ||
![]() |
00d3bbdee9 | ||
![]() |
3ebef744dc | ||
![]() |
f302d35945 | ||
![]() |
28099c37ec | ||
![]() |
2cb166b405 | ||
![]() |
259c877f69 | ||
![]() |
3d62c42bbf | ||
![]() |
076fba75c8 | ||
![]() |
a3acee1782 | ||
![]() |
a71a6ce03a | ||
![]() |
f617342c23 | ||
![]() |
2b09ad6c15 | ||
![]() |
f8514eb35f | ||
![]() |
f2ddcdd63a | ||
![]() |
f1bf5d0582 | ||
![]() |
5770c96bf7 | ||
![]() |
a02837b28c | ||
![]() |
41883f3f9c | ||
![]() |
cb24f54685 | ||
![]() |
e02622d600 | ||
![]() |
a802880011 | ||
![]() |
df1678fac7 | ||
![]() |
6d3f873d6b | ||
![]() |
c7e2f0f4a9 | ||
![]() |
65d4deba5e | ||
![]() |
af3ea9607e | ||
![]() |
eb95e6c6b5 | ||
![]() |
f5e93f436b | ||
![]() |
e87993a252 | ||
![]() |
0d0a707019 | ||
![]() |
2142a7d2af | ||
![]() |
231a56b18a | ||
![]() |
8c1c55c83f | ||
![]() |
bdb9552576 | ||
![]() |
b8321fe3b0 | ||
![]() |
ce3b186dc5 | ||
![]() |
eab3e89a11 | ||
![]() |
7267244e94 | ||
![]() |
3bb74c37e7 | ||
![]() |
9a9f9ae178 | ||
![]() |
4f52697185 | ||
![]() |
f6b3fdb094 | ||
![]() |
f1eb0e22b9 | ||
![]() |
ee469866d7 | ||
![]() |
6d07060b7e | ||
![]() |
f166126525 | ||
![]() |
97fe19d61d | ||
![]() |
96a71ec80d | ||
![]() |
9a5c93036a |
70
App.py
70
App.py
@ -22,6 +22,7 @@ from config import load_config, save_config
|
|||||||
from data_service import MiningDashboardService
|
from data_service import MiningDashboardService
|
||||||
from worker_service import WorkerService
|
from worker_service import WorkerService
|
||||||
from state_manager import StateManager, arrow_history, metrics_log
|
from state_manager import StateManager, arrow_history, metrics_log
|
||||||
|
from config import get_timezone
|
||||||
|
|
||||||
# Initialize Flask app
|
# Initialize Flask app
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
@ -45,7 +46,7 @@ scheduler_recreate_lock = threading.Lock()
|
|||||||
scheduler = None
|
scheduler = None
|
||||||
|
|
||||||
# Global start time
|
# Global start time
|
||||||
SERVER_START_TIME = datetime.now(ZoneInfo("America/Los_Angeles"))
|
SERVER_START_TIME = datetime.now(ZoneInfo(get_timezone()))
|
||||||
|
|
||||||
# Configure logging
|
# Configure logging
|
||||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
@ -417,8 +418,8 @@ def dashboard():
|
|||||||
# If still None after our attempt, create default metrics
|
# If still None after our attempt, create default metrics
|
||||||
if cached_metrics is None:
|
if cached_metrics is None:
|
||||||
default_metrics = {
|
default_metrics = {
|
||||||
"server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
|
"server_timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat(),
|
||||||
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat(),
|
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo(get_timezone())).isoformat(),
|
||||||
"hashrate_24hr": None,
|
"hashrate_24hr": None,
|
||||||
"hashrate_24hr_unit": "TH/s",
|
"hashrate_24hr_unit": "TH/s",
|
||||||
"hashrate_3hr": None,
|
"hashrate_3hr": None,
|
||||||
@ -453,12 +454,12 @@ def dashboard():
|
|||||||
"arrow_history": {}
|
"arrow_history": {}
|
||||||
}
|
}
|
||||||
logging.warning("Rendering dashboard with default metrics - no data available yet")
|
logging.warning("Rendering dashboard with default metrics - no data available yet")
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %H:%M:%S %p")
|
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%Y-%m-%d %H:%M:%S %p")
|
||||||
return render_template("dashboard.html", metrics=default_metrics, current_time=current_time)
|
return render_template("dashboard.html", metrics=default_metrics, current_time=current_time)
|
||||||
|
|
||||||
# If we have metrics, use them
|
# If we have metrics, use them
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %H:%M:%S %p")
|
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)# api/time endpoint
|
return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time)
|
||||||
|
|
||||||
@app.route("/api/metrics")
|
@app.route("/api/metrics")
|
||||||
def api_metrics():
|
def api_metrics():
|
||||||
@ -467,18 +468,30 @@ def api_metrics():
|
|||||||
update_metrics_job()
|
update_metrics_job()
|
||||||
return jsonify(cached_metrics)
|
return jsonify(cached_metrics)
|
||||||
|
|
||||||
|
@app.route("/api/available_timezones")
|
||||||
|
def available_timezones():
|
||||||
|
"""Return a list of available timezones."""
|
||||||
|
from zoneinfo import available_timezones
|
||||||
|
return jsonify({"timezones": sorted(available_timezones())})
|
||||||
|
|
||||||
|
@app.route('/api/timezone', methods=['GET'])
|
||||||
|
def get_timezone_config():
|
||||||
|
from flask import jsonify
|
||||||
|
from config import get_timezone
|
||||||
|
return jsonify({"timezone": get_timezone()})
|
||||||
|
|
||||||
# Add this new route to App.py
|
# Add this new route to App.py
|
||||||
@app.route("/blocks")
|
@app.route("/blocks")
|
||||||
def blocks_page():
|
def blocks_page():
|
||||||
"""Serve the blocks overview page."""
|
"""Serve the blocks overview page."""
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%b %d, %Y, %I:%M:%S %p")
|
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p")
|
||||||
return render_template("blocks.html", current_time=current_time)
|
return render_template("blocks.html", current_time=current_time)
|
||||||
|
|
||||||
# --- Workers Dashboard Route and API ---
|
# --- Workers Dashboard Route and API ---
|
||||||
@app.route("/workers")
|
@app.route("/workers")
|
||||||
def workers_dashboard():
|
def workers_dashboard():
|
||||||
"""Serve the workers overview dashboard page."""
|
"""Serve the workers overview dashboard page."""
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %I:%M:%S %p")
|
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%Y-%m-%d %I:%M:%S %p")
|
||||||
|
|
||||||
# Only get minimal worker stats for initial page load
|
# Only get minimal worker stats for initial page load
|
||||||
# Client-side JS will fetch the full data via API
|
# Client-side JS will fetch the full data via API
|
||||||
@ -507,8 +520,8 @@ def api_workers():
|
|||||||
def api_time():
|
def api_time():
|
||||||
"""API endpoint for server time."""
|
"""API endpoint for server time."""
|
||||||
return jsonify({ # correct time
|
return jsonify({ # correct time
|
||||||
"server_timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat(),
|
"server_timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat(),
|
||||||
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo("America/Los_Angeles")).isoformat()
|
"server_start_time": SERVER_START_TIME.astimezone(ZoneInfo(get_timezone())).isoformat()
|
||||||
})
|
})
|
||||||
|
|
||||||
# --- New Config Endpoints ---
|
# --- New Config Endpoints ---
|
||||||
@ -586,7 +599,7 @@ def update_config():
|
|||||||
def health_check():
|
def health_check():
|
||||||
"""Health check endpoint with enhanced system diagnostics."""
|
"""Health check endpoint with enhanced system diagnostics."""
|
||||||
# Calculate uptime
|
# Calculate uptime
|
||||||
uptime_seconds = (datetime.now(ZoneInfo("America/Los_Angeles")) - SERVER_START_TIME).total_seconds()
|
uptime_seconds = (datetime.now(ZoneInfo(get_timezone())) - SERVER_START_TIME).total_seconds()
|
||||||
|
|
||||||
# Get process memory usage
|
# Get process memory usage
|
||||||
try:
|
try:
|
||||||
@ -604,7 +617,7 @@ def health_check():
|
|||||||
if cached_metrics and cached_metrics.get("server_timestamp"):
|
if cached_metrics and cached_metrics.get("server_timestamp"):
|
||||||
try:
|
try:
|
||||||
last_update = datetime.fromisoformat(cached_metrics["server_timestamp"])
|
last_update = datetime.fromisoformat(cached_metrics["server_timestamp"])
|
||||||
data_age = (datetime.now(ZoneInfo("America/Los_Angeles")) - last_update).total_seconds()
|
data_age = (datetime.now(ZoneInfo(get_timezone())) - last_update).total_seconds()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error calculating data age: {e}")
|
logging.error(f"Error calculating data age: {e}")
|
||||||
|
|
||||||
@ -637,7 +650,7 @@ def health_check():
|
|||||||
"redis": {
|
"redis": {
|
||||||
"connected": state_manager.redis_client is not None
|
"connected": state_manager.redis_client is not None
|
||||||
},
|
},
|
||||||
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Log health check if status is not healthy
|
# Log health check if status is not healthy
|
||||||
@ -781,7 +794,7 @@ def api_clear_notifications():
|
|||||||
@app.route("/notifications")
|
@app.route("/notifications")
|
||||||
def notifications_page():
|
def notifications_page():
|
||||||
"""Serve the notifications page."""
|
"""Serve the notifications page."""
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%b %d, %Y, %I:%M:%S %p")
|
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p")
|
||||||
return render_template("notifications.html", current_time=current_time)
|
return render_template("notifications.html", current_time=current_time)
|
||||||
|
|
||||||
@app.errorhandler(404)
|
@app.errorhandler(404)
|
||||||
@ -808,17 +821,42 @@ class RobustMiddleware:
|
|||||||
start_response("500 Internal Server Error", [("Content-Type", "text/html")])
|
start_response("500 Internal Server Error", [("Content-Type", "text/html")])
|
||||||
return [b"<h1>Internal Server Error</h1>"]
|
return [b"<h1>Internal Server Error</h1>"]
|
||||||
|
|
||||||
|
@app.route("/api/reset-chart-data", methods=["POST"])
|
||||||
|
def reset_chart_data():
|
||||||
|
"""API endpoint to reset chart data history."""
|
||||||
|
try:
|
||||||
|
global arrow_history, state_manager
|
||||||
|
|
||||||
|
# Clear hashrate data from in-memory dictionary
|
||||||
|
hashrate_keys = ["hashrate_60sec", "hashrate_3hr", "hashrate_10min", "hashrate_24hr"]
|
||||||
|
for key in hashrate_keys:
|
||||||
|
if key in arrow_history:
|
||||||
|
arrow_history[key] = []
|
||||||
|
|
||||||
|
# Force an immediate save to Redis if available
|
||||||
|
if state_manager and hasattr(state_manager, 'redis_client') and state_manager.redis_client:
|
||||||
|
# Force save by overriding the time check
|
||||||
|
state_manager.last_save_time = 0
|
||||||
|
state_manager.save_graph_state()
|
||||||
|
logging.info("Chart data reset saved to Redis immediately")
|
||||||
|
|
||||||
|
return jsonify({"status": "success", "message": "Chart data reset successfully"})
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error resetting chart data: {e}")
|
||||||
|
return jsonify({"status": "error", "message": str(e)}), 500
|
||||||
|
|
||||||
# Add the middleware
|
# Add the middleware
|
||||||
app.wsgi_app = RobustMiddleware(app.wsgi_app)
|
app.wsgi_app = RobustMiddleware(app.wsgi_app)
|
||||||
|
|
||||||
# Update this section in App.py to properly initialize services
|
# Update this section in App.py to properly initialize services
|
||||||
|
|
||||||
# Initialize the dashboard service and worker service
|
# Initialize the dashboard service with network fee parameter
|
||||||
config = load_config()
|
config = load_config()
|
||||||
dashboard_service = MiningDashboardService(
|
dashboard_service = MiningDashboardService(
|
||||||
config.get("power_cost", 0.0),
|
config.get("power_cost", 0.0),
|
||||||
config.get("power_usage", 0.0),
|
config.get("power_usage", 0.0),
|
||||||
config.get("wallet")
|
config.get("wallet"),
|
||||||
|
network_fee=config.get("network_fee", 0.0) # Add network fee parameter
|
||||||
)
|
)
|
||||||
worker_service = WorkerService()
|
worker_service = WorkerService()
|
||||||
# Connect the services
|
# Connect the services
|
||||||
|
63
README.md
63
README.md
@ -1,4 +1,4 @@
|
|||||||
# Ocean.xyz Bitcoin Mining Dashboard
|
# DeepSea Dashboard
|
||||||
|
|
||||||
## A Retro Mining Monitoring Solution
|
## A Retro Mining Monitoring Solution
|
||||||
|
|
||||||
@ -6,11 +6,10 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
|||||||
|
|
||||||
---
|
---
|
||||||
## Gallery:
|
## Gallery:
|
||||||

|
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -25,7 +24,6 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
|||||||
- **Payout Monitoring**: View unpaid balance and estimated time to next payout
|
- **Payout Monitoring**: View unpaid balance and estimated time to next payout
|
||||||
- **Pool Fee Analysis**: Monitor pool fee percentages with visual indicator when optimal rates (0.9-1.3%) are detected
|
- **Pool Fee Analysis**: Monitor pool fee percentages with visual indicator when optimal rates (0.9-1.3%) are detected
|
||||||
|
|
||||||
|
|
||||||
### Worker Management
|
### Worker Management
|
||||||
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
|
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
|
||||||
- **Status Monitoring**: Real-time status indicators for online and offline devices
|
- **Status Monitoring**: Real-time status indicators for online and offline devices
|
||||||
@ -42,6 +40,7 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
|||||||
- **Backup Polling**: Fallback to traditional polling if real-time connection fails
|
- **Backup Polling**: Fallback to traditional polling if real-time connection fails
|
||||||
- **Cross-Tab Synchronization**: Data consistency across multiple browser tabs
|
- **Cross-Tab Synchronization**: Data consistency across multiple browser tabs
|
||||||
- **Server Health Monitoring**: Built-in watchdog processes ensure reliability
|
- **Server Health Monitoring**: Built-in watchdog processes ensure reliability
|
||||||
|
- **Error Handling**: Displays a user-friendly error page (`error.html`) for unexpected issues.
|
||||||
|
|
||||||
### Distinctive Design Elements
|
### Distinctive Design Elements
|
||||||
- **Retro Terminal Aesthetic**: Nostalgic interface with modern functionality
|
- **Retro Terminal Aesthetic**: Nostalgic interface with modern functionality
|
||||||
@ -49,14 +48,19 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
|||||||
- **System Monitor**: Floating status display with uptime and refresh information
|
- **System Monitor**: Floating status display with uptime and refresh information
|
||||||
- **Responsive Interface**: Adapts to desktop and mobile devices
|
- **Responsive Interface**: Adapts to desktop and mobile devices
|
||||||
|
|
||||||
|
### DeepSea Theme
|
||||||
|
- **Underwater Effects**: Light rays and digital noise create an immersive experience.
|
||||||
|
- **Retro Glitch Effects**: Subtle animations for a nostalgic feel.
|
||||||
|
- **Theme Toggle**: Switch between Bitcoin and DeepSea themes with a single click.
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
### Installation
|
### Installation
|
||||||
|
|
||||||
1. Clone the repository
|
1. Clone the repository
|
||||||
```
|
```
|
||||||
git clone https://github.com/Djobleezy/Custom-Ocean.xyz-Dashboard.git
|
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git
|
||||||
cd Custom-Ocean.xyz-Dashboard
|
cd DeepSea-Dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Install dependencies:
|
2. Install dependencies:
|
||||||
@ -69,21 +73,12 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
|
|||||||
python setup.py
|
python setup.py
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Configure your mining settings in [config.json](https://github.com/Djobleezy/Custom-Ocean.xyz-Dashboard/blob/main/config.json):
|
4. Start the application:
|
||||||
```json
|
|
||||||
{
|
|
||||||
"power_cost": 0.12,
|
|
||||||
"power_usage": 3450,
|
|
||||||
"wallet": "yourwallethere" <--- make sure to replace this value in all project files (boot.html, app.py, config.py, config.json, & setup.py)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Start the application:
|
|
||||||
```
|
```
|
||||||
python App.py
|
python App.py
|
||||||
```
|
```
|
||||||
|
|
||||||
6. Open your browser at `http://localhost:5000`
|
5. Open your browser at `http://localhost:5000`
|
||||||
|
|
||||||
For detailed deployment instructions with Redis persistence and Gunicorn configuration, see [deployment_steps.md](deployment_steps.md).
|
For detailed deployment instructions with Redis persistence and Gunicorn configuration, see [deployment_steps.md](deployment_steps.md).
|
||||||
|
|
||||||
@ -113,6 +108,8 @@ You can modify the following environment variables in the `docker-compose.yml` f
|
|||||||
- `WALLET`: Your Bitcoin wallet address.
|
- `WALLET`: Your Bitcoin wallet address.
|
||||||
- `POWER_COST`: Cost of power per kWh.
|
- `POWER_COST`: Cost of power per kWh.
|
||||||
- `POWER_USAGE`: Power usage in watts.
|
- `POWER_USAGE`: Power usage in watts.
|
||||||
|
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
|
||||||
|
- `TIMEZONE`: Local timezone for displaying time information.
|
||||||
|
|
||||||
Redis data is stored in a persistent volume (`redis_data`), and application logs are saved in the `./logs` directory.
|
Redis data is stored in a persistent volume (`redis_data`), and application logs are saved in the `./logs` directory.
|
||||||
|
|
||||||
@ -168,12 +165,18 @@ Built with a modern stack for reliability and performance:
|
|||||||
- **Resilience**: Automatic recovery mechanisms and state persistence
|
- **Resilience**: Automatic recovery mechanisms and state persistence
|
||||||
- **Configuration**: Environment variables and JSON-based settings
|
- **Configuration**: Environment variables and JSON-based settings
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
- `/api/metrics`: Provides real-time mining metrics.
|
||||||
|
- `/api/available_timezones`: Returns a list of supported timezones.
|
||||||
|
- `/api/config`: Fetches or updates the mining configuration.
|
||||||
|
- `/api/health`: Returns the health status of the application.
|
||||||
|
|
||||||
## Project Structure
|
## Project Structure
|
||||||
|
|
||||||
The project follows a modular architecture with clear separation of concerns:
|
The project follows a modular architecture with clear separation of concerns:
|
||||||
|
|
||||||
```
|
```
|
||||||
bitcoin-mining-dashboard/
|
DeepSea-Dashboard/
|
||||||
│
|
│
|
||||||
├── App.py # Main application entry point
|
├── App.py # Main application entry point
|
||||||
├── config.py # Configuration management
|
├── config.py # Configuration management
|
||||||
@ -182,9 +185,12 @@ bitcoin-mining-dashboard/
|
|||||||
├── models.py # Data models
|
├── models.py # Data models
|
||||||
├── state_manager.py # Manager for persistent state
|
├── state_manager.py # Manager for persistent state
|
||||||
├── worker_service.py # Service for worker data management
|
├── worker_service.py # Service for worker data management
|
||||||
|
├── notification_service.py # Service for notifications
|
||||||
|
├── minify.py # Script for minifying assets
|
||||||
├── setup.py # Setup script for organizing files
|
├── setup.py # Setup script for organizing files
|
||||||
├── requirements.txt # Python dependencies
|
├── requirements.txt # Python dependencies
|
||||||
├── Dockerfile # Docker configuration
|
├── Dockerfile # Docker configuration
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
│
|
│
|
||||||
├── templates/ # HTML templates
|
├── templates/ # HTML templates
|
||||||
│ ├── base.html # Base template with common elements
|
│ ├── base.html # Base template with common elements
|
||||||
@ -192,6 +198,7 @@ bitcoin-mining-dashboard/
|
|||||||
│ ├── dashboard.html # Main dashboard template
|
│ ├── dashboard.html # Main dashboard template
|
||||||
│ ├── workers.html # Workers dashboard template
|
│ ├── workers.html # Workers dashboard template
|
||||||
│ ├── blocks.html # Bitcoin blocks template
|
│ ├── blocks.html # Bitcoin blocks template
|
||||||
|
│ ├── notifications.html # Notifications template
|
||||||
│ └── error.html # Error page template
|
│ └── error.html # Error page template
|
||||||
│
|
│
|
||||||
├── static/ # Static assets
|
├── static/ # Static assets
|
||||||
@ -201,18 +208,24 @@ bitcoin-mining-dashboard/
|
|||||||
│ │ ├── workers.css # Workers page styles
|
│ │ ├── workers.css # Workers page styles
|
||||||
│ │ ├── boot.css # Boot sequence styles
|
│ │ ├── boot.css # Boot sequence styles
|
||||||
│ │ ├── blocks.css # Blocks page styles
|
│ │ ├── blocks.css # Blocks page styles
|
||||||
|
│ │ ├── notifications.css # Notifications page styles
|
||||||
│ │ ├── error.css # Error page styles
|
│ │ ├── error.css # Error page styles
|
||||||
│ │ └── retro-refresh.css # Floating refresh bar styles
|
│ │ ├── retro-refresh.css # Floating refresh bar styles
|
||||||
|
│ │ └── theme-toggle.css # Theme toggle styles
|
||||||
│ │
|
│ │
|
||||||
│ └── js/ # JavaScript files
|
│ └── js/ # JavaScript files
|
||||||
│ ├── main.js # Main dashboard functionality
|
│ ├── main.js # Main dashboard functionality
|
||||||
│ ├── workers.js # Workers page functionality
|
│ ├── workers.js # Workers page functionality
|
||||||
│ ├── blocks.js # Blocks page functionality
|
│ ├── blocks.js # Blocks page functionality
|
||||||
|
│ ├── notifications.js # Notifications functionality
|
||||||
│ ├── block-animation.js # Block mining animation
|
│ ├── block-animation.js # Block mining animation
|
||||||
│ └── BitcoinProgressBar.js # System monitor functionality
|
│ ├── BitcoinProgressBar.js # System monitor functionality
|
||||||
|
│ └── theme.js # Theme toggle functionality
|
||||||
│
|
│
|
||||||
├── deployment_steps.md # Deployment guide
|
├── deployment_steps.md # Deployment guide
|
||||||
└── project_structure.md # Additional structure documentation
|
├── project_structure.md # Additional structure documentation
|
||||||
|
├── LICENSE.md # License information
|
||||||
|
└── logs/ # Application logs (generated at runtime)
|
||||||
```
|
```
|
||||||
|
|
||||||
For more detailed information on the architecture and component interactions, see [project_structure.md](project_structure.md).
|
For more detailed information on the architecture and component interactions, see [project_structure.md](project_structure.md).
|
||||||
@ -226,6 +239,7 @@ For optimal performance:
|
|||||||
3. Use the system monitor to verify connection status
|
3. Use the system monitor to verify connection status
|
||||||
4. Access the health endpoint at `/api/health` for diagnostics
|
4. Access the health endpoint at `/api/health` for diagnostics
|
||||||
5. For stale data issues, use the Force Refresh function
|
5. For stale data issues, use the Force Refresh function
|
||||||
|
6. Use hotkey Shift+R to clear chart and Redis data (as needed, not required)
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
@ -234,5 +248,6 @@ Available under the MIT License. This is an independent project not affiliated w
|
|||||||
## Acknowledgments
|
## Acknowledgments
|
||||||
|
|
||||||
- Ocean.xyz mining pool for their service
|
- Ocean.xyz mining pool for their service
|
||||||
|
- mempool.guide
|
||||||
- The open-source community for their contributions
|
- The open-source community for their contributions
|
||||||
- Bitcoin protocol developers
|
- Bitcoin protocol developers
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"power_cost": 0.0,
|
"power_cost": 0.0,
|
||||||
"power_usage": 0.0,
|
"power_usage": 0.0,
|
||||||
"wallet": "yourwallethere"
|
"wallet": "yourwallethere",
|
||||||
|
"timezone": "America/Los_Angeles",
|
||||||
|
"network_fee": 0.0
|
||||||
}
|
}
|
||||||
|
35
config.py
35
config.py
@ -12,14 +12,13 @@ CONFIG_FILE = "config.json"
|
|||||||
def load_config():
|
def load_config():
|
||||||
"""
|
"""
|
||||||
Load configuration from file or return defaults if file doesn't exist.
|
Load configuration from file or return defaults if file doesn't exist.
|
||||||
|
|
||||||
Returns:
|
|
||||||
dict: Configuration dictionary with settings
|
|
||||||
"""
|
"""
|
||||||
default_config = {
|
default_config = {
|
||||||
"power_cost": 0.0,
|
"power_cost": 0.0,
|
||||||
"power_usage": 0.0,
|
"power_usage": 0.0,
|
||||||
"wallet": "yourwallethere"
|
"wallet": "yourwallethere",
|
||||||
|
"timezone": "America/Los_Angeles",
|
||||||
|
"network_fee": 0.0 # Add default network fee
|
||||||
}
|
}
|
||||||
|
|
||||||
if os.path.exists(CONFIG_FILE):
|
if os.path.exists(CONFIG_FILE):
|
||||||
@ -27,6 +26,12 @@ def load_config():
|
|||||||
with open(CONFIG_FILE, "r") as f:
|
with open(CONFIG_FILE, "r") as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
logging.info(f"Configuration loaded from {CONFIG_FILE}")
|
logging.info(f"Configuration loaded from {CONFIG_FILE}")
|
||||||
|
|
||||||
|
# Ensure network_fee is present even in existing config files
|
||||||
|
if "network_fee" not in config:
|
||||||
|
config["network_fee"] = default_config["network_fee"]
|
||||||
|
logging.info("Added missing network_fee to config with default value")
|
||||||
|
|
||||||
return config
|
return config
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading config: {e}")
|
logging.error(f"Error loading config: {e}")
|
||||||
@ -35,6 +40,28 @@ def load_config():
|
|||||||
|
|
||||||
return default_config
|
return default_config
|
||||||
|
|
||||||
|
def get_timezone():
|
||||||
|
"""
|
||||||
|
Get the configured timezone with fallback to default.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Timezone identifier
|
||||||
|
"""
|
||||||
|
# First check environment variable (for Docker)
|
||||||
|
import os
|
||||||
|
env_timezone = os.environ.get("TIMEZONE")
|
||||||
|
if env_timezone:
|
||||||
|
return env_timezone
|
||||||
|
|
||||||
|
# Then check config file
|
||||||
|
config = load_config()
|
||||||
|
timezone = config.get("timezone")
|
||||||
|
if timezone:
|
||||||
|
return timezone
|
||||||
|
|
||||||
|
# Default to Los Angeles
|
||||||
|
return "America/Los_Angeles"
|
||||||
|
|
||||||
def save_config(config):
|
def save_config(config):
|
||||||
"""
|
"""
|
||||||
Save configuration to file.
|
Save configuration to file.
|
||||||
|
@ -12,22 +12,25 @@ import requests
|
|||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
|
|
||||||
from models import OceanData, WorkerData, convert_to_ths
|
from models import OceanData, WorkerData, convert_to_ths
|
||||||
|
from config import get_timezone
|
||||||
|
|
||||||
class MiningDashboardService:
|
class MiningDashboardService:
|
||||||
"""Service for fetching and processing mining dashboard data."""
|
"""Service for fetching and processing mining dashboard data."""
|
||||||
|
|
||||||
def __init__(self, power_cost, power_usage, wallet):
|
def __init__(self, power_cost, power_usage, wallet, network_fee=0.0):
|
||||||
"""
|
"""
|
||||||
Initialize the mining dashboard service.
|
Initialize the mining dashboard service.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
power_cost (float): Cost of power in $ per kWh
|
power_cost (float): Cost of power in $ per kWh
|
||||||
power_usage (float): Power usage in watts
|
power_usage (float): Power usage in watts
|
||||||
wallet (str): Bitcoin wallet address for Ocean.xyz
|
wallet (str): Bitcoin wallet address for Ocean.xyz
|
||||||
|
network_fee (float): Additional network fee percentage
|
||||||
"""
|
"""
|
||||||
self.power_cost = power_cost
|
self.power_cost = power_cost
|
||||||
self.power_usage = power_usage
|
self.power_usage = power_usage
|
||||||
self.wallet = wallet
|
self.wallet = wallet
|
||||||
|
self.network_fee = network_fee
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.sats_per_btc = 100_000_000
|
self.sats_per_btc = 100_000_000
|
||||||
self.previous_values = {}
|
self.previous_values = {}
|
||||||
@ -36,13 +39,13 @@ class MiningDashboardService:
|
|||||||
def fetch_metrics(self):
|
def fetch_metrics(self):
|
||||||
"""
|
"""
|
||||||
Fetch metrics from Ocean.xyz and other sources.
|
Fetch metrics from Ocean.xyz and other sources.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Mining metrics data
|
dict: Mining metrics data
|
||||||
"""
|
"""
|
||||||
# Add execution time tracking
|
# Add execution time tracking
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with ThreadPoolExecutor(max_workers=2) as executor:
|
with ThreadPoolExecutor(max_workers=2) as executor:
|
||||||
future_ocean = executor.submit(self.get_ocean_data)
|
future_ocean = executor.submit(self.get_ocean_data)
|
||||||
@ -59,12 +62,12 @@ class MiningDashboardService:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
difficulty, network_hashrate, btc_price, block_count = btc_stats
|
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 we failed to get network hashrate, use a reasonable default to prevent division by zero
|
||||||
if network_hashrate is None:
|
if network_hashrate is None:
|
||||||
logging.warning("Using default network hashrate")
|
logging.warning("Using default network hashrate")
|
||||||
network_hashrate = 500e18 # ~500 EH/s as a reasonable fallback
|
network_hashrate = 500e18 # ~500 EH/s as a reasonable fallback
|
||||||
|
|
||||||
# If we failed to get BTC price, use a reasonable default
|
# If we failed to get BTC price, use a reasonable default
|
||||||
if btc_price is None:
|
if btc_price is None:
|
||||||
logging.warning("Using default BTC price")
|
logging.warning("Using default BTC price")
|
||||||
@ -79,7 +82,25 @@ class MiningDashboardService:
|
|||||||
block_reward = 3.125
|
block_reward = 3.125
|
||||||
blocks_per_day = 86400 / 600
|
blocks_per_day = 86400 / 600
|
||||||
daily_btc_gross = hash_proportion * block_reward * blocks_per_day
|
daily_btc_gross = hash_proportion * block_reward * blocks_per_day
|
||||||
daily_btc_net = daily_btc_gross * (1 - 0.02 - 0.028)
|
|
||||||
|
# Use actual pool fees instead of hardcoded values
|
||||||
|
# Get the pool fee percentage from ocean_data, default to 2.0% if not available
|
||||||
|
pool_fee_percent = ocean_data.pool_fees_percentage if ocean_data.pool_fees_percentage is not None else 2.0
|
||||||
|
|
||||||
|
# Get the network fee from the configuration (default to 0.0% if not set)
|
||||||
|
from config import load_config
|
||||||
|
config = load_config()
|
||||||
|
network_fee_percent = config.get("network_fee", 0.0)
|
||||||
|
|
||||||
|
# Calculate total fee percentage (converting from percentage to decimal)
|
||||||
|
total_fee_rate = (pool_fee_percent + network_fee_percent) / 100.0
|
||||||
|
|
||||||
|
# Calculate net BTC accounting for actual fees
|
||||||
|
daily_btc_net = daily_btc_gross * (1 - total_fee_rate)
|
||||||
|
|
||||||
|
# Log the fee calculations for transparency
|
||||||
|
logging.info(f"Earnings calculation using pool fee: {pool_fee_percent}% + network fee: {network_fee_percent}%")
|
||||||
|
logging.info(f"Total fee rate: {total_fee_rate}, Daily BTC gross: {daily_btc_gross}, Daily BTC net: {daily_btc_net}")
|
||||||
|
|
||||||
daily_revenue = round(daily_btc_net * btc_price, 2) if btc_price is not None else None
|
daily_revenue = round(daily_btc_net * btc_price, 2) if btc_price is not None else None
|
||||||
daily_power_cost = round((self.power_usage / 1000) * self.power_cost * 24, 2)
|
daily_power_cost = round((self.power_usage / 1000) * self.power_cost * 24, 2)
|
||||||
@ -112,7 +133,11 @@ class MiningDashboardService:
|
|||||||
'block_number': block_count,
|
'block_number': block_count,
|
||||||
'network_hashrate': (network_hashrate / 1e18) if network_hashrate else None,
|
'network_hashrate': (network_hashrate / 1e18) if network_hashrate else None,
|
||||||
'difficulty': difficulty,
|
'difficulty': difficulty,
|
||||||
|
'daily_btc_gross': daily_btc_gross,
|
||||||
'daily_btc_net': daily_btc_net,
|
'daily_btc_net': daily_btc_net,
|
||||||
|
'pool_fee_percent': pool_fee_percent,
|
||||||
|
'network_fee_percent': network_fee_percent,
|
||||||
|
'total_fee_rate': total_fee_rate,
|
||||||
'estimated_earnings_per_day': estimated_earnings_per_day,
|
'estimated_earnings_per_day': estimated_earnings_per_day,
|
||||||
'daily_revenue': daily_revenue,
|
'daily_revenue': daily_revenue,
|
||||||
'daily_power_cost': daily_power_cost,
|
'daily_power_cost': daily_power_cost,
|
||||||
@ -136,8 +161,8 @@ class MiningDashboardService:
|
|||||||
metrics['estimated_rewards_in_window_sats'] = int(round(estimated_rewards_in_window * self.sats_per_btc))
|
metrics['estimated_rewards_in_window_sats'] = int(round(estimated_rewards_in_window * self.sats_per_btc))
|
||||||
|
|
||||||
# --- Add server timestamps to the response in Los Angeles Time ---
|
# --- Add server timestamps to the response in Los Angeles Time ---
|
||||||
metrics["server_timestamp"] = datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
metrics["server_timestamp"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
metrics["server_start_time"] = datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
metrics["server_start_time"] = datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
|
|
||||||
# Log execution time
|
# Log execution time
|
||||||
execution_time = time.time() - start_time
|
execution_time = time.time() - start_time
|
||||||
@ -360,7 +385,7 @@ class MiningDashboardService:
|
|||||||
try:
|
try:
|
||||||
naive_dt = datetime.strptime(last_share_str, "%Y-%m-%d %H:%M")
|
naive_dt = datetime.strptime(last_share_str, "%Y-%m-%d %H:%M")
|
||||||
utc_dt = naive_dt.replace(tzinfo=ZoneInfo("UTC"))
|
utc_dt = naive_dt.replace(tzinfo=ZoneInfo("UTC"))
|
||||||
la_dt = utc_dt.astimezone(ZoneInfo("America/Los_Angeles"))
|
la_dt = utc_dt.astimezone(ZoneInfo(get_timezone()))
|
||||||
data.total_last_share = la_dt.strftime("%Y-%m-%d %I:%M %p")
|
data.total_last_share = la_dt.strftime("%Y-%m-%d %I:%M %p")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error converting last share time '{last_share_str}': {e}")
|
logging.error(f"Error converting last share time '{last_share_str}': {e}")
|
||||||
@ -638,7 +663,6 @@ class MiningDashboardService:
|
|||||||
# Find total worker counts
|
# Find total worker counts
|
||||||
workers_online = 0
|
workers_online = 0
|
||||||
workers_offline = 0
|
workers_offline = 0
|
||||||
avg_acceptance_rate = 95.0 # Default value
|
|
||||||
|
|
||||||
# Iterate through worker rows in the table
|
# Iterate through worker rows in the table
|
||||||
for row in workers_table.find_all('tr', class_='table-row'):
|
for row in workers_table.find_all('tr', class_='table-row'):
|
||||||
@ -674,7 +698,6 @@ class MiningDashboardService:
|
|||||||
"efficiency": 90.0, # Default efficiency
|
"efficiency": 90.0, # Default efficiency
|
||||||
"last_share": "N/A",
|
"last_share": "N/A",
|
||||||
"earnings": 0,
|
"earnings": 0,
|
||||||
"acceptance_rate": 95.0, # Default acceptance rate
|
|
||||||
"power_consumption": 0,
|
"power_consumption": 0,
|
||||||
"temperature": 0
|
"temperature": 0
|
||||||
}
|
}
|
||||||
@ -814,9 +837,8 @@ class MiningDashboardService:
|
|||||||
'workers_online': workers_online,
|
'workers_online': workers_online,
|
||||||
'workers_offline': workers_offline,
|
'workers_offline': workers_offline,
|
||||||
'total_earnings': total_earnings,
|
'total_earnings': total_earnings,
|
||||||
'avg_acceptance_rate': avg_acceptance_rate,
|
|
||||||
'daily_sats': daily_sats,
|
'daily_sats': daily_sats,
|
||||||
'timestamp': datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
logging.info(f"Successfully retrieved worker data: {len(workers)} workers")
|
logging.info(f"Successfully retrieved worker data: {len(workers)} workers")
|
||||||
@ -875,7 +897,6 @@ class MiningDashboardService:
|
|||||||
"efficiency": 90.0,
|
"efficiency": 90.0,
|
||||||
"last_share": "N/A",
|
"last_share": "N/A",
|
||||||
"earnings": 0,
|
"earnings": 0,
|
||||||
"acceptance_rate": 95.0,
|
|
||||||
"power_consumption": 0,
|
"power_consumption": 0,
|
||||||
"temperature": 0
|
"temperature": 0
|
||||||
}
|
}
|
||||||
@ -962,8 +983,7 @@ class MiningDashboardService:
|
|||||||
'workers_online': workers_online,
|
'workers_online': workers_online,
|
||||||
'workers_offline': workers_offline,
|
'workers_offline': workers_offline,
|
||||||
'total_earnings': total_earnings,
|
'total_earnings': total_earnings,
|
||||||
'avg_acceptance_rate': 99.0,
|
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
'timestamp': datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
|
||||||
}
|
}
|
||||||
logging.info(f"Successfully retrieved {len(workers)} workers across multiple pages")
|
logging.info(f"Successfully retrieved {len(workers)} workers across multiple pages")
|
||||||
return result
|
return result
|
||||||
|
@ -16,8 +16,8 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
|
|||||||
|
|
||||||
1. Clone the repository:
|
1. Clone the repository:
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/yourusername/bitcoin-mining-dashboard.git
|
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git
|
||||||
cd bitcoin-mining-dashboard
|
cd DeepSea-Dashboard
|
||||||
```
|
```
|
||||||
|
|
||||||
2. Create a virtual environment (recommended):
|
2. Create a virtual environment (recommended):
|
||||||
@ -36,21 +36,12 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
|
|||||||
python setup.py
|
python setup.py
|
||||||
```
|
```
|
||||||
|
|
||||||
5. Configure your mining parameters in `config.json`:
|
5. Start the application:
|
||||||
```json
|
|
||||||
{
|
|
||||||
"power_cost": 0.12, // Cost of electricity per kWh
|
|
||||||
"power_usage": 3450, // Power consumption in watts
|
|
||||||
"wallet": "your-wallet-address" // Your Ocean.xyz wallet
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
6. Start the application:
|
|
||||||
```bash
|
```bash
|
||||||
python App.py
|
python App.py
|
||||||
```
|
```
|
||||||
|
|
||||||
7. Access the dashboard at `http://localhost:5000`
|
6. Access the dashboard at `http://localhost:5000`
|
||||||
|
|
||||||
### Option 2: Production Deployment with Gunicorn
|
### Option 2: Production Deployment with Gunicorn
|
||||||
|
|
||||||
|
@ -8,6 +8,11 @@ services:
|
|||||||
- redis_data:/data
|
- redis_data:/data
|
||||||
ports:
|
ports:
|
||||||
- "6379:6379"
|
- "6379:6379"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
|
interval: 10s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
dashboard:
|
dashboard:
|
||||||
build: .
|
build: .
|
||||||
@ -16,14 +21,22 @@ services:
|
|||||||
- "5000:5000"
|
- "5000:5000"
|
||||||
environment:
|
environment:
|
||||||
- REDIS_URL=redis://redis:6379
|
- REDIS_URL=redis://redis:6379
|
||||||
- WALLET=bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9
|
- WALLET=35eS5Lsqw8NCjFJ8zhp9JaEmyvLDwg6XtS
|
||||||
- POWER_COST=0
|
- POWER_COST=0
|
||||||
- POWER_USAGE=0
|
- POWER_USAGE=0
|
||||||
|
- NETWORK_FEE=0
|
||||||
|
- TIMEZONE=America/Los_Angeles
|
||||||
- LOG_LEVEL=INFO
|
- LOG_LEVEL=INFO
|
||||||
volumes:
|
volumes:
|
||||||
- ./logs:/app/logs
|
- ./logs:/app/logs
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
redis_data:
|
redis_data:
|
||||||
|
12
dockerfile
12
dockerfile
@ -1,4 +1,4 @@
|
|||||||
FROM python:3.9-slim
|
FROM python:3.9.18-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@ -17,8 +17,8 @@ COPY *.py .
|
|||||||
COPY config.json .
|
COPY config.json .
|
||||||
COPY setup.py .
|
COPY setup.py .
|
||||||
|
|
||||||
# Create necessary directories
|
# Create all necessary directories in one command
|
||||||
RUN mkdir -p static/css static/js templates logs
|
RUN mkdir -p static/css static/js templates logs /app/logs
|
||||||
|
|
||||||
# Copy static files and templates
|
# Copy static files and templates
|
||||||
COPY static/css/*.css static/css/
|
COPY static/css/*.css static/css/
|
||||||
@ -29,7 +29,7 @@ COPY templates/*.html templates/
|
|||||||
RUN python setup.py
|
RUN python setup.py
|
||||||
|
|
||||||
# Run the minifier to process HTML templates
|
# Run the minifier to process HTML templates
|
||||||
RUN python minify.py
|
RUN python minify.py
|
||||||
|
|
||||||
# Create a non-root user for better security
|
# Create a non-root user for better security
|
||||||
RUN adduser --disabled-password --gecos '' appuser
|
RUN adduser --disabled-password --gecos '' appuser
|
||||||
@ -37,9 +37,6 @@ RUN adduser --disabled-password --gecos '' appuser
|
|||||||
# Change ownership of the /app directory so appuser can write files
|
# Change ownership of the /app directory so appuser can write files
|
||||||
RUN chown -R appuser:appuser /app
|
RUN chown -R appuser:appuser /app
|
||||||
|
|
||||||
# Create a directory for logs with proper permissions
|
|
||||||
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
|
|
||||||
|
|
||||||
# Switch to non-root user
|
# Switch to non-root user
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
@ -49,7 +46,6 @@ EXPOSE 5000
|
|||||||
# Set environment variables
|
# Set environment variables
|
||||||
ENV FLASK_ENV=production
|
ENV FLASK_ENV=production
|
||||||
ENV PYTHONUNBUFFERED=1
|
ENV PYTHONUNBUFFERED=1
|
||||||
ENV PYTHON_UNBUFFERED=1
|
|
||||||
|
|
||||||
# Add healthcheck
|
# Add healthcheck
|
||||||
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
|
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \
|
||||||
|
247
minify.py
247
minify.py
@ -1,6 +1,14 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
import os
|
import os
|
||||||
import jsmin
|
import jsmin
|
||||||
|
import htmlmin
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# Set up logging
|
||||||
|
logging.basicConfig(level=logging.INFO,
|
||||||
|
format='%(asctime)s - %(levelname)s - %(message)s')
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def minify_js_files():
|
def minify_js_files():
|
||||||
"""Minify JavaScript files."""
|
"""Minify JavaScript files."""
|
||||||
@ -9,25 +17,230 @@ def minify_js_files():
|
|||||||
os.makedirs(min_dir, exist_ok=True)
|
os.makedirs(min_dir, exist_ok=True)
|
||||||
|
|
||||||
minified_count = 0
|
minified_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
for js_file in os.listdir(js_dir):
|
for js_file in os.listdir(js_dir):
|
||||||
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
|
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
|
||||||
input_path = os.path.join(js_dir, js_file)
|
try:
|
||||||
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
|
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()
|
# Skip already minified files if they're newer than source
|
||||||
|
if os.path.exists(output_path) and \
|
||||||
# Minify the content
|
os.path.getmtime(output_path) > os.path.getmtime(input_path):
|
||||||
minified = jsmin.jsmin(js_content)
|
logger.info(f"Skipping {js_file} (already up to date)")
|
||||||
|
skipped_count += 1
|
||||||
# Write minified content
|
continue
|
||||||
with open(output_path, 'w') as f:
|
|
||||||
f.write(minified)
|
with open(input_path, 'r', encoding='utf-8') as f:
|
||||||
|
js_content = f.read()
|
||||||
minified_count += 1
|
|
||||||
print(f"Minified {js_file}")
|
# 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}")
|
||||||
|
|
||||||
print(f"Total files minified: {minified_count}")
|
logger.info(f"JavaScript minification: {minified_count} files minified, {skipped_count} files skipped")
|
||||||
|
return minified_count
|
||||||
|
|
||||||
|
def minify_css_files():
|
||||||
|
"""Minify CSS files using simple compression techniques."""
|
||||||
|
css_dir = 'static/css'
|
||||||
|
min_dir = os.path.join(css_dir, 'min')
|
||||||
|
os.makedirs(min_dir, exist_ok=True)
|
||||||
|
|
||||||
|
minified_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
for css_file in os.listdir(css_dir):
|
||||||
|
if css_file.endswith('.css') and not css_file.endswith('.min.css'):
|
||||||
|
try:
|
||||||
|
input_path = os.path.join(css_dir, css_file)
|
||||||
|
output_path = os.path.join(min_dir, css_file.replace('.css', '.min.css'))
|
||||||
|
|
||||||
|
# Skip already minified files if they're newer than source
|
||||||
|
if os.path.exists(output_path) and \
|
||||||
|
os.path.getmtime(output_path) > os.path.getmtime(input_path):
|
||||||
|
logger.info(f"Skipping {css_file} (already up to date)")
|
||||||
|
skipped_count += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
with open(input_path, 'r', encoding='utf-8') as f:
|
||||||
|
css_content = f.read()
|
||||||
|
|
||||||
|
# Simple CSS minification using string replacements
|
||||||
|
# Remove comments
|
||||||
|
import re
|
||||||
|
css_minified = re.sub(r'/\*[\s\S]*?\*/', '', css_content)
|
||||||
|
# Remove whitespace
|
||||||
|
css_minified = re.sub(r'\s+', ' ', css_minified)
|
||||||
|
# Remove spaces around selectors
|
||||||
|
css_minified = re.sub(r'\s*{\s*', '{', css_minified)
|
||||||
|
css_minified = re.sub(r'\s*}\s*', '}', css_minified)
|
||||||
|
css_minified = re.sub(r'\s*;\s*', ';', css_minified)
|
||||||
|
css_minified = re.sub(r'\s*:\s*', ':', css_minified)
|
||||||
|
css_minified = re.sub(r'\s*,\s*', ',', css_minified)
|
||||||
|
# Remove last semicolons
|
||||||
|
css_minified = re.sub(r';}', '}', css_minified)
|
||||||
|
|
||||||
|
with open(output_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(css_minified)
|
||||||
|
|
||||||
|
size_original = len(css_content)
|
||||||
|
size_minified = len(css_minified)
|
||||||
|
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
|
||||||
|
|
||||||
|
logger.info(f"Minified {css_file} - Reduced by {reduction:.1f}%")
|
||||||
|
minified_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing {css_file}: {e}")
|
||||||
|
|
||||||
|
logger.info(f"CSS minification: {minified_count} files minified, {skipped_count} files skipped")
|
||||||
|
return minified_count
|
||||||
|
|
||||||
|
def minify_html_templates():
|
||||||
|
"""Minify HTML template files."""
|
||||||
|
templates_dir = 'templates'
|
||||||
|
minified_count = 0
|
||||||
|
skipped_count = 0
|
||||||
|
|
||||||
|
for html_file in os.listdir(templates_dir):
|
||||||
|
if html_file.endswith('.html'):
|
||||||
|
try:
|
||||||
|
input_path = os.path.join(templates_dir, html_file)
|
||||||
|
|
||||||
|
with open(input_path, 'r', encoding='utf-8') as f:
|
||||||
|
html_content = f.read()
|
||||||
|
|
||||||
|
# Minify HTML content while keeping important whitespace
|
||||||
|
minified = htmlmin.minify(html_content,
|
||||||
|
remove_comments=True,
|
||||||
|
remove_empty_space=True,
|
||||||
|
remove_all_empty_space=False,
|
||||||
|
reduce_boolean_attributes=True)
|
||||||
|
|
||||||
|
# Write back to the same file
|
||||||
|
with open(input_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(minified)
|
||||||
|
|
||||||
|
size_original = len(html_content)
|
||||||
|
size_minified = len(minified)
|
||||||
|
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
|
||||||
|
|
||||||
|
logger.info(f"Minified {html_file} - Reduced by {reduction:.1f}%")
|
||||||
|
minified_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Error processing {html_file}: {e}")
|
||||||
|
|
||||||
|
logger.info(f"HTML minification: {minified_count} files minified, {skipped_count} files skipped")
|
||||||
|
return minified_count
|
||||||
|
|
||||||
|
def create_size_report():
|
||||||
|
"""Create a report of file sizes before and after minification."""
|
||||||
|
results = []
|
||||||
|
|
||||||
|
# Check JS files
|
||||||
|
js_dir = 'static/js'
|
||||||
|
min_dir = os.path.join(js_dir, 'min')
|
||||||
|
if os.path.exists(min_dir):
|
||||||
|
for js_file in os.listdir(js_dir):
|
||||||
|
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
|
||||||
|
orig_path = os.path.join(js_dir, js_file)
|
||||||
|
min_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
|
||||||
|
|
||||||
|
if os.path.exists(min_path):
|
||||||
|
orig_size = os.path.getsize(orig_path)
|
||||||
|
min_size = os.path.getsize(min_path)
|
||||||
|
reduction = (1 - min_size / orig_size) * 100 if orig_size > 0 else 0
|
||||||
|
results.append({
|
||||||
|
'file': js_file,
|
||||||
|
'type': 'JavaScript',
|
||||||
|
'original_size': orig_size,
|
||||||
|
'minified_size': min_size,
|
||||||
|
'reduction': reduction
|
||||||
|
})
|
||||||
|
|
||||||
|
# Check CSS files
|
||||||
|
css_dir = 'static/css'
|
||||||
|
min_dir = os.path.join(css_dir, 'min')
|
||||||
|
if os.path.exists(min_dir):
|
||||||
|
for css_file in os.listdir(css_dir):
|
||||||
|
if css_file.endswith('.css') and not css_file.endswith('.min.css'):
|
||||||
|
orig_path = os.path.join(css_dir, css_file)
|
||||||
|
min_path = os.path.join(min_dir, css_file.replace('.css', '.min.css'))
|
||||||
|
|
||||||
|
if os.path.exists(min_path):
|
||||||
|
orig_size = os.path.getsize(orig_path)
|
||||||
|
min_size = os.path.getsize(min_path)
|
||||||
|
reduction = (1 - min_size / orig_size) * 100 if orig_size > 0 else 0
|
||||||
|
results.append({
|
||||||
|
'file': css_file,
|
||||||
|
'type': 'CSS',
|
||||||
|
'original_size': orig_size,
|
||||||
|
'minified_size': min_size,
|
||||||
|
'reduction': reduction
|
||||||
|
})
|
||||||
|
|
||||||
|
# Print the report
|
||||||
|
total_orig = sum(item['original_size'] for item in results)
|
||||||
|
total_min = sum(item['minified_size'] for item in results)
|
||||||
|
total_reduction = (1 - total_min / total_orig) * 100 if total_orig > 0 else 0
|
||||||
|
|
||||||
|
logger.info("\n" + "="*50)
|
||||||
|
logger.info("MINIFICATION REPORT")
|
||||||
|
logger.info("="*50)
|
||||||
|
logger.info(f"{'File':<30} {'Type':<10} {'Original':<10} {'Minified':<10} {'Reduction'}")
|
||||||
|
logger.info("-"*70)
|
||||||
|
|
||||||
|
for item in results:
|
||||||
|
logger.info(f"{item['file']:<30} {item['type']:<10} "
|
||||||
|
f"{item['original_size']/1024:.1f}KB {item['minified_size']/1024:.1f}KB "
|
||||||
|
f"{item['reduction']:.1f}%")
|
||||||
|
|
||||||
|
logger.info("-"*70)
|
||||||
|
logger.info(f"{'TOTAL:':<30} {'':<10} {total_orig/1024:.1f}KB {total_min/1024:.1f}KB {total_reduction:.1f}%")
|
||||||
|
logger.info("="*50)
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main function to run minification tasks."""
|
||||||
|
import argparse
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description='Minify web assets')
|
||||||
|
parser.add_argument('--js', action='store_true', help='Minify JavaScript files')
|
||||||
|
parser.add_argument('--css', action='store_true', help='Minify CSS files')
|
||||||
|
parser.add_argument('--html', action='store_true', help='Minify HTML templates')
|
||||||
|
parser.add_argument('--all', action='store_true', help='Minify all assets')
|
||||||
|
parser.add_argument('--report', action='store_true', help='Generate size report only')
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
# If no arguments, default to --all
|
||||||
|
if not (args.js or args.css or args.html or args.report):
|
||||||
|
args.all = True
|
||||||
|
|
||||||
|
if args.all or args.js:
|
||||||
|
minify_js_files()
|
||||||
|
|
||||||
|
if args.all or args.css:
|
||||||
|
minify_css_files()
|
||||||
|
|
||||||
|
if args.all or args.html:
|
||||||
|
minify_html_templates()
|
||||||
|
|
||||||
|
# Always generate the report at the end if any minification was done
|
||||||
|
if args.report or args.all or args.js or args.css:
|
||||||
|
create_size_report()
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
minify_js_files()
|
main()
|
||||||
|
93
models.py
93
models.py
@ -2,11 +2,13 @@
|
|||||||
Data models for the Bitcoin Mining Dashboard.
|
Data models for the Bitcoin Mining Dashboard.
|
||||||
"""
|
"""
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Dict, List, Union, Any
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class OceanData:
|
class OceanData:
|
||||||
"""Data structure for Ocean.xyz pool mining data."""
|
"""Data structure for Ocean.xyz pool mining data."""
|
||||||
|
# Keep original definitions with None default to maintain backward compatibility
|
||||||
pool_total_hashrate: float = None
|
pool_total_hashrate: float = None
|
||||||
pool_total_hashrate_unit: str = None
|
pool_total_hashrate_unit: str = None
|
||||||
hashrate_24hr: float = None
|
hashrate_24hr: float = None
|
||||||
@ -31,6 +33,42 @@ class OceanData:
|
|||||||
blocks_found: str = None
|
blocks_found: str = None
|
||||||
total_last_share: str = "N/A"
|
total_last_share: str = "N/A"
|
||||||
last_block_earnings: str = None
|
last_block_earnings: str = None
|
||||||
|
pool_fees_percentage: float = None # Added missing attribute
|
||||||
|
|
||||||
|
def get_normalized_hashrate(self, timeframe: str = "3hr") -> float:
|
||||||
|
"""
|
||||||
|
Get a normalized hashrate value in TH/s regardless of original units.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeframe: The timeframe to get ("24hr", "3hr", "10min", "5min", "60sec")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Normalized hashrate in TH/s
|
||||||
|
"""
|
||||||
|
if timeframe == "24hr" and self.hashrate_24hr is not None:
|
||||||
|
return convert_to_ths(self.hashrate_24hr, self.hashrate_24hr_unit)
|
||||||
|
elif timeframe == "3hr" and self.hashrate_3hr is not None:
|
||||||
|
return convert_to_ths(self.hashrate_3hr, self.hashrate_3hr_unit)
|
||||||
|
elif timeframe == "10min" and self.hashrate_10min is not None:
|
||||||
|
return convert_to_ths(self.hashrate_10min, self.hashrate_10min_unit)
|
||||||
|
elif timeframe == "5min" and self.hashrate_5min is not None:
|
||||||
|
return convert_to_ths(self.hashrate_5min, self.hashrate_5min_unit)
|
||||||
|
elif timeframe == "60sec" and self.hashrate_60sec is not None:
|
||||||
|
return convert_to_ths(self.hashrate_60sec, self.hashrate_60sec_unit)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert the OceanData object to a dictionary."""
|
||||||
|
return {k: v for k, v in self.__dict__.items()}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> 'OceanData':
|
||||||
|
"""Create an OceanData instance from a dictionary."""
|
||||||
|
filtered_data = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
if k in cls.__annotations__:
|
||||||
|
filtered_data[k] = v
|
||||||
|
return cls(**filtered_data)
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class WorkerData:
|
class WorkerData:
|
||||||
@ -50,6 +88,61 @@ class WorkerData:
|
|||||||
power_consumption: float = 0
|
power_consumption: float = 0
|
||||||
temperature: float = 0
|
temperature: float = 0
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
"""
|
||||||
|
Validate worker data after initialization.
|
||||||
|
Ensures values are within acceptable ranges and formats.
|
||||||
|
"""
|
||||||
|
# Ensure hashrates are non-negative
|
||||||
|
if self.hashrate_60sec is not None and self.hashrate_60sec < 0:
|
||||||
|
self.hashrate_60sec = 0
|
||||||
|
|
||||||
|
if self.hashrate_3hr is not None and self.hashrate_3hr < 0:
|
||||||
|
self.hashrate_3hr = 0
|
||||||
|
|
||||||
|
# Ensure status is valid, but don't raise exceptions for backward compatibility
|
||||||
|
if self.status not in ["online", "offline"]:
|
||||||
|
logging.warning(f"Worker {self.name}: Invalid status '{self.status}', using 'offline'")
|
||||||
|
self.status = "offline"
|
||||||
|
|
||||||
|
# Ensure type is valid, but don't raise exceptions for backward compatibility
|
||||||
|
if self.type not in ["ASIC", "Bitaxe"]:
|
||||||
|
logging.warning(f"Worker {self.name}: Invalid type '{self.type}', using 'ASIC'")
|
||||||
|
self.type = "ASIC"
|
||||||
|
|
||||||
|
def get_normalized_hashrate(self, timeframe: str = "3hr") -> float:
|
||||||
|
"""
|
||||||
|
Get normalized hashrate in TH/s.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
timeframe: The timeframe to get ("3hr" or "60sec")
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
float: Normalized hashrate in TH/s
|
||||||
|
"""
|
||||||
|
if timeframe == "3hr":
|
||||||
|
return convert_to_ths(self.hashrate_3hr, self.hashrate_3hr_unit)
|
||||||
|
elif timeframe == "60sec":
|
||||||
|
return convert_to_ths(self.hashrate_60sec, self.hashrate_60sec_unit)
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def to_dict(self) -> Dict[str, Any]:
|
||||||
|
"""Convert the WorkerData object to a dictionary."""
|
||||||
|
return {k: v for k, v in self.__dict__.items()}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_dict(cls, data: Dict[str, Any]) -> 'WorkerData':
|
||||||
|
"""Create a WorkerData instance from a dictionary."""
|
||||||
|
filtered_data = {}
|
||||||
|
for k, v in data.items():
|
||||||
|
if k in cls.__annotations__:
|
||||||
|
filtered_data[k] = v
|
||||||
|
return cls(**filtered_data)
|
||||||
|
|
||||||
|
class HashRateConversionError(Exception):
|
||||||
|
"""Exception raised for errors in hashrate unit conversion."""
|
||||||
|
pass
|
||||||
|
|
||||||
def convert_to_ths(value, unit):
|
def convert_to_ths(value, unit):
|
||||||
"""
|
"""
|
||||||
Convert any hashrate unit to TH/s equivalent.
|
Convert any hashrate unit to TH/s equivalent.
|
||||||
|
@ -6,6 +6,13 @@ import uuid
|
|||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
from collections import deque
|
from collections import deque
|
||||||
|
from typing import List, Dict, Any, Optional, Union
|
||||||
|
|
||||||
|
# Constants to replace magic values
|
||||||
|
ONE_DAY_SECONDS = 86400
|
||||||
|
DEFAULT_TARGET_HOUR = 12
|
||||||
|
SIGNIFICANT_HASHRATE_CHANGE_PERCENT = 25
|
||||||
|
NOTIFICATION_WINDOW_MINUTES = 5
|
||||||
|
|
||||||
class NotificationLevel(Enum):
|
class NotificationLevel(Enum):
|
||||||
INFO = "info"
|
INFO = "info"
|
||||||
@ -40,51 +47,78 @@ class NotificationService:
|
|||||||
# Load last block height from state
|
# Load last block height from state
|
||||||
self._load_last_block_height()
|
self._load_last_block_height()
|
||||||
|
|
||||||
def _load_notifications(self):
|
def _get_redis_value(self, key: str, default: Any = None) -> Any:
|
||||||
"""Load notifications from persistent storage."""
|
"""Generic method to retrieve values from Redis."""
|
||||||
|
try:
|
||||||
|
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
|
||||||
|
value = self.state_manager.redis_client.get(key)
|
||||||
|
if value:
|
||||||
|
return value.decode('utf-8')
|
||||||
|
return default
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[NotificationService] Error retrieving {key} from Redis: {e}")
|
||||||
|
return default
|
||||||
|
|
||||||
|
def _set_redis_value(self, key: str, value: Any) -> bool:
|
||||||
|
"""Generic method to set values in Redis."""
|
||||||
|
try:
|
||||||
|
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
|
||||||
|
self.state_manager.redis_client.set(key, str(value))
|
||||||
|
logging.info(f"[NotificationService] Saved {key} to Redis: {value}")
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"[NotificationService] Error saving {key} to Redis: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _load_notifications(self) -> None:
|
||||||
|
"""Load notifications with enhanced error handling."""
|
||||||
try:
|
try:
|
||||||
stored_notifications = self.state_manager.get_notifications()
|
stored_notifications = self.state_manager.get_notifications()
|
||||||
if stored_notifications:
|
if stored_notifications:
|
||||||
self.notifications = stored_notifications
|
self.notifications = stored_notifications
|
||||||
logging.info(f"Loaded {len(self.notifications)} notifications from storage")
|
logging.info(f"[NotificationService] Loaded {len(self.notifications)} notifications from storage")
|
||||||
|
else:
|
||||||
|
self.notifications = []
|
||||||
|
logging.info("[NotificationService] No notifications found in storage, starting with empty list")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading notifications: {e}")
|
logging.error(f"[NotificationService] Error loading notifications: {e}")
|
||||||
|
self.notifications = [] # Ensure we have a valid list
|
||||||
|
|
||||||
def _load_last_block_height(self):
|
def _load_last_block_height(self) -> None:
|
||||||
"""Load last block height from persistent storage."""
|
"""Load last block height from persistent storage."""
|
||||||
try:
|
try:
|
||||||
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client:
|
self.last_block_height = self._get_redis_value("last_block_height")
|
||||||
# Use Redis if available
|
if self.last_block_height:
|
||||||
last_height = self.state_manager.redis_client.get("last_block_height")
|
logging.info(f"[NotificationService] Loaded last block height from storage: {self.last_block_height}")
|
||||||
if last_height:
|
|
||||||
self.last_block_height = last_height.decode('utf-8')
|
|
||||||
logging.info(f"Loaded last block height from storage: {self.last_block_height}")
|
|
||||||
else:
|
else:
|
||||||
logging.info("Redis not available, starting with no last block height")
|
logging.info("[NotificationService] No last block height found, starting with None")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error loading last block height: {e}")
|
logging.error(f"[NotificationService] Error loading last block height: {e}")
|
||||||
|
|
||||||
def _save_last_block_height(self):
|
def _save_last_block_height(self) -> None:
|
||||||
"""Save last block height to persistent storage."""
|
"""Save last block height to persistent storage."""
|
||||||
try:
|
if self.last_block_height:
|
||||||
if hasattr(self.state_manager, 'redis_client') and self.state_manager.redis_client and self.last_block_height:
|
self._set_redis_value("last_block_height", 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):
|
def _save_notifications(self) -> None:
|
||||||
"""Save notifications to persistent storage."""
|
"""Save notifications with improved pruning."""
|
||||||
try:
|
try:
|
||||||
# Prune to max size before saving
|
# Sort by timestamp before pruning to ensure we keep the most recent
|
||||||
if len(self.notifications) > self.max_notifications:
|
if len(self.notifications) > self.max_notifications:
|
||||||
self.notifications = self.notifications[-self.max_notifications:]
|
self.notifications.sort(key=lambda n: n.get("timestamp", ""), reverse=True)
|
||||||
|
self.notifications = self.notifications[:self.max_notifications]
|
||||||
|
|
||||||
self.state_manager.save_notifications(self.notifications)
|
self.state_manager.save_notifications(self.notifications)
|
||||||
|
logging.info(f"[NotificationService] Saved {len(self.notifications)} notifications")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error saving notifications: {e}")
|
logging.error(f"[NotificationService] Error saving notifications: {e}")
|
||||||
|
|
||||||
def add_notification(self, message, level=NotificationLevel.INFO, category=NotificationCategory.SYSTEM, data=None):
|
def add_notification(self,
|
||||||
|
message: str,
|
||||||
|
level: NotificationLevel = NotificationLevel.INFO,
|
||||||
|
category: NotificationCategory = NotificationCategory.SYSTEM,
|
||||||
|
data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Add a new notification.
|
Add a new notification.
|
||||||
|
|
||||||
@ -112,12 +146,17 @@ class NotificationService:
|
|||||||
self.notifications.append(notification)
|
self.notifications.append(notification)
|
||||||
self._save_notifications()
|
self._save_notifications()
|
||||||
|
|
||||||
logging.info(f"Added notification: {message}")
|
logging.info(f"[NotificationService] Added notification: {message}")
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
def get_notifications(self, limit=50, offset=0, unread_only=False, category=None, level=None):
|
def get_notifications(self,
|
||||||
|
limit: int = 50,
|
||||||
|
offset: int = 0,
|
||||||
|
unread_only: bool = False,
|
||||||
|
category: Optional[str] = None,
|
||||||
|
level: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Get filtered notifications.
|
Get filtered notifications with optimized filtering.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
limit (int): Maximum number to return
|
limit (int): Maximum number to return
|
||||||
@ -129,31 +168,25 @@ class NotificationService:
|
|||||||
Returns:
|
Returns:
|
||||||
list: Filtered notifications
|
list: Filtered notifications
|
||||||
"""
|
"""
|
||||||
filtered = self.notifications
|
# Apply all filters in a single pass
|
||||||
|
filtered = [
|
||||||
# Apply filters
|
n for n in self.notifications
|
||||||
if unread_only:
|
if (not unread_only or not n.get("read", False)) and
|
||||||
filtered = [n for n in filtered if not n.get("read", False)]
|
(not category or n.get("category") == category) and
|
||||||
|
(not level or n.get("level") == level)
|
||||||
if category:
|
]
|
||||||
filtered = [n for n in filtered if n.get("category") == category]
|
|
||||||
|
|
||||||
if level:
|
|
||||||
filtered = [n for n in filtered if n.get("level") == level]
|
|
||||||
|
|
||||||
# Sort by timestamp (newest first)
|
# Sort by timestamp (newest first)
|
||||||
filtered = sorted(filtered, key=lambda n: n.get("timestamp", ""), reverse=True)
|
filtered = sorted(filtered, key=lambda n: n.get("timestamp", ""), reverse=True)
|
||||||
|
|
||||||
# Apply pagination
|
# Apply pagination
|
||||||
paginated = filtered[offset:offset + limit]
|
return filtered[offset:offset + limit]
|
||||||
|
|
||||||
return paginated
|
|
||||||
|
|
||||||
def get_unread_count(self):
|
def get_unread_count(self) -> int:
|
||||||
"""Get count of unread notifications."""
|
"""Get count of unread notifications."""
|
||||||
return sum(1 for n in self.notifications if not n.get("read", False))
|
return sum(1 for n in self.notifications if not n.get("read", False))
|
||||||
|
|
||||||
def mark_as_read(self, notification_id=None):
|
def mark_as_read(self, notification_id: Optional[str] = None) -> bool:
|
||||||
"""
|
"""
|
||||||
Mark notification(s) as read.
|
Mark notification(s) as read.
|
||||||
|
|
||||||
@ -169,16 +202,18 @@ class NotificationService:
|
|||||||
for n in self.notifications:
|
for n in self.notifications:
|
||||||
if n.get("id") == notification_id:
|
if n.get("id") == notification_id:
|
||||||
n["read"] = True
|
n["read"] = True
|
||||||
|
logging.info(f"[NotificationService] Marked notification {notification_id} as read")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
# Mark all as read
|
# Mark all as read
|
||||||
for n in self.notifications:
|
for n in self.notifications:
|
||||||
n["read"] = True
|
n["read"] = True
|
||||||
|
logging.info(f"[NotificationService] Marked all {len(self.notifications)} notifications as read")
|
||||||
|
|
||||||
self._save_notifications()
|
self._save_notifications()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def delete_notification(self, notification_id):
|
def delete_notification(self, notification_id: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Delete a specific notification.
|
Delete a specific notification.
|
||||||
|
|
||||||
@ -188,13 +223,19 @@ class NotificationService:
|
|||||||
Returns:
|
Returns:
|
||||||
bool: True if successful
|
bool: True if successful
|
||||||
"""
|
"""
|
||||||
|
original_count = len(self.notifications)
|
||||||
self.notifications = [n for n in self.notifications if n.get("id") != notification_id]
|
self.notifications = [n for n in self.notifications if n.get("id") != notification_id]
|
||||||
self._save_notifications()
|
deleted = original_count - len(self.notifications)
|
||||||
return True
|
|
||||||
|
if deleted > 0:
|
||||||
|
logging.info(f"[NotificationService] Deleted notification {notification_id}")
|
||||||
|
self._save_notifications()
|
||||||
|
|
||||||
|
return deleted > 0
|
||||||
|
|
||||||
def clear_notifications(self, category=None, older_than_days=None):
|
def clear_notifications(self, category: Optional[str] = None, older_than_days: Optional[int] = None) -> int:
|
||||||
"""
|
"""
|
||||||
Clear notifications.
|
Clear notifications with optimized filtering.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
category (str, optional): Only clear specific category
|
category (str, optional): Only clear specific category
|
||||||
@ -205,44 +246,48 @@ class NotificationService:
|
|||||||
"""
|
"""
|
||||||
original_count = len(self.notifications)
|
original_count = len(self.notifications)
|
||||||
|
|
||||||
if category and older_than_days:
|
cutoff_date = None
|
||||||
|
if older_than_days:
|
||||||
cutoff_date = datetime.now() - timedelta(days=older_than_days)
|
cutoff_date = datetime.now() - timedelta(days=older_than_days)
|
||||||
self.notifications = [
|
|
||||||
n for n in self.notifications
|
# Apply filters in a single pass
|
||||||
if n.get("category") != category or
|
self.notifications = [
|
||||||
datetime.fromisoformat(n.get("timestamp", datetime.now().isoformat())) >= cutoff_date
|
n for n in self.notifications
|
||||||
]
|
if (not category or n.get("category") != category) and
|
||||||
elif category:
|
(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]
|
]
|
||||||
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 = []
|
|
||||||
|
|
||||||
self._save_notifications()
|
cleared_count = original_count - len(self.notifications)
|
||||||
return original_count - len(self.notifications)
|
if cleared_count > 0:
|
||||||
|
logging.info(f"[NotificationService] Cleared {cleared_count} notifications")
|
||||||
|
self._save_notifications()
|
||||||
|
|
||||||
|
return cleared_count
|
||||||
|
|
||||||
def check_and_generate_notifications(self, current_metrics, previous_metrics):
|
def check_and_generate_notifications(self, current_metrics: Dict[str, Any], previous_metrics: Optional[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""
|
||||||
Check metrics and generate notifications for significant events.
|
Check metrics and generate notifications for significant events.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
current_metrics: Current system metrics
|
||||||
|
previous_metrics: Previous system metrics for comparison
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: Newly created notifications
|
||||||
"""
|
"""
|
||||||
new_notifications = []
|
new_notifications = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Skip if no metrics
|
# Skip if no metrics
|
||||||
if not current_metrics:
|
if not current_metrics:
|
||||||
logging.warning("No current metrics available, skipping notification checks")
|
logging.warning("[NotificationService] No current metrics available, skipping notification checks")
|
||||||
return new_notifications
|
return new_notifications
|
||||||
|
|
||||||
# Check for block updates (using persistent storage)
|
# Check for block updates (using persistent storage)
|
||||||
last_block_height = current_metrics.get("last_block_height")
|
last_block_height = current_metrics.get("last_block_height")
|
||||||
if last_block_height and last_block_height != "N/A":
|
if last_block_height and last_block_height != "N/A":
|
||||||
if self.last_block_height is not None and self.last_block_height != last_block_height:
|
if self.last_block_height is not None and self.last_block_height != last_block_height:
|
||||||
logging.info(f"Block change detected: {self.last_block_height} -> {last_block_height}")
|
logging.info(f"[NotificationService] Block change detected: {self.last_block_height} -> {last_block_height}")
|
||||||
block_notification = self._generate_block_notification(current_metrics)
|
block_notification = self._generate_block_notification(current_metrics)
|
||||||
if block_notification:
|
if block_notification:
|
||||||
new_notifications.append(block_notification)
|
new_notifications.append(block_notification)
|
||||||
@ -273,7 +318,7 @@ class NotificationService:
|
|||||||
return new_notifications
|
return new_notifications
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error generating notifications: {e}")
|
logging.error(f"[NotificationService] Error generating notifications: {e}")
|
||||||
error_notification = self.add_notification(
|
error_notification = self.add_notification(
|
||||||
f"Error generating notifications: {str(e)}",
|
f"Error generating notifications: {str(e)}",
|
||||||
level=NotificationLevel.ERROR,
|
level=NotificationLevel.ERROR,
|
||||||
@ -281,42 +326,28 @@ class NotificationService:
|
|||||||
)
|
)
|
||||||
return [error_notification]
|
return [error_notification]
|
||||||
|
|
||||||
def _should_post_daily_stats(self):
|
def _should_post_daily_stats(self) -> bool:
|
||||||
"""Check if it's time to post daily stats (once per day at 12 PM)."""
|
"""Check if it's time to post daily stats with improved clarity."""
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
# 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:
|
# Only proceed if we're in the target hour and within first 5 minutes
|
||||||
# 1. It's a different day AND
|
if now.hour != DEFAULT_TARGET_HOUR or now.minute >= NOTIFICATION_WINDOW_MINUTES:
|
||||||
# 2. It's the target hour (12 PM) AND
|
return False
|
||||||
# 3. It's within the first 5 minutes of that hour
|
|
||||||
if is_different_day and current_hour == target_hour and current_minute < 5:
|
# If we have a last_daily_stats timestamp, check if it's a different day
|
||||||
logging.info(f"Posting daily stats at {current_hour}:{current_minute}")
|
if self.last_daily_stats and now.date() <= self.last_daily_stats.date():
|
||||||
self.last_daily_stats = now
|
return False
|
||||||
return True
|
|
||||||
else:
|
# All conditions met, update timestamp and return True
|
||||||
# First time - post if we're at the target hour
|
logging.info(f"[NotificationService] Posting daily stats at {now.hour}:{now.minute}")
|
||||||
if current_hour == target_hour and current_minute < 5:
|
self.last_daily_stats = now
|
||||||
logging.info(f"First time posting daily stats at {current_hour}:{current_minute}")
|
return True
|
||||||
self.last_daily_stats = now
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
def _generate_daily_stats(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
|
|
||||||
def _generate_daily_stats(self, metrics):
|
|
||||||
"""Generate daily stats notification."""
|
"""Generate daily stats notification."""
|
||||||
try:
|
try:
|
||||||
if not metrics:
|
if not metrics:
|
||||||
logging.warning("No metrics available for daily stats")
|
logging.warning("[NotificationService] No metrics available for daily stats")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Format hashrate with appropriate unit
|
# Format hashrate with appropriate unit
|
||||||
@ -331,7 +362,7 @@ class NotificationService:
|
|||||||
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined (${daily_profit_usd:.2f})"
|
message = f"Daily Mining Summary: {hashrate_24hr} {hashrate_unit} average hashrate, {daily_mined_sats} SATS mined (${daily_profit_usd:.2f})"
|
||||||
|
|
||||||
# Add notification
|
# Add notification
|
||||||
logging.info(f"Generating daily stats notification: {message}")
|
logging.info(f"[NotificationService] Generating daily stats notification: {message}")
|
||||||
return self.add_notification(
|
return self.add_notification(
|
||||||
message,
|
message,
|
||||||
level=NotificationLevel.INFO,
|
level=NotificationLevel.INFO,
|
||||||
@ -344,16 +375,16 @@ class NotificationService:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error generating daily stats notification: {e}")
|
logging.error(f"[NotificationService] Error generating daily stats notification: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _generate_block_notification(self, metrics):
|
def _generate_block_notification(self, metrics: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""Generate notification for a new block found."""
|
"""Generate notification for a new block found."""
|
||||||
try:
|
try:
|
||||||
last_block_height = metrics.get("last_block_height", "Unknown")
|
last_block_height = metrics.get("last_block_height", "Unknown")
|
||||||
last_block_earnings = metrics.get("last_block_earnings", "0")
|
last_block_earnings = metrics.get("last_block_earnings", "0")
|
||||||
|
|
||||||
logging.info(f"Generating block notification: height={last_block_height}, earnings={last_block_earnings}")
|
logging.info(f"[NotificationService] Generating block notification: height={last_block_height}, earnings={last_block_earnings}")
|
||||||
|
|
||||||
message = f"New block found by the pool! Block #{last_block_height}, earnings: {last_block_earnings} SATS"
|
message = f"New block found by the pool! Block #{last_block_height}, earnings: {last_block_earnings} SATS"
|
||||||
|
|
||||||
@ -367,87 +398,95 @@ class NotificationService:
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error generating block notification: {e}")
|
logging.error(f"[NotificationService] Error generating block notification: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _check_hashrate_change(self, current, previous):
|
def _parse_numeric_value(self, value_str: Any) -> float:
|
||||||
|
"""Parse numeric values from strings that may include units."""
|
||||||
|
if isinstance(value_str, (int, float)):
|
||||||
|
return float(value_str)
|
||||||
|
|
||||||
|
if isinstance(value_str, str):
|
||||||
|
# Extract just the numeric part
|
||||||
|
parts = value_str.split()
|
||||||
|
try:
|
||||||
|
return float(parts[0])
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
return 0.0
|
||||||
|
|
||||||
|
def _check_hashrate_change(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""Check for significant hashrate changes using 10-minute average."""
|
"""Check for significant hashrate changes using 10-minute average."""
|
||||||
try:
|
try:
|
||||||
# Change from 3hr to 10min hashrate values
|
# Get 10min hashrate values
|
||||||
current_10min = current.get("hashrate_10min", 0)
|
current_10min = current.get("hashrate_10min", 0)
|
||||||
previous_10min = previous.get("hashrate_10min", 0)
|
previous_10min = previous.get("hashrate_10min", 0)
|
||||||
|
|
||||||
# Log what we're comparing
|
# Log what we're comparing
|
||||||
logging.debug(f"Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}")
|
logging.debug(f"[NotificationService] Comparing 10min hashrates - current: {current_10min}, previous: {previous_10min}")
|
||||||
|
|
||||||
# Skip if values are missing
|
# Skip if values are missing
|
||||||
if not current_10min or not previous_10min:
|
if not current_10min or not previous_10min:
|
||||||
logging.debug("Skipping hashrate check - missing values")
|
logging.debug("[NotificationService] Skipping hashrate check - missing values")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Handle strings with units (e.g., "10.5 TH/s")
|
# Parse values consistently
|
||||||
if isinstance(current_10min, str):
|
current_value = self._parse_numeric_value(current_10min)
|
||||||
current_10min = float(current_10min.split()[0])
|
previous_value = self._parse_numeric_value(previous_10min)
|
||||||
else:
|
|
||||||
current_10min = float(current_10min)
|
logging.debug(f"[NotificationService] Converted 10min hashrates - current: {current_value}, previous: {previous_value}")
|
||||||
|
|
||||||
if isinstance(previous_10min, str):
|
|
||||||
previous_10min = float(previous_10min.split()[0])
|
|
||||||
else:
|
|
||||||
previous_10min = float(previous_10min)
|
|
||||||
|
|
||||||
logging.debug(f"Converted 10min hashrates - current: {current_10min}, previous: {previous_10min}")
|
|
||||||
|
|
||||||
# Skip if previous was zero (prevents division by zero)
|
# Skip if previous was zero (prevents division by zero)
|
||||||
if previous_10min == 0:
|
if previous_value == 0:
|
||||||
logging.debug("Skipping hashrate check - previous was zero")
|
logging.debug("[NotificationService] Skipping hashrate check - previous was zero")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# Calculate percentage change
|
# Calculate percentage change
|
||||||
percent_change = ((current_10min - previous_10min) / previous_10min) * 100
|
percent_change = ((current_value - previous_value) / previous_value) * 100
|
||||||
logging.debug(f"10min hashrate change: {percent_change:.1f}%")
|
logging.debug(f"[NotificationService] 10min hashrate change: {percent_change:.1f}%")
|
||||||
|
|
||||||
# Significant decrease (more than 25%)
|
# Significant decrease
|
||||||
if percent_change <= -25:
|
if percent_change <= -SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
|
||||||
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease"
|
message = f"Significant 10min hashrate drop detected: {abs(percent_change):.1f}% decrease"
|
||||||
logging.info(f"Generating hashrate notification: {message}")
|
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
|
||||||
return self.add_notification(
|
return self.add_notification(
|
||||||
message,
|
message,
|
||||||
level=NotificationLevel.WARNING,
|
level=NotificationLevel.WARNING,
|
||||||
category=NotificationCategory.HASHRATE,
|
category=NotificationCategory.HASHRATE,
|
||||||
data={
|
data={
|
||||||
"previous": previous_10min,
|
"previous": previous_value,
|
||||||
"current": current_10min,
|
"current": current_value,
|
||||||
"change": percent_change,
|
"change": percent_change,
|
||||||
"timeframe": "10min" # Add timeframe to the data
|
"timeframe": "10min"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Significant increase (more than 25%)
|
# Significant increase
|
||||||
elif percent_change >= 25:
|
elif percent_change >= SIGNIFICANT_HASHRATE_CHANGE_PERCENT:
|
||||||
message = f"10min hashrate increase detected: {percent_change:.1f}% increase"
|
message = f"10min hashrate increase detected: {percent_change:.1f}% increase"
|
||||||
logging.info(f"Generating hashrate notification: {message}")
|
logging.info(f"[NotificationService] Generating hashrate notification: {message}")
|
||||||
return self.add_notification(
|
return self.add_notification(
|
||||||
message,
|
message,
|
||||||
level=NotificationLevel.SUCCESS,
|
level=NotificationLevel.SUCCESS,
|
||||||
category=NotificationCategory.HASHRATE,
|
category=NotificationCategory.HASHRATE,
|
||||||
data={
|
data={
|
||||||
"previous": previous_10min,
|
"previous": previous_value,
|
||||||
"current": current_10min,
|
"current": current_value,
|
||||||
"change": percent_change,
|
"change": percent_change,
|
||||||
"timeframe": "10min" # Add timeframe to the data
|
"timeframe": "10min"
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error checking hashrate change: {e}")
|
logging.error(f"[NotificationService] Error checking hashrate change: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _check_earnings_progress(self, current, previous):
|
def _check_earnings_progress(self, current: Dict[str, Any], previous: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
||||||
"""Check for significant earnings progress or payout approach."""
|
"""Check for significant earnings progress or payout approach."""
|
||||||
try:
|
try:
|
||||||
current_unpaid = float(current.get("unpaid_earnings", "0").split()[0]) if isinstance(current.get("unpaid_earnings"), str) else current.get("unpaid_earnings", 0)
|
current_unpaid = self._parse_numeric_value(current.get("unpaid_earnings", "0"))
|
||||||
|
|
||||||
# Check if approaching payout
|
# Check if approaching payout
|
||||||
if current.get("est_time_to_payout"):
|
if current.get("est_time_to_payout"):
|
||||||
@ -480,7 +519,7 @@ class NotificationService:
|
|||||||
|
|
||||||
# Check for payout (unpaid balance reset)
|
# Check for payout (unpaid balance reset)
|
||||||
if previous.get("unpaid_earnings"):
|
if previous.get("unpaid_earnings"):
|
||||||
previous_unpaid = float(previous.get("unpaid_earnings", "0").split()[0]) if isinstance(previous.get("unpaid_earnings"), str) else previous.get("unpaid_earnings", 0)
|
previous_unpaid = self._parse_numeric_value(previous.get("unpaid_earnings", "0"))
|
||||||
|
|
||||||
# If balance significantly decreased, likely a payout occurred
|
# If balance significantly decreased, likely a payout occurred
|
||||||
if previous_unpaid > 0 and current_unpaid < previous_unpaid * 0.5:
|
if previous_unpaid > 0 and current_unpaid < previous_unpaid * 0.5:
|
||||||
@ -498,12 +537,12 @@ class NotificationService:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Error checking earnings progress: {e}")
|
logging.error(f"[NotificationService] Error checking earnings progress: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _should_send_payout_notification(self):
|
def _should_send_payout_notification(self) -> bool:
|
||||||
"""Check if enough time has passed since the last payout notification."""
|
"""Check if enough time has passed since the last payout notification."""
|
||||||
if self.last_payout_notification_time is None:
|
if self.last_payout_notification_time is None:
|
||||||
return True
|
return True
|
||||||
time_since_last_notification = datetime.now() - self.last_payout_notification_time
|
time_since_last_notification = datetime.now() - self.last_payout_notification_time
|
||||||
return time_since_last_notification.total_seconds() > 86400 # 1 Day
|
return time_since_last_notification.total_seconds() > ONE_DAY_SECONDS
|
@ -5,22 +5,29 @@ This document provides a comprehensive overview of the Bitcoin Mining Dashboard
|
|||||||
## Directory Structure
|
## Directory Structure
|
||||||
|
|
||||||
```
|
```
|
||||||
bitcoin-mining-dashboard/
|
DeepSea-Dashboard/
|
||||||
│
|
│
|
||||||
├── App.py # Main application entry point and Flask routes
|
├── App.py # Main application entry point
|
||||||
├── config.py # Configuration management utilities
|
├── config.py # Configuration management
|
||||||
├── config.json # User configuration file
|
├── config.json # Configuration file
|
||||||
├── data_service.py # Service for fetching mining/market data
|
├── data_service.py # Service for fetching mining data
|
||||||
├── models.py # Data models and conversion utilities
|
├── models.py # Data models
|
||||||
├── state_manager.py # Manager for persistent state and history
|
├── state_manager.py # Manager for persistent state
|
||||||
├── worker_service.py # Service for worker data management
|
├── worker_service.py # Service for worker data management
|
||||||
|
├── notification_service.py # Service for notifications
|
||||||
|
├── minify.py # Script for minifying assets
|
||||||
|
├── setup.py # Setup script for organizing files
|
||||||
|
├── requirements.txt # Python dependencies
|
||||||
|
├── Dockerfile # Docker configuration
|
||||||
|
├── docker-compose.yml # Docker Compose configuration
|
||||||
│
|
│
|
||||||
├── templates/ # HTML templates
|
├── templates/ # HTML templates
|
||||||
│ ├── base.html # Base template with common elements
|
│ ├── base.html # Base template with common elements
|
||||||
│ ├── boot.html # Boot sequence animation
|
│ ├── boot.html # Boot sequence animation
|
||||||
│ ├── dashboard.html # Main dashboard template
|
│ ├── dashboard.html # Main dashboard template
|
||||||
│ ├── workers.html # Workers overview template
|
│ ├── workers.html # Workers dashboard template
|
||||||
│ ├── blocks.html # Bitcoin blocks template
|
│ ├── blocks.html # Bitcoin blocks template
|
||||||
|
│ ├── notifications.html # Notifications template
|
||||||
│ └── error.html # Error page template
|
│ └── error.html # Error page template
|
||||||
│
|
│
|
||||||
├── static/ # Static assets
|
├── static/ # Static assets
|
||||||
@ -30,23 +37,24 @@ bitcoin-mining-dashboard/
|
|||||||
│ │ ├── workers.css # Workers page styles
|
│ │ ├── workers.css # Workers page styles
|
||||||
│ │ ├── boot.css # Boot sequence styles
|
│ │ ├── boot.css # Boot sequence styles
|
||||||
│ │ ├── blocks.css # Blocks page styles
|
│ │ ├── blocks.css # Blocks page styles
|
||||||
|
│ │ ├── notifications.css # Notifications page styles
|
||||||
│ │ ├── error.css # Error page styles
|
│ │ ├── error.css # Error page styles
|
||||||
│ │ └── retro-refresh.css # Floating system monitor styles
|
│ │ ├── retro-refresh.css # Floating refresh bar styles
|
||||||
|
│ │ └── theme-toggle.css # Theme toggle styles
|
||||||
│ │
|
│ │
|
||||||
│ └── js/ # JavaScript files
|
│ └── js/ # JavaScript files
|
||||||
│ ├── main.js # Main dashboard functionality
|
│ ├── main.js # Main dashboard functionality
|
||||||
│ ├── workers.js # Workers page functionality
|
│ ├── workers.js # Workers page functionality
|
||||||
│ ├── blocks.js # Blocks page functionality
|
│ ├── blocks.js # Blocks page functionality
|
||||||
|
│ ├── notifications.js # Notifications functionality
|
||||||
│ ├── block-animation.js # Block mining animation
|
│ ├── block-animation.js # Block mining animation
|
||||||
│ └── BitcoinProgressBar.js # System monitor implementation
|
│ ├── BitcoinProgressBar.js # System monitor functionality
|
||||||
|
│ └── theme.js # Theme toggle functionality
|
||||||
│
|
│
|
||||||
├── logs/ # Application logs directory
|
├── deployment_steps.md # Deployment guide
|
||||||
├── requirements.txt # Python dependencies
|
├── project_structure.md # Additional structure documentation
|
||||||
├── Dockerfile # Docker configuration
|
├── LICENSE.md # License information
|
||||||
├── setup.py # Setup script for organizing files
|
└── logs/ # Application logs (generated at runtime)
|
||||||
├── deployment_steps.md # Deployment documentation
|
|
||||||
├── project_structure.md # This document
|
|
||||||
└── README.md # Project overview and instructions
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Core Components
|
## Core Components
|
||||||
@ -120,7 +128,7 @@ The application uses Jinja2 templates with a retro-themed design:
|
|||||||
Client-side functionality is organized into modular JavaScript files:
|
Client-side functionality is organized into modular JavaScript files:
|
||||||
- **main.js**: Dashboard functionality, real-time updates, and chart rendering
|
- **main.js**: Dashboard functionality, real-time updates, and chart rendering
|
||||||
- **workers.js**: Worker grid rendering, filtering, and mini-chart creation
|
- **workers.js**: Worker grid rendering, filtering, and mini-chart creation
|
||||||
- **blocks.js**: Block explorer with data fetching from mempool.space
|
- **blocks.js**: Block explorer with data fetching from mempool.guide
|
||||||
- **block-animation.js**: Interactive block mining animation
|
- **block-animation.js**: Interactive block mining animation
|
||||||
- **BitcoinProgressBar.js**: Floating system monitor with uptime and connection status
|
- **BitcoinProgressBar.js**: Floating system monitor with uptime and connection status
|
||||||
|
|
||||||
@ -210,28 +218,28 @@ All hashrates are normalized to TH/s internally because:
|
|||||||
|
|
||||||
```
|
```
|
||||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||||
│ Ocean.xyz API │ │ blockchain.info │ │ mempool.space │
|
│ Ocean.xyz API │ │ blockchain.info │ │ mempool.guide │
|
||||||
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
|
||||||
│ │ │
|
│ │ │
|
||||||
▼ ▼ ▼
|
▼ ▼ ▼
|
||||||
┌────────────────────────────────────────────────────────────────────┐
|
┌────────────────────────────────────────────────────────────────────┐
|
||||||
│ 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 │
|
||||||
└───────────────────────────────┬────────────────────────────────────┘
|
└───────────────────────────────┬────────────────────────────────────┘
|
||||||
│
|
│
|
||||||
┌────────────────────────────────────────────┐
|
┌────────────────────────────────────────────┐
|
||||||
|
26
setup.py
26
setup.py
@ -60,6 +60,7 @@ FILE_MAPPINGS = {
|
|||||||
'retro-refresh.css': 'static/css/retro-refresh.css',
|
'retro-refresh.css': 'static/css/retro-refresh.css',
|
||||||
'blocks.css': 'static/css/blocks.css',
|
'blocks.css': 'static/css/blocks.css',
|
||||||
'notifications.css': 'static/css/notifications.css',
|
'notifications.css': 'static/css/notifications.css',
|
||||||
|
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
|
||||||
|
|
||||||
# JS files
|
# JS files
|
||||||
'main.js': 'static/js/main.js',
|
'main.js': 'static/js/main.js',
|
||||||
@ -67,6 +68,7 @@ FILE_MAPPINGS = {
|
|||||||
'blocks.js': 'static/js/blocks.js',
|
'blocks.js': 'static/js/blocks.js',
|
||||||
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
|
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
|
||||||
'notifications.js': 'static/js/notifications.js',
|
'notifications.js': 'static/js/notifications.js',
|
||||||
|
'theme.js': 'static/js/theme.js', # Added theme.js
|
||||||
|
|
||||||
# Template files
|
# Template files
|
||||||
'base.html': 'templates/base.html',
|
'base.html': 'templates/base.html',
|
||||||
@ -82,7 +84,9 @@ FILE_MAPPINGS = {
|
|||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
"power_cost": 0.0,
|
"power_cost": 0.0,
|
||||||
"power_usage": 0.0,
|
"power_usage": 0.0,
|
||||||
"wallet": "yourwallethere"
|
"wallet": "yourwallethere",
|
||||||
|
"timezone": "America/Los_Angeles", # Added default timezone
|
||||||
|
"network_fee": 0.0 # Added default network fee
|
||||||
}
|
}
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
@ -92,10 +96,13 @@ def parse_arguments():
|
|||||||
parser.add_argument('--wallet', type=str, help='Set your Ocean.xyz wallet address')
|
parser.add_argument('--wallet', type=str, help='Set your Ocean.xyz wallet address')
|
||||||
parser.add_argument('--power-cost', type=float, help='Set your electricity cost per kWh')
|
parser.add_argument('--power-cost', type=float, help='Set your electricity cost per kWh')
|
||||||
parser.add_argument('--power-usage', type=float, help='Set your power consumption in watts')
|
parser.add_argument('--power-usage', type=float, help='Set your power consumption in watts')
|
||||||
|
parser.add_argument('--network-fee', type=float, help='Set your network fee percentage') # Added network fee parameter
|
||||||
|
parser.add_argument('--timezone', type=str, help='Set your timezone (e.g., America/Los_Angeles)') # Added timezone parameter
|
||||||
parser.add_argument('--skip-checks', action='store_true', help='Skip dependency checks')
|
parser.add_argument('--skip-checks', action='store_true', help='Skip dependency checks')
|
||||||
parser.add_argument('--force', action='store_true', help='Force file overwrite')
|
parser.add_argument('--force', action='store_true', help='Force file overwrite')
|
||||||
parser.add_argument('--config', type=str, help='Path to custom config.json')
|
parser.add_argument('--config', type=str, help='Path to custom config.json')
|
||||||
parser.add_argument('--minify', action='store_true', help='Minify JavaScript files')
|
parser.add_argument('--minify', action='store_true', help='Minify JavaScript files')
|
||||||
|
parser.add_argument('--theme', choices=['bitcoin', 'deepsea'], help='Set the default UI theme') # Added theme parameter
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
def create_directory_structure():
|
def create_directory_structure():
|
||||||
@ -275,6 +282,19 @@ def create_config(args):
|
|||||||
else:
|
else:
|
||||||
logger.warning("Power usage cannot be negative, using default or existing value")
|
logger.warning("Power usage cannot be negative, using default or existing value")
|
||||||
|
|
||||||
|
# Update config from command line arguments
|
||||||
|
if args.timezone:
|
||||||
|
config["timezone"] = args.timezone
|
||||||
|
|
||||||
|
if args.network_fee is not None:
|
||||||
|
if args.network_fee >= 0:
|
||||||
|
config["network_fee"] = args.network_fee
|
||||||
|
else:
|
||||||
|
logger.warning("Network fee cannot be negative, using default or existing value")
|
||||||
|
|
||||||
|
if args.theme:
|
||||||
|
config["theme"] = args.theme
|
||||||
|
|
||||||
# Save the configuration
|
# Save the configuration
|
||||||
try:
|
try:
|
||||||
with open(config_file, 'w') as f:
|
with open(config_file, 'w') as f:
|
||||||
@ -288,7 +308,9 @@ def create_config(args):
|
|||||||
logger.info("Current configuration:")
|
logger.info("Current configuration:")
|
||||||
logger.info(f" ├── Wallet address: {config['wallet']}")
|
logger.info(f" ├── Wallet address: {config['wallet']}")
|
||||||
logger.info(f" ├── Power cost: ${config['power_cost']} per kWh")
|
logger.info(f" ├── Power cost: ${config['power_cost']} per kWh")
|
||||||
logger.info(f" └── Power usage: {config['power_usage']} watts")
|
logger.info(f" ├── Power usage: {config['power_usage']} watts")
|
||||||
|
logger.info(f" ├── Network fee: {config['network_fee']}%")
|
||||||
|
logger.info(f" └── Timezone: {config['timezone']}")
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import time
|
|||||||
import gc
|
import gc
|
||||||
import threading
|
import threading
|
||||||
import redis
|
import redis
|
||||||
|
from config import get_timezone
|
||||||
|
|
||||||
# Global variables for arrow history, legacy hashrate history, and a log of full metrics snapshots.
|
# Global variables for arrow history, legacy hashrate history, and a log of full metrics snapshots.
|
||||||
arrow_history = {} # stored per second
|
arrow_history = {} # stored per second
|
||||||
@ -327,7 +328,7 @@ class StateManager:
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
|
||||||
current_second = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%H:%M:%S")
|
current_second = datetime.now(ZoneInfo(get_timezone())).strftime("%H:%M:%S")
|
||||||
|
|
||||||
with state_lock:
|
with state_lock:
|
||||||
for key in arrow_keys:
|
for key in arrow_keys:
|
||||||
|
@ -112,7 +112,6 @@
|
|||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-time {
|
.block-time {
|
||||||
@ -142,27 +141,22 @@
|
|||||||
|
|
||||||
.block-info-value.yellow {
|
.block-info-value.yellow {
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-info-value.green {
|
.block-info-value.green {
|
||||||
color: #32CD32;
|
color: #32CD32;
|
||||||
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-info-value.blue {
|
.block-info-value.blue {
|
||||||
color: #00dfff;
|
color: #00dfff;
|
||||||
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-info-value.white {
|
.block-info-value.white {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-info-value.red {
|
.block-info-value.red {
|
||||||
color: #ff5555;
|
color: #ff5555;
|
||||||
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Loader */
|
/* Loader */
|
||||||
@ -221,7 +215,6 @@
|
|||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
border-bottom: 1px solid var(--primary-color);
|
border-bottom: 1px solid var(--primary-color);
|
||||||
text-shadow: 0 0 5px var(--primary-color);
|
|
||||||
animation: flicker 4s infinite;
|
animation: flicker 4s infinite;
|
||||||
font-family: var(--header-font);
|
font-family: var(--header-font);
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -242,7 +235,6 @@
|
|||||||
.block-modal-close:hover,
|
.block-modal-close:hover,
|
||||||
.block-modal-close:focus {
|
.block-modal-close:focus {
|
||||||
color: #ffa500;
|
color: #ffa500;
|
||||||
text-shadow: 0 0 10px rgba(255, 165, 0, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.block-modal-body {
|
.block-modal-body {
|
||||||
@ -265,7 +257,6 @@
|
|||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,3 +1,442 @@
|
|||||||
|
/* Config form styling - fixed width and hidden by default */
|
||||||
|
#config-form {
|
||||||
|
display: none;
|
||||||
|
width: 500px;
|
||||||
|
max-width: 90%;
|
||||||
|
margin: 30px auto;
|
||||||
|
padding: 20px;
|
||||||
|
background-color: #0d0d0d;
|
||||||
|
border: 1px solid #f7931a;
|
||||||
|
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boot text color - updated with theme toggling */
|
||||||
|
body:not(.deepsea-theme) #terminal,
|
||||||
|
body:not(.deepsea-theme) #output,
|
||||||
|
body:not(.deepsea-theme) #prompt-container,
|
||||||
|
body:not(.deepsea-theme) #prompt-text,
|
||||||
|
body:not(.deepsea-theme) #user-input,
|
||||||
|
body:not(.deepsea-theme) #loading-message {
|
||||||
|
color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DeepSea theme text color */
|
||||||
|
body.deepsea-theme #terminal,
|
||||||
|
body.deepsea-theme #output,
|
||||||
|
body.deepsea-theme #prompt-container,
|
||||||
|
body.deepsea-theme #prompt-text,
|
||||||
|
body.deepsea-theme #user-input,
|
||||||
|
body.deepsea-theme #loading-message {
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DeepSea cursor color */
|
||||||
|
body.deepsea-theme .cursor,
|
||||||
|
body.deepsea-theme .prompt-cursor {
|
||||||
|
background-color: #0088cc;
|
||||||
|
box-shadow: 0 0 5px rgba(0, 136, 204, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Boot-specific DeepSea theme adjustments */
|
||||||
|
body.deepsea-theme #bitcoin-logo {
|
||||||
|
color: #0088cc;
|
||||||
|
border-color: #0088cc;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 136, 204, 0.5);
|
||||||
|
box-shadow: 0 0 15px rgba(0, 136, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme #config-form {
|
||||||
|
border: 1px solid #0088cc;
|
||||||
|
box-shadow: 0 0 10px rgba(0, 136, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .config-title {
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .form-group label {
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .form-group input,
|
||||||
|
body.deepsea-theme .form-group select {
|
||||||
|
border: 1px solid #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .form-group input:focus,
|
||||||
|
body.deepsea-theme .form-group select:focus {
|
||||||
|
box-shadow: 0 0 5px #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .btn {
|
||||||
|
background-color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .btn:hover {
|
||||||
|
background-color: #00b3ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .btn-secondary {
|
||||||
|
background-color: #333;
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .tooltip .tooltip-text {
|
||||||
|
border: 1px solid #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .form-group select {
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, #0088cc 50%), linear-gradient(135deg, #0088cc 50%, transparent 50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* DeepSea skip button */
|
||||||
|
body.deepsea-theme #skip-button {
|
||||||
|
background-color: #0088cc;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 136, 204, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme #skip-button:hover {
|
||||||
|
background-color: #00b3ff;
|
||||||
|
box-shadow: 0 0 12px rgba(0, 136, 204, 0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Original Bitcoin styling preserved by default */
|
||||||
|
.config-title {
|
||||||
|
font-size: 24px;
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input,
|
||||||
|
.form-group select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px;
|
||||||
|
background-color: #0d0d0d;
|
||||||
|
border: 1px solid #f7931a;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group input:focus,
|
||||||
|
.form-group select:focus {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: 0 0 5px #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f7931a;
|
||||||
|
color: #000;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #ffa32e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background-color: #333;
|
||||||
|
color: #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#form-message {
|
||||||
|
margin-top: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-success {
|
||||||
|
background-color: rgba(50, 205, 50, 0.2);
|
||||||
|
border: 1px solid #32CD32;
|
||||||
|
color: #32CD32;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message-error {
|
||||||
|
background-color: rgba(255, 0, 0, 0.2);
|
||||||
|
border: 1px solid #ff0000;
|
||||||
|
color: #ff0000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
background-color: #333;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 50%;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 14px;
|
||||||
|
font-size: 10px;
|
||||||
|
cursor: help;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip .tooltip-text {
|
||||||
|
visibility: hidden;
|
||||||
|
width: 200px;
|
||||||
|
background-color: #000;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 5px;
|
||||||
|
position: absolute;
|
||||||
|
z-index: 1;
|
||||||
|
bottom: 125%;
|
||||||
|
left: 50%;
|
||||||
|
margin-left: -100px;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s;
|
||||||
|
font-size: 14px;
|
||||||
|
border: 1px solid #f7931a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tooltip:hover .tooltip-text {
|
||||||
|
visibility: visible;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the select dropdown with custom arrow */
|
||||||
|
.form-group select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
background-image: linear-gradient(45deg, transparent 50%, #f7931a 50%), linear-gradient(135deg, #f7931a 50%, transparent 50%);
|
||||||
|
background-position: calc(100% - 15px) calc(1em + 0px), calc(100% - 10px) calc(1em + 0px);
|
||||||
|
background-size: 5px 5px, 5px 5px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base styling for the Bitcoin logo */
|
||||||
|
#bitcoin-logo {
|
||||||
|
position: relative;
|
||||||
|
white-space: pre;
|
||||||
|
font-family: monospace;
|
||||||
|
height: 130px; /* Set fixed height to match original logo */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Update the DeepSea theme logo styling */
|
||||||
|
body.deepsea-theme #bitcoin-logo {
|
||||||
|
color: transparent; /* Hide original logo */
|
||||||
|
position: relative;
|
||||||
|
text-shadow: none;
|
||||||
|
min-height: 120px; /* Ensure enough height for the new logo */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add the new DeepSea ASCII art */
|
||||||
|
body.deepsea-theme #bitcoin-logo::after {
|
||||||
|
content: " ____ ____ \A| _ \\ ___ ___ _ __/ ___| ___ __ _ \A| | | |/ _ \\/ _ \\ '_ \\___ \\ / _ \\/ _` |\A| |_| | __/ __/ |_) |__) | __/ (_| |\A|____/ \\___|\\___|_.__/____/ \\___|\\__,_|\A|_| ";
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%); /* Center perfectly */
|
||||||
|
font-size: 100%; /* Full size */
|
||||||
|
font-weight: bold;
|
||||||
|
line-height: 1.2;
|
||||||
|
color: #0088cc;
|
||||||
|
white-space: pre;
|
||||||
|
display: block;
|
||||||
|
text-shadow: 0 0 10px rgba(0, 136, 204, 0.5);
|
||||||
|
font-family: monospace;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add "DeepSea" version info */
|
||||||
|
body.deepsea-theme #bitcoin-logo::before {
|
||||||
|
content: "v.21";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 10px;
|
||||||
|
color: #0088cc;
|
||||||
|
font-size: 16px;
|
||||||
|
text-shadow: 0 0 5px rgba(0, 136, 204, 0.5);
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
z-index: 2; /* Ensure version displays on top */
|
||||||
|
}
|
||||||
|
/* Ocean Wave Ripple Effect for DeepSea Theme */
|
||||||
|
body.deepsea-theme::after {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: transparent;
|
||||||
|
opacity: 0.1;
|
||||||
|
z-index: 10;
|
||||||
|
animation: oceanRipple 8s infinite linear;
|
||||||
|
background-image: repeating-linear-gradient( 0deg, rgba(0, 136, 204, 0.1), rgba(0, 136, 204, 0.1) 1px, transparent 1px, transparent 6px );
|
||||||
|
background-size: 100% 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ocean waves moving animation */
|
||||||
|
@keyframes oceanRipple {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateY(6px);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Retro glitch effect for DeepSea Theme */
|
||||||
|
body.deepsea-theme::before {
|
||||||
|
content: "";
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 3;
|
||||||
|
opacity: 0.15;
|
||||||
|
background-image: linear-gradient(rgba(18, 16, 16, 0) 50%, rgba(0, 73, 109, 0.1) 50%), linear-gradient(90deg, rgba(0, 81, 122, 0.03), rgba(0, 136, 204, 0.08), rgba(0, 191, 255, 0.03));
|
||||||
|
background-size: 100% 2px, 3px 100%;
|
||||||
|
animation: glitchEffect 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Glitch animation */
|
||||||
|
@keyframes glitchEffect {
|
||||||
|
0% {
|
||||||
|
opacity: 0.15;
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
opacity: 0.17;
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
opacity: 0.14;
|
||||||
|
background-position: -1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
opacity: 0.15;
|
||||||
|
background-position: 1px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
opacity: 0.16;
|
||||||
|
background-position: -2px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
opacity: 0.15;
|
||||||
|
background-position: 0 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Deep underwater light rays */
|
||||||
|
body.deepsea-theme {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.deepsea-theme .underwater-rays {
|
||||||
|
position: fixed;
|
||||||
|
top: -50%;
|
||||||
|
left: -50%;
|
||||||
|
right: -50%;
|
||||||
|
bottom: -50%;
|
||||||
|
width: 200%;
|
||||||
|
height: 200%;
|
||||||
|
background: rgba(0, 0, 0, 0);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1;
|
||||||
|
background-image: radial-gradient(ellipse at top, rgba(0, 136, 204, 0.1) 0%, rgba(0, 136, 204, 0) 70%), radial-gradient(ellipse at bottom, rgba(0, 91, 138, 0.15) 0%, rgba(0, 0, 0, 0) 70%);
|
||||||
|
animation: lightRays 15s ease infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Light ray animation */
|
||||||
|
@keyframes lightRays {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg) scale(1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg) scale(1.1);
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle digital noise texture */
|
||||||
|
body.deepsea-theme .digital-noise {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAA3XAAAN1wFCKJt4AAAAB3RJTUUH4woEFQwNDaabTQAAABl0RVh0Q29tbWVudABDcmVhdGVkIHdpdGggR0lNUFeBDhcAAACASURBVGje7dixDcIwFEbhb8QMKWn5dwEWY4fswAasRJkBkhfAIarsNDEF5x5LrV/dJ1cEAAAAAOzHuefF5byzZ7tS6xDj6qoQpdRxUvNM6lH3rPeM1+ZJ3ROtqe9feGcjY8z74M8UvJGxEVHxTcIbGSsR+SECAAAAsC9/8G82GwHDD80AAAAASUVORK5CYII=');
|
||||||
|
opacity: 0.05;
|
||||||
|
z-index: 2;
|
||||||
|
pointer-events: none;
|
||||||
|
animation: noise 0.5s steps(5) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Noise animation */
|
||||||
|
@keyframes noise {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
20% {
|
||||||
|
transform: translate(-1px, 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
40% {
|
||||||
|
transform: translate(1px, -1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
60% {
|
||||||
|
transform: translate(-2px, -1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
80% {
|
||||||
|
transform: translate(2px, 1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translate(0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Base Styles with a subtle radial background for extra depth */
|
/* Base Styles with a subtle radial background for extra depth */
|
||||||
body {
|
body {
|
||||||
background: linear-gradient(135deg, #121212, #000000);
|
background: linear-gradient(135deg, #121212, #000000);
|
||||||
@ -8,7 +447,6 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.4);
|
|
||||||
height: calc(100vh - 100px);
|
height: calc(100vh - 100px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -79,32 +517,26 @@ body::before {
|
|||||||
/* Neon-inspired color classes */
|
/* Neon-inspired color classes */
|
||||||
.green {
|
.green {
|
||||||
color: #39ff14 !important;
|
color: #39ff14 !important;
|
||||||
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
color: #00dfff !important;
|
color: #00dfff !important;
|
||||||
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yellow {
|
.yellow {
|
||||||
color: #ffd700 !important;
|
color: #ffd700 !important;
|
||||||
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
color: #ff2d2d !important;
|
color: #ff2d2d !important;
|
||||||
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.magenta {
|
.magenta {
|
||||||
color: #ff2d95 !important;
|
color: #ff2d95 !important;
|
||||||
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bitcoin Logo styling with extra neon border */
|
/* Bitcoin Logo styling with extra neon border */
|
||||||
@ -143,6 +575,7 @@ body::before {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
|
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
|
||||||
transition: all 0.2s ease;
|
transition: all 0.2s ease;
|
||||||
|
z-index: 50; /* Lower z-index value */
|
||||||
}
|
}
|
||||||
|
|
||||||
#skip-button:hover {
|
#skip-button:hover {
|
||||||
@ -150,6 +583,24 @@ body::before {
|
|||||||
box-shadow: 0 0 12px rgba(247, 147, 26, 0.7);
|
box-shadow: 0 0 12px rgba(247, 147, 26, 0.7);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mobile-specific adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
#skip-button {
|
||||||
|
bottom: 25px;
|
||||||
|
right: 10px;
|
||||||
|
padding: 10px 18px; /* Larger touch target for mobile */
|
||||||
|
font-size: 18px;
|
||||||
|
height: 40px;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add this to your CSS */
|
||||||
|
#config-form {
|
||||||
|
z-index: 100; /* Higher than the skip button */
|
||||||
|
position: relative; /* Needed for z-index to work properly */
|
||||||
|
}
|
||||||
|
|
||||||
/* Prompt Styling */
|
/* Prompt Styling */
|
||||||
#prompt-container {
|
#prompt-container {
|
||||||
display: none;
|
display: none;
|
||||||
@ -159,7 +610,6 @@ body::before {
|
|||||||
#prompt-text {
|
#prompt-text {
|
||||||
color: #f7931a;
|
color: #f7931a;
|
||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
|
||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -175,7 +625,6 @@ body::before {
|
|||||||
height: 33px;
|
height: 33px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: top;
|
vertical-align: top;
|
||||||
}
|
}
|
||||||
@ -203,7 +652,6 @@ body::before {
|
|||||||
#loading-message {
|
#loading-message {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#debug-info {
|
#debug-info {
|
||||||
|
@ -1,3 +1,73 @@
|
|||||||
|
.footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
padding: 10px 0;
|
||||||
|
color: grey;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
border-top: 1px solid rgba(128, 128, 128, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
</style >
|
||||||
|
<!-- Preload theme to prevent flicker -->
|
||||||
|
<style id="theme-preload" >
|
||||||
|
/* Theme-aware loading state */
|
||||||
|
html.bitcoin-theme {
|
||||||
|
background-color: #111111;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.deepsea-theme {
|
||||||
|
background-color: #0c141a;
|
||||||
|
}
|
||||||
|
|
||||||
|
#theme-loader {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 9999;
|
||||||
|
font-family: 'VT323', monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.bitcoin-theme #theme-loader {
|
||||||
|
background-color: #111111;
|
||||||
|
color: #f2a900;
|
||||||
|
}
|
||||||
|
|
||||||
|
html.deepsea-theme #theme-loader {
|
||||||
|
background-color: #0c141a;
|
||||||
|
color: #0088cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-icon {
|
||||||
|
font-size: 48px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
animation: spin 2s infinite linear;
|
||||||
|
}
|
||||||
|
|
||||||
|
#loader-text {
|
||||||
|
font-size: 24px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide content during load */
|
||||||
|
body {
|
||||||
|
visibility: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* Common styling elements shared across all pages */
|
/* Common styling elements shared across all pages */
|
||||||
:root {
|
:root {
|
||||||
--bg-color: #0a0a0a;
|
--bg-color: #0a0a0a;
|
||||||
@ -61,7 +131,6 @@ h1 {
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-family: var(--header-font);
|
font-family: var(--header-font);
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
text-shadow: 0 0 10px var(--primary-color);
|
|
||||||
animation: flicker 4s infinite;
|
animation: flicker 4s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,7 +179,6 @@ h1 {
|
|||||||
color: grey;
|
color: grey;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.7rem; /* Decreased font size */
|
font-size: 0.7rem; /* Decreased font size */
|
||||||
text-shadow: 0 0 5px grey;
|
|
||||||
padding: 5px 10px; /* Add padding for a larger clickable area */
|
padding: 5px 10px; /* Add padding for a larger clickable area */
|
||||||
transition: background-color 0.3s ease; /* Optional: Add hover effect */
|
transition: background-color 0.3s ease; /* Optional: Add hover effect */
|
||||||
}
|
}
|
||||||
@ -167,7 +235,6 @@ h1 {
|
|||||||
padding: 0.3rem 0.5rem;
|
padding: 0.3rem 0.5rem;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
border-bottom: 1px solid var(--primary-color);
|
border-bottom: 1px solid var(--primary-color);
|
||||||
text-shadow: 0 0 5px var(--primary-color);
|
|
||||||
animation: flicker 4s infinite;
|
animation: flicker 4s infinite;
|
||||||
font-family: var(--header-font);
|
font-family: var(--header-font);
|
||||||
}
|
}
|
||||||
@ -189,7 +256,6 @@ h1 {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
text-shadow: 0 0 5px rgba(255, 0, 0, 0.8);
|
|
||||||
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
|
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -245,16 +311,16 @@ h1 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.offline-dot {
|
.offline-dot {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: red;
|
background: red;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
margin-left: 0.5em;
|
margin-left: 0.5em;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -1px;
|
top: -1px;
|
||||||
animation: glow 3s infinite;
|
animation: glow 3s infinite;
|
||||||
box-shadow: 0 0 10px red, 0 0 20px red;
|
box-shadow: 0 0 10px red, 0 0 20px red !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes glowRed {
|
@keyframes glowRed {
|
||||||
@ -269,53 +335,44 @@ h1 {
|
|||||||
|
|
||||||
.red-glow, .status-red {
|
.red-glow, .status-red {
|
||||||
color: #ff2d2d !important;
|
color: #ff2d2d !important;
|
||||||
text-shadow: 0 0 2px #ff2d2d, 0 0 2px #ff2d2d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yellow-glow {
|
.yellow-glow {
|
||||||
color: #ffd700 !important;
|
color: #ffd700 !important;
|
||||||
text-shadow: 0 0 2px #ffd700, 0 0 2px #ffd700;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue-glow {
|
.blue-glow {
|
||||||
color: #00dfff !important;
|
color: #00dfff !important;
|
||||||
text-shadow: 0 0 2px #00dfff, 0 0 2px #00dfff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.white-glow {
|
.white-glow {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
text-shadow: 0 0 2px #ffffff, 0 0 2px #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Basic color classes for backward compatibility */
|
/* Basic color classes for backward compatibility */
|
||||||
.green {
|
.green {
|
||||||
color: #39ff14 !important;
|
color: #39ff14 !important;
|
||||||
text-shadow: 0 0 2px #39ff14, 0 0 2px #39ff14;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.blue {
|
.blue {
|
||||||
color: #00dfff !important;
|
color: #00dfff !important;
|
||||||
text-shadow: 0 0 2px #00dfff, 0 0 2px #00dfff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.yellow {
|
.yellow {
|
||||||
color: #ffd700 !important;
|
color: #ffd700 !important;
|
||||||
text-shadow: 0 0 2px #ffd700, 0 0 2px #ffd700;
|
font-weight: normal !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.white {
|
.white {
|
||||||
color: #ffffff !important;
|
color: #ffffff !important;
|
||||||
text-shadow: 0 0 2px #ffffff, 0 0 2px #ffffff;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.red {
|
.red {
|
||||||
color: #ff2d2d !important;
|
color: #ff2d2d !important;
|
||||||
text-shadow: 0 0 2px #ff2d2d, 0 0 2px #ff2d2d;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.magenta {
|
.magenta {
|
||||||
color: #ff2d95 !important;
|
color: #ff2d95 !important;
|
||||||
text-shadow: 0 0 2px #ff2d95, 0 0 2px #ff2d95;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Bitcoin Progress Bar Styles */
|
/* Bitcoin Progress Bar Styles */
|
||||||
@ -398,7 +455,6 @@ h1 {
|
|||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
margin-top: 0.3rem;
|
margin-top: 0.3rem;
|
||||||
text-shadow: 0 0 5px var(--primary-color);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
@ -113,7 +113,6 @@
|
|||||||
.metric-value {
|
.metric-value {
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 0 6px #32cd32;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Yellow color family (BTC price, sats metrics, time to payout) */
|
/* Yellow color family (BTC price, sats metrics, time to payout) */
|
||||||
@ -125,7 +124,6 @@
|
|||||||
#estimated_rewards_in_window_sats,
|
#estimated_rewards_in_window_sats,
|
||||||
#est_time_to_payout {
|
#est_time_to_payout {
|
||||||
color: #ffd700;
|
color: #ffd700;
|
||||||
text-shadow: 0 0 6px rgba(255, 215, 0, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Green color family (profits, earnings) */
|
/* Green color family (profits, earnings) */
|
||||||
@ -134,13 +132,11 @@
|
|||||||
#daily_profit_usd,
|
#daily_profit_usd,
|
||||||
#monthly_profit_usd {
|
#monthly_profit_usd {
|
||||||
color: #32CD32;
|
color: #32CD32;
|
||||||
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Red color family (costs) */
|
/* Red color family (costs) */
|
||||||
#daily_power_cost {
|
#daily_power_cost {
|
||||||
color: #ff5555 !important;
|
color: #ff5555 !important;
|
||||||
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* White metrics (general stats) */
|
/* White metrics (general stats) */
|
||||||
@ -154,19 +150,16 @@
|
|||||||
#last_block_height,
|
#last_block_height,
|
||||||
#pool_fees_percentage {
|
#pool_fees_percentage {
|
||||||
color: #ffffff;
|
color: #ffffff;
|
||||||
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Blue metrics (time data) */
|
/* Blue metrics (time data) */
|
||||||
#last_block_time {
|
#last_block_time {
|
||||||
color: #00dfff;
|
color: #00dfff;
|
||||||
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body strong {
|
.card-body strong {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
margin-right: 0.25rem;
|
margin-right: 0.25rem;
|
||||||
text-shadow: 0 0 2px var(--primary-color);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-body p {
|
.card-body p {
|
||||||
@ -225,14 +218,30 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.datum-label {
|
.datum-label {
|
||||||
color: #ff9d00; /* Orange color */
|
color: #ffffff; /* White color */
|
||||||
font-size: 0.85em;
|
font-size: 0.95em;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
padding: 2px 5px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.2);
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
letter-spacing: 0.5px;
|
letter-spacing: 2px;
|
||||||
vertical-align: middle;
|
}
|
||||||
}
|
|
||||||
|
/* Pool luck indicators */
|
||||||
|
.very-lucky {
|
||||||
|
color: #32CD32 !important;
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lucky {
|
||||||
|
color: #90EE90 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.normal-luck {
|
||||||
|
color: #ffd700 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unlucky {
|
||||||
|
color: #ff5555 !important;
|
||||||
|
}
|
||||||
|
@ -39,7 +39,6 @@ body {
|
|||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
padding-top: 50px;
|
padding-top: 50px;
|
||||||
font-family: var(--terminal-font);
|
font-family: var(--terminal-font);
|
||||||
text-shadow: 0 0 5px rgba(255, 255, 255, 0.3);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a.btn-primary {
|
a.btn-primary {
|
||||||
@ -97,7 +96,6 @@ h1 {
|
|||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
font-family: var(--header-font);
|
font-family: var(--header-font);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 0 10px var(--primary-color);
|
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
}
|
}
|
||||||
@ -108,7 +106,6 @@ p {
|
|||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
color: #ff5555;
|
color: #ff5555;
|
||||||
text-shadow: 0 0 8px rgba(255, 85, 85, 0.6);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cursor blink for terminal feel */
|
/* Cursor blink for terminal feel */
|
||||||
@ -133,6 +130,5 @@ p {
|
|||||||
font-family: var(--terminal-font);
|
font-family: var(--terminal-font);
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
color: #00dfff;
|
color: #00dfff;
|
||||||
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
|
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
@ -70,7 +70,6 @@ body {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1rem; /* Match card header font size */
|
font-size: 1.1rem; /* Match card header font size */
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
text-shadow: 0 0 5px var(--primary-color);
|
|
||||||
animation: flicker 4s infinite; /* Add flicker animation from card headers */
|
animation: flicker 4s infinite; /* Add flicker animation from card headers */
|
||||||
font-family: var(--header-font); /* Use the same font variable */
|
font-family: var(--header-font); /* Use the same font variable */
|
||||||
padding: 0.3rem 0; /* Match card header padding */
|
padding: 0.3rem 0; /* Match card header padding */
|
||||||
@ -232,7 +231,6 @@ body {
|
|||||||
#retro-terminal-bar #progress-text {
|
#retro-terminal-bar #progress-text {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--terminal-text);
|
color: var(--terminal-text);
|
||||||
text-shadow: 0 0 5px var(--terminal-text);
|
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -242,7 +240,6 @@ body {
|
|||||||
#retro-terminal-bar #uptimeTimer {
|
#retro-terminal-bar #uptimeTimer {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: var(--terminal-text);
|
color: var(--terminal-text);
|
||||||
text-shadow: 0 0 5px var(--terminal-text);
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: relative;
|
position: relative;
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
|
209
static/css/theme-toggle.css
Normal file
209
static/css/theme-toggle.css
Normal file
@ -0,0 +1,209 @@
|
|||||||
|
/* 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;
|
||||||
|
}
|
||||||
|
}
|
@ -95,7 +95,6 @@
|
|||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
text-shadow: 0 0 5px var(--primary-color);
|
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@ -157,14 +156,12 @@
|
|||||||
background-color: rgba(50, 205, 50, 0.2);
|
background-color: rgba(50, 205, 50, 0.2);
|
||||||
border: 1px solid #32CD32;
|
border: 1px solid #32CD32;
|
||||||
color: #32CD32;
|
color: #32CD32;
|
||||||
text-shadow: 0 0 5px rgba(50, 205, 50, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-badge-offline {
|
.status-badge-offline {
|
||||||
background-color: rgba(255, 85, 85, 0.2);
|
background-color: rgba(255, 85, 85, 0.2);
|
||||||
border: 1px solid #ff5555;
|
border: 1px solid #ff5555;
|
||||||
color: #ff5555;
|
color: #ff5555;
|
||||||
text-shadow: 0 0 5px rgba(255, 85, 85, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stats bars */
|
/* Stats bars */
|
||||||
|
@ -2,11 +2,40 @@
|
|||||||
* BitcoinMinuteRefresh.js - Simplified Bitcoin-themed floating uptime monitor
|
* BitcoinMinuteRefresh.js - Simplified Bitcoin-themed floating uptime monitor
|
||||||
*
|
*
|
||||||
* This module creates a Bitcoin-themed terminal that shows server uptime.
|
* This module creates a Bitcoin-themed terminal that shows server uptime.
|
||||||
|
* Now includes DeepSea theme support.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const BitcoinMinuteRefresh = (function () {
|
const BitcoinMinuteRefresh = (function () {
|
||||||
// Constants
|
// Constants
|
||||||
const STORAGE_KEY = 'bitcoin_last_refresh_time'; // For cross-page sync
|
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'
|
||||||
|
};
|
||||||
|
|
||||||
// Private variables
|
// Private variables
|
||||||
let terminalElement = null;
|
let terminalElement = null;
|
||||||
@ -16,18 +45,141 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
let uptimeInterval = null;
|
let uptimeInterval = null;
|
||||||
let isInitialized = false;
|
let isInitialized = false;
|
||||||
let refreshCallback = null;
|
let refreshCallback = null;
|
||||||
|
let currentThemeColor = BITCOIN_COLOR; // Default Bitcoin color
|
||||||
|
let dragListenersAdded = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logging helper function
|
||||||
|
* @param {string} message - Message to log
|
||||||
|
* @param {string} level - Log level (log, warn, error)
|
||||||
|
*/
|
||||||
|
function log(message, level = 'log') {
|
||||||
|
const prefix = "BitcoinMinuteRefresh: ";
|
||||||
|
if (level === 'error') {
|
||||||
|
console.error(prefix + message);
|
||||||
|
} else if (level === 'warn') {
|
||||||
|
console.warn(prefix + message);
|
||||||
|
} else {
|
||||||
|
console.log(prefix + message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to set multiple styles on an element
|
||||||
|
* @param {Element} element - The DOM element to style
|
||||||
|
* @param {Object} styles - Object with style properties
|
||||||
|
*/
|
||||||
|
function applyStyles(element, styles) {
|
||||||
|
Object.keys(styles).forEach(key => {
|
||||||
|
element.style[key] = styles[key];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the current theme color
|
||||||
|
*/
|
||||||
|
function applyThemeColor() {
|
||||||
|
// Check if theme toggle is set to DeepSea
|
||||||
|
const isDeepSeaTheme = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
|
||||||
|
currentThemeColor = isDeepSeaTheme ? DEEPSEA_COLOR : BITCOIN_COLOR;
|
||||||
|
|
||||||
|
// Don't try to update DOM elements if they don't exist yet
|
||||||
|
if (!terminalElement) return;
|
||||||
|
|
||||||
|
// Define color values based on theme
|
||||||
|
const rgbValues = isDeepSeaTheme ? '0, 136, 204' : '247, 147, 26';
|
||||||
|
|
||||||
|
// Create theme config
|
||||||
|
const themeConfig = {
|
||||||
|
color: currentThemeColor,
|
||||||
|
borderColor: currentThemeColor,
|
||||||
|
boxShadow: `0 0 5px rgba(${rgbValues}, 0.3)`,
|
||||||
|
textShadow: `0 0 5px rgba(${rgbValues}, 0.8)`,
|
||||||
|
borderColorRGBA: `rgba(${rgbValues}, 0.5)`,
|
||||||
|
textShadowStrong: `0 0 8px rgba(${rgbValues}, 0.8)`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply styles to terminal
|
||||||
|
applyStyles(terminalElement, {
|
||||||
|
borderColor: themeConfig.color,
|
||||||
|
color: themeConfig.color,
|
||||||
|
boxShadow: themeConfig.boxShadow
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update header border
|
||||||
|
const headerElement = terminalElement.querySelector(SELECTORS.HEADER);
|
||||||
|
if (headerElement) {
|
||||||
|
headerElement.style.borderColor = themeConfig.color;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update terminal title
|
||||||
|
const titleElement = terminalElement.querySelector(SELECTORS.TITLE);
|
||||||
|
if (titleElement) {
|
||||||
|
applyStyles(titleElement, {
|
||||||
|
color: themeConfig.color,
|
||||||
|
textShadow: themeConfig.textShadow
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime timer border
|
||||||
|
const uptimeTimer = terminalElement.querySelector(SELECTORS.TIMER);
|
||||||
|
if (uptimeTimer) {
|
||||||
|
uptimeTimer.style.borderColor = themeConfig.borderColorRGBA;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update uptime separators
|
||||||
|
const separators = terminalElement.querySelectorAll(SELECTORS.SEPARATORS);
|
||||||
|
separators.forEach(sep => {
|
||||||
|
sep.style.textShadow = themeConfig.textShadowStrong;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update uptime title
|
||||||
|
const uptimeTitle = terminalElement.querySelector(SELECTORS.UPTIME_TITLE);
|
||||||
|
if (uptimeTitle) {
|
||||||
|
uptimeTitle.style.textShadow = themeConfig.textShadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update minimized view
|
||||||
|
const miniLabel = terminalElement.querySelector(SELECTORS.MINI_LABEL);
|
||||||
|
if (miniLabel) {
|
||||||
|
miniLabel.style.color = themeConfig.color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for theme changes
|
||||||
|
*/
|
||||||
|
function setupThemeChangeListener() {
|
||||||
|
// Listen for theme change events from localStorage
|
||||||
|
window.addEventListener('storage', function (e) {
|
||||||
|
if (e.key === STORAGE_KEYS.THEME) {
|
||||||
|
applyThemeColor();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Debounce function to limit execution frequency
|
||||||
|
*/
|
||||||
|
function debounce(func, wait) {
|
||||||
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add dragging functionality to the terminal
|
* Add dragging functionality to the terminal
|
||||||
*/
|
*/
|
||||||
function addDraggingBehavior() {
|
function addDraggingBehavior() {
|
||||||
// Find the terminal element
|
// Find the terminal element
|
||||||
const terminal = document.getElementById('bitcoin-terminal') ||
|
const terminal = document.getElementById(DOM_IDS.TERMINAL) ||
|
||||||
document.querySelector('.bitcoin-terminal') ||
|
document.querySelector('.bitcoin-terminal') ||
|
||||||
document.getElementById('retro-terminal-bar');
|
document.getElementById('retro-terminal-bar');
|
||||||
|
|
||||||
if (!terminal) {
|
if (!terminal) {
|
||||||
console.warn('Terminal element not found for drag behavior');
|
log('Terminal element not found for drag behavior', 'warn');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +193,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
if (window.innerWidth < 768) return;
|
if (window.innerWidth < 768) return;
|
||||||
|
|
||||||
// Don't handle drag if clicking on controls
|
// Don't handle drag if clicking on controls
|
||||||
if (e.target.closest('.terminal-dot')) return;
|
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
|
||||||
|
|
||||||
isDragging = true;
|
isDragging = true;
|
||||||
terminal.classList.add('dragging');
|
terminal.classList.add('dragging');
|
||||||
@ -63,8 +215,8 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
e.preventDefault(); // Prevent text selection
|
e.preventDefault(); // Prevent text selection
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to handle mouse move (dragging)
|
// Function to handle mouse move (dragging) with debounce for better performance
|
||||||
function handleMouseMove(e) {
|
const handleMouseMove = debounce(function (e) {
|
||||||
if (!isDragging) return;
|
if (!isDragging) return;
|
||||||
|
|
||||||
// Calculate the horizontal movement - vertical stays fixed
|
// Calculate the horizontal movement - vertical stays fixed
|
||||||
@ -79,7 +231,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
terminal.style.left = newLeft + 'px';
|
terminal.style.left = newLeft + 'px';
|
||||||
terminal.style.right = 'auto'; // Remove right positioning
|
terminal.style.right = 'auto'; // Remove right positioning
|
||||||
terminal.style.transform = 'none'; // Remove transformations
|
terminal.style.transform = 'none'; // Remove transformations
|
||||||
}
|
}, 10);
|
||||||
|
|
||||||
// Function to handle mouse up (drag end)
|
// Function to handle mouse up (drag end)
|
||||||
function handleMouseUp() {
|
function handleMouseUp() {
|
||||||
@ -90,7 +242,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the terminal header for dragging
|
// Find the terminal header for dragging
|
||||||
const terminalHeader = terminal.querySelector('.terminal-header');
|
const terminalHeader = terminal.querySelector(SELECTORS.HEADER);
|
||||||
if (terminalHeader) {
|
if (terminalHeader) {
|
||||||
terminalHeader.addEventListener('mousedown', handleMouseDown);
|
terminalHeader.addEventListener('mousedown', handleMouseDown);
|
||||||
} else {
|
} else {
|
||||||
@ -98,27 +250,88 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
terminal.addEventListener('mousedown', handleMouseDown);
|
terminal.addEventListener('mousedown', handleMouseDown);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add mousemove and mouseup listeners to document
|
// Add touch support for mobile/tablet
|
||||||
document.addEventListener('mousemove', handleMouseMove);
|
function handleTouchStart(e) {
|
||||||
document.addEventListener('mouseup', handleMouseUp);
|
if (window.innerWidth < 768) return;
|
||||||
|
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
|
||||||
|
|
||||||
// Handle window resize to keep terminal visible
|
const touch = e.touches[0];
|
||||||
window.addEventListener('resize', function () {
|
isDragging = true;
|
||||||
if (window.innerWidth < 768) {
|
terminal.classList.add('dragging');
|
||||||
// Reset position for mobile view
|
|
||||||
terminal.style.left = '50%';
|
startX = touch.clientX;
|
||||||
terminal.style.right = 'auto';
|
|
||||||
terminal.style.transform = 'translateX(-50%)';
|
const style = window.getComputedStyle(terminal);
|
||||||
|
if (style.left !== 'auto') {
|
||||||
|
startLeft = parseInt(style.left) || 0;
|
||||||
} else {
|
} else {
|
||||||
// Ensure terminal stays visible in desktop view
|
startLeft = window.innerWidth - (parseInt(style.right) || 0) - terminal.offsetWidth;
|
||||||
const maxLeft = window.innerWidth - terminal.offsetWidth;
|
|
||||||
const currentLeft = parseInt(window.getComputedStyle(terminal).left) || 0;
|
|
||||||
|
|
||||||
if (currentLeft > maxLeft) {
|
|
||||||
terminal.style.left = maxLeft + 'px';
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
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';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mark listeners as added
|
||||||
|
dragListenersAdded = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -127,7 +340,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
function createTerminalElement() {
|
function createTerminalElement() {
|
||||||
// Container element
|
// Container element
|
||||||
terminalElement = document.createElement('div');
|
terminalElement = document.createElement('div');
|
||||||
terminalElement.id = 'bitcoin-terminal';
|
terminalElement.id = DOM_IDS.TERMINAL;
|
||||||
terminalElement.className = 'bitcoin-terminal';
|
terminalElement.className = 'bitcoin-terminal';
|
||||||
|
|
||||||
// Terminal content - simplified for uptime-only
|
// Terminal content - simplified for uptime-only
|
||||||
@ -145,23 +358,23 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
<div class="status-dot connected"></div>
|
<div class="status-dot connected"></div>
|
||||||
<span>LIVE</span>
|
<span>LIVE</span>
|
||||||
</div>
|
</div>
|
||||||
<span id="terminal-clock" class="terminal-clock">00:00:00</span>
|
<span id="${DOM_IDS.CLOCK}" class="terminal-clock">00:00:00</span>
|
||||||
</div>
|
</div>
|
||||||
<div id="uptime-timer" class="uptime-timer">
|
<div id="uptime-timer" class="uptime-timer">
|
||||||
<div class="uptime-title">UPTIME</div>
|
<div class="uptime-title">UPTIME</div>
|
||||||
<div class="uptime-display">
|
<div class="uptime-display">
|
||||||
<div class="uptime-value">
|
<div class="uptime-value">
|
||||||
<span id="uptime-hours" class="uptime-number">00</span>
|
<span id="${DOM_IDS.UPTIME_HOURS}" class="uptime-number">00</span>
|
||||||
<span class="uptime-label">H</span>
|
<span class="uptime-label">H</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="uptime-separator">:</div>
|
<div class="uptime-separator">:</div>
|
||||||
<div class="uptime-value">
|
<div class="uptime-value">
|
||||||
<span id="uptime-minutes" class="uptime-number">00</span>
|
<span id="${DOM_IDS.UPTIME_MINUTES}" class="uptime-number">00</span>
|
||||||
<span class="uptime-label">M</span>
|
<span class="uptime-label">M</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="uptime-separator">:</div>
|
<div class="uptime-separator">:</div>
|
||||||
<div class="uptime-value">
|
<div class="uptime-value">
|
||||||
<span id="uptime-seconds" class="uptime-number">00</span>
|
<span id="${DOM_IDS.UPTIME_SECONDS}" class="uptime-number">00</span>
|
||||||
<span class="uptime-label">S</span>
|
<span class="uptime-label">S</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -170,7 +383,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
<div class="terminal-minimized">
|
<div class="terminal-minimized">
|
||||||
<div class="minimized-uptime">
|
<div class="minimized-uptime">
|
||||||
<span class="mini-uptime-label">UPTIME</span>
|
<span class="mini-uptime-label">UPTIME</span>
|
||||||
<span id="minimized-uptime-value">00:00:00</span>
|
<span id="${DOM_IDS.MINIMIZED_UPTIME}">00:00:00</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="minimized-status-dot connected"></div>
|
<div class="minimized-status-dot connected"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -186,12 +399,12 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
uptimeElement = document.getElementById('uptime-timer');
|
uptimeElement = document.getElementById('uptime-timer');
|
||||||
|
|
||||||
// Check if terminal was previously collapsed
|
// Check if terminal was previously collapsed
|
||||||
if (localStorage.getItem('bitcoin_terminal_collapsed') === 'true') {
|
if (localStorage.getItem(STORAGE_KEYS.COLLAPSED) === 'true') {
|
||||||
terminalElement.classList.add('collapsed');
|
terminalElement.classList.add('collapsed');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add custom styles if not already present
|
// Add custom styles if not already present
|
||||||
if (!document.getElementById('bitcoin-terminal-styles')) {
|
if (!document.getElementById(DOM_IDS.STYLES)) {
|
||||||
addStyles();
|
addStyles();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -200,8 +413,13 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
* Add CSS styles for the terminal
|
* Add CSS styles for the terminal
|
||||||
*/
|
*/
|
||||||
function addStyles() {
|
function addStyles() {
|
||||||
|
// Use the currentThemeColor variable instead of hardcoded colors
|
||||||
const styleElement = document.createElement('style');
|
const styleElement = document.createElement('style');
|
||||||
styleElement.id = 'bitcoin-terminal-styles';
|
styleElement.id = DOM_IDS.STYLES;
|
||||||
|
|
||||||
|
// Generate RGB values for dynamic colors
|
||||||
|
const rgbValues = currentThemeColor === DEEPSEA_COLOR ? '0, 136, 204' : '247, 147, 26';
|
||||||
|
|
||||||
styleElement.textContent = `
|
styleElement.textContent = `
|
||||||
/* Terminal Container */
|
/* Terminal Container */
|
||||||
.bitcoin-terminal {
|
.bitcoin-terminal {
|
||||||
@ -210,14 +428,14 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
right: 20px;
|
right: 20px;
|
||||||
width: 230px;
|
width: 230px;
|
||||||
background-color: #000000;
|
background-color: #000000;
|
||||||
border: 1px solid #f7931a;
|
border: 1px solid ${currentThemeColor};
|
||||||
color: #f7931a;
|
color: ${currentThemeColor};
|
||||||
font-family: 'VT323', monospace;
|
font-family: 'VT323', monospace;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
transition: all 0.3s ease;
|
transition: all 0.3s ease;
|
||||||
box-shadow: 0 0 5px rgba(247, 147, 26, 0.3);
|
box-shadow: 0 0 5px rgba(${rgbValues}, 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Terminal Header */
|
/* Terminal Header */
|
||||||
@ -225,10 +443,10 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-bottom: 1px solid #f7931a;
|
border-bottom: 1px solid ${currentThemeColor};
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: pointer; /* Add pointer (hand) cursor on hover */
|
cursor: grab; /* Add grab cursor on hover */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Apply grabbing cursor during active drag */
|
/* Apply grabbing cursor during active drag */
|
||||||
@ -238,10 +456,9 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.terminal-title {
|
.terminal-title {
|
||||||
color: #f7931a;
|
color: ${currentThemeColor};
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
|
||||||
animation: terminal-flicker 4s infinite;
|
animation: terminal-flicker 4s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -305,7 +522,6 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
.terminal-clock {
|
.terminal-clock {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
|
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
|
||||||
@ -315,7 +531,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
background-color: #111;
|
background-color: #111;
|
||||||
border: 1px solid rgba(247, 147, 26, 0.5);
|
border: 1px solid rgba(${rgbValues}, 0.5);
|
||||||
margin-top: 5px;
|
margin-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -342,7 +558,6 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
|
|
||||||
color: #dee2e6;
|
color: #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,7 +571,6 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
font-size: 1.4rem;
|
font-size: 1.4rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.uptime-title {
|
.uptime-title {
|
||||||
@ -364,16 +578,15 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 2px;
|
letter-spacing: 2px;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
|
|
||||||
margin-bottom: 3px;
|
margin-bottom: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Show button */
|
/* Show button */
|
||||||
#bitcoin-terminal-show {
|
#${DOM_IDS.SHOW_BUTTON} {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 10px;
|
bottom: 10px;
|
||||||
right: 10px;
|
right: 10px;
|
||||||
background-color: #f7931a;
|
background-color: ${currentThemeColor};
|
||||||
color: #000;
|
color: #000;
|
||||||
border: none;
|
border: none;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
@ -381,7 +594,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
display: none;
|
display: none;
|
||||||
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
|
box-shadow: 0 0 10px rgba(${rgbValues}, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* CRT scanline effect */
|
/* CRT scanline effect */
|
||||||
@ -447,13 +660,12 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
margin-left: 45px;
|
margin-left: 45px;
|
||||||
color: #f7931a;
|
color: ${currentThemeColor};
|
||||||
}
|
}
|
||||||
|
|
||||||
#minimized-uptime-value {
|
#${DOM_IDS.MINIMIZED_UPTIME} {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
|
|
||||||
margin-left: 45px;
|
margin-left: 45px;
|
||||||
color: #dee2e6;
|
color: #dee2e6;
|
||||||
}
|
}
|
||||||
@ -537,21 +749,25 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
function updateClock() {
|
function updateClock() {
|
||||||
try {
|
try {
|
||||||
const now = new Date(Date.now() + (serverTimeOffset || 0));
|
const now = new Date(Date.now() + (serverTimeOffset || 0));
|
||||||
let hours = now.getHours();
|
// Use the global timezone setting if available
|
||||||
const minutes = String(now.getMinutes()).padStart(2, '0');
|
const timeZone = window.dashboardTimezone || 'America/Los_Angeles';
|
||||||
const seconds = String(now.getSeconds()).padStart(2, '0');
|
|
||||||
const ampm = hours >= 12 ? 'PM' : 'AM';
|
// Format the time in the configured timezone
|
||||||
hours = hours % 12;
|
const timeString = now.toLocaleTimeString('en-US', {
|
||||||
hours = hours ? hours : 12; // the hour '0' should be '12'
|
hour: '2-digit',
|
||||||
const timeString = `${String(hours).padStart(2, '0')}:${minutes}:${seconds} ${ampm}`;
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
timeZone: timeZone
|
||||||
|
});
|
||||||
|
|
||||||
// Update clock in normal view
|
// Update clock in normal view
|
||||||
const clockElement = document.getElementById('terminal-clock');
|
const clockElement = document.getElementById(DOM_IDS.CLOCK);
|
||||||
if (clockElement) {
|
if (clockElement) {
|
||||||
clockElement.textContent = timeString;
|
clockElement.textContent = timeString;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("BitcoinMinuteRefresh: Error updating clock:", e);
|
log("Error updating clock: " + e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -569,40 +785,67 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
|
||||||
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
|
||||||
|
|
||||||
// Update the main uptime display with digital clock style
|
// Format numbers with leading zeros
|
||||||
const uptimeHoursElement = document.getElementById('uptime-hours');
|
const formattedTime = {
|
||||||
const uptimeMinutesElement = document.getElementById('uptime-minutes');
|
hours: String(hours).padStart(2, '0'),
|
||||||
const uptimeSecondsElement = document.getElementById('uptime-seconds');
|
minutes: String(minutes).padStart(2, '0'),
|
||||||
|
seconds: String(seconds).padStart(2, '0')
|
||||||
|
};
|
||||||
|
|
||||||
if (uptimeHoursElement) {
|
// Update the main uptime display with digital clock style
|
||||||
uptimeHoursElement.textContent = String(hours).padStart(2, '0');
|
const elements = {
|
||||||
}
|
hours: document.getElementById(DOM_IDS.UPTIME_HOURS),
|
||||||
if (uptimeMinutesElement) {
|
minutes: document.getElementById(DOM_IDS.UPTIME_MINUTES),
|
||||||
uptimeMinutesElement.textContent = String(minutes).padStart(2, '0');
|
seconds: document.getElementById(DOM_IDS.UPTIME_SECONDS),
|
||||||
}
|
minimized: document.getElementById(DOM_IDS.MINIMIZED_UPTIME)
|
||||||
if (uptimeSecondsElement) {
|
};
|
||||||
uptimeSecondsElement.textContent = String(seconds).padStart(2, '0');
|
|
||||||
}
|
// 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;
|
||||||
|
|
||||||
// Update the minimized uptime display
|
// Update the minimized uptime display
|
||||||
const minimizedUptimeElement = document.getElementById('minimized-uptime-value');
|
if (elements.minimized) {
|
||||||
if (minimizedUptimeElement) {
|
elements.minimized.textContent = `${formattedTime.hours}:${formattedTime.minutes}:${formattedTime.seconds}`;
|
||||||
minimizedUptimeElement.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("BitcoinMinuteRefresh: Error updating uptime:", e);
|
log("Error updating uptime: " + e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start animation frame loop for smooth updates
|
||||||
|
*/
|
||||||
|
function startAnimationLoop() {
|
||||||
|
let lastUpdate = 0;
|
||||||
|
const updateInterval = 1000; // Update every second
|
||||||
|
|
||||||
|
function animationFrame(timestamp) {
|
||||||
|
// Only update once per second to save resources
|
||||||
|
if (timestamp - lastUpdate >= updateInterval) {
|
||||||
|
updateClock();
|
||||||
|
updateUptime();
|
||||||
|
lastUpdate = timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Continue the animation loop
|
||||||
|
requestAnimationFrame(animationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the loop
|
||||||
|
requestAnimationFrame(animationFrame);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify other tabs that data has been refreshed
|
* Notify other tabs that data has been refreshed
|
||||||
*/
|
*/
|
||||||
function notifyRefresh() {
|
function notifyRefresh() {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
localStorage.setItem(STORAGE_KEY, now.toString());
|
localStorage.setItem(STORAGE_KEY, now.toString());
|
||||||
localStorage.setItem('bitcoin_refresh_event', 'refresh-' + now);
|
localStorage.setItem(STORAGE_KEYS.REFRESH_EVENT, 'refresh-' + now);
|
||||||
console.log("BitcoinMinuteRefresh: Notified other tabs of refresh at " + new Date(now).toISOString());
|
log("Notified other tabs of refresh at " + new Date(now).toISOString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -612,21 +855,30 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
// Store the refresh callback
|
// Store the refresh callback
|
||||||
refreshCallback = refreshFunc;
|
refreshCallback = refreshFunc;
|
||||||
|
|
||||||
|
// Get current theme status
|
||||||
|
applyThemeColor();
|
||||||
|
|
||||||
// Create the terminal element if it doesn't exist
|
// Create the terminal element if it doesn't exist
|
||||||
if (!document.getElementById('bitcoin-terminal')) {
|
if (!document.getElementById(DOM_IDS.TERMINAL)) {
|
||||||
createTerminalElement();
|
createTerminalElement();
|
||||||
} else {
|
} else {
|
||||||
// Get references to existing elements
|
// Get references to existing elements
|
||||||
terminalElement = document.getElementById('bitcoin-terminal');
|
terminalElement = document.getElementById(DOM_IDS.TERMINAL);
|
||||||
uptimeElement = document.getElementById('uptime-timer');
|
uptimeElement = document.getElementById('uptime-timer');
|
||||||
|
|
||||||
|
// Apply theme to existing element
|
||||||
|
applyThemeColor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up listener for theme changes
|
||||||
|
setupThemeChangeListener();
|
||||||
|
|
||||||
// Try to get stored server time information
|
// Try to get stored server time information
|
||||||
try {
|
try {
|
||||||
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
|
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0');
|
||||||
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
|
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("BitcoinMinuteRefresh: Error reading server time from localStorage:", e);
|
log("Error reading server time from localStorage: " + e.message, 'error');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any existing intervals
|
// Clear any existing intervals
|
||||||
@ -634,11 +886,8 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
clearInterval(uptimeInterval);
|
clearInterval(uptimeInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set up interval for updating clock and uptime display
|
// Use requestAnimationFrame for smoother animations
|
||||||
uptimeInterval = setInterval(function () {
|
startAnimationLoop();
|
||||||
updateClock();
|
|
||||||
updateUptime();
|
|
||||||
}, 1000); // Update every second is sufficient for uptime display
|
|
||||||
|
|
||||||
// Listen for storage events to sync across tabs
|
// Listen for storage events to sync across tabs
|
||||||
window.removeEventListener('storage', handleStorageChange);
|
window.removeEventListener('storage', handleStorageChange);
|
||||||
@ -651,15 +900,15 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
// Mark as initialized
|
// Mark as initialized
|
||||||
isInitialized = true;
|
isInitialized = true;
|
||||||
|
|
||||||
console.log("BitcoinMinuteRefresh: Initialized");
|
log("Initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle storage changes for cross-tab synchronization
|
* Handle storage changes for cross-tab synchronization
|
||||||
*/
|
*/
|
||||||
function handleStorageChange(event) {
|
function handleStorageChange(event) {
|
||||||
if (event.key === 'bitcoin_refresh_event') {
|
if (event.key === STORAGE_KEYS.REFRESH_EVENT) {
|
||||||
console.log("BitcoinMinuteRefresh: Detected refresh from another tab");
|
log("Detected refresh from another tab");
|
||||||
|
|
||||||
// If another tab refreshed, consider refreshing this one too
|
// If another tab refreshed, consider refreshing this one too
|
||||||
// But don't refresh if it was just refreshed recently (5 seconds)
|
// But don't refresh if it was just refreshed recently (5 seconds)
|
||||||
@ -667,12 +916,12 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
|
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
|
||||||
refreshCallback();
|
refreshCallback();
|
||||||
}
|
}
|
||||||
} else if (event.key === 'serverTimeOffset' || event.key === 'serverStartTime') {
|
} else if (event.key === STORAGE_KEYS.SERVER_OFFSET || event.key === STORAGE_KEYS.SERVER_START) {
|
||||||
try {
|
try {
|
||||||
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
|
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0');
|
||||||
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
|
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("BitcoinMinuteRefresh: Error reading updated server time:", e);
|
log("Error reading updated server time: " + e.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -682,7 +931,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
*/
|
*/
|
||||||
function handleVisibilityChange() {
|
function handleVisibilityChange() {
|
||||||
if (!document.hidden) {
|
if (!document.hidden) {
|
||||||
console.log("BitcoinMinuteRefresh: Page became visible, updating");
|
log("Page became visible, updating");
|
||||||
|
|
||||||
// Update immediately when page becomes visible
|
// Update immediately when page becomes visible
|
||||||
updateClock();
|
updateClock();
|
||||||
@ -706,13 +955,13 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
serverStartTime = startTime;
|
serverStartTime = startTime;
|
||||||
|
|
||||||
// Store in localStorage for cross-page sharing
|
// Store in localStorage for cross-page sharing
|
||||||
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
|
localStorage.setItem(STORAGE_KEYS.SERVER_OFFSET, serverTimeOffset.toString());
|
||||||
localStorage.setItem('serverStartTime', serverStartTime.toString());
|
localStorage.setItem(STORAGE_KEYS.SERVER_START, serverStartTime.toString());
|
||||||
|
|
||||||
// Update the uptime immediately
|
// Update the uptime immediately
|
||||||
updateUptime();
|
updateUptime();
|
||||||
|
|
||||||
console.log("BitcoinMinuteRefresh: Server time updated - offset:", serverTimeOffset, "ms");
|
log("Server time updated - offset: " + serverTimeOffset + " ms");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -722,7 +971,7 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
if (!terminalElement) return;
|
if (!terminalElement) return;
|
||||||
|
|
||||||
terminalElement.classList.toggle('collapsed');
|
terminalElement.classList.toggle('collapsed');
|
||||||
localStorage.setItem('bitcoin_terminal_collapsed', terminalElement.classList.contains('collapsed'));
|
localStorage.setItem(STORAGE_KEYS.COLLAPSED, terminalElement.classList.contains('collapsed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -734,15 +983,15 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
terminalElement.style.display = 'none';
|
terminalElement.style.display = 'none';
|
||||||
|
|
||||||
// Create show button if it doesn't exist
|
// Create show button if it doesn't exist
|
||||||
if (!document.getElementById('bitcoin-terminal-show')) {
|
if (!document.getElementById(DOM_IDS.SHOW_BUTTON)) {
|
||||||
const showButton = document.createElement('button');
|
const showButton = document.createElement('button');
|
||||||
showButton.id = 'bitcoin-terminal-show';
|
showButton.id = DOM_IDS.SHOW_BUTTON;
|
||||||
showButton.textContent = 'Show Monitor';
|
showButton.textContent = 'Show Monitor';
|
||||||
showButton.onclick = showTerminal;
|
showButton.onclick = showTerminal;
|
||||||
document.body.appendChild(showButton);
|
document.body.appendChild(showButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('bitcoin-terminal-show').style.display = 'block';
|
document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -752,7 +1001,10 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
if (!terminalElement) return;
|
if (!terminalElement) return;
|
||||||
|
|
||||||
terminalElement.style.display = 'block';
|
terminalElement.style.display = 'block';
|
||||||
document.getElementById('bitcoin-terminal-show').style.display = 'none';
|
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON);
|
||||||
|
if (showButton) {
|
||||||
|
showButton.style.display = 'none';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
@ -762,11 +1014,12 @@ const BitcoinMinuteRefresh = (function () {
|
|||||||
updateServerTime: updateServerTime,
|
updateServerTime: updateServerTime,
|
||||||
toggleTerminal: toggleTerminal,
|
toggleTerminal: toggleTerminal,
|
||||||
hideTerminal: hideTerminal,
|
hideTerminal: hideTerminal,
|
||||||
showTerminal: showTerminal
|
showTerminal: showTerminal,
|
||||||
|
updateTheme: applyThemeColor
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// Auto-initialize when document is ready if a refresh function is available in the global scope
|
// Auto-initialize when document is ready
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
// Check if manualRefresh function exists in global scope
|
// Check if manualRefresh function exists in global scope
|
||||||
if (typeof window.manualRefresh === 'function') {
|
if (typeof window.manualRefresh === 'function') {
|
||||||
@ -774,4 +1027,7 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||||||
} else {
|
} else {
|
||||||
console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually");
|
console.log("BitcoinMinuteRefresh: No refresh function found, will need to be initialized manually");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update theme based on current setting
|
||||||
|
setTimeout(() => BitcoinMinuteRefresh.updateTheme(), 100);
|
||||||
});
|
});
|
||||||
|
@ -1,58 +1,157 @@
|
|||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
// Constants for configuration
|
||||||
|
const REFRESH_INTERVAL = 60000; // 60 seconds
|
||||||
|
const TOAST_DISPLAY_TIME = 3000; // 3 seconds
|
||||||
|
const DEFAULT_TIMEZONE = 'America/Los_Angeles';
|
||||||
|
const SATOSHIS_PER_BTC = 100000000;
|
||||||
|
const MAX_CACHE_SIZE = 20; // Number of block heights to cache
|
||||||
|
|
||||||
|
// POOL configuration
|
||||||
|
const POOL_CONFIG = {
|
||||||
|
oceanPools: ['ocean', 'oceanpool', 'oceanxyz', 'ocean.xyz'],
|
||||||
|
oceanColor: '#00ffff',
|
||||||
|
defaultUnknownColor: '#999999'
|
||||||
|
};
|
||||||
|
|
||||||
// Global variables
|
// Global variables
|
||||||
let currentStartHeight = null;
|
let currentStartHeight = null;
|
||||||
const mempoolBaseUrl = "https://mempool.space";
|
const mempoolBaseUrl = "https://mempool.guide"; // Switched from mempool.space to mempool.guide - more aligned with Ocean.xyz ethos
|
||||||
let blocksCache = {};
|
let blocksCache = {};
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
// DOM ready initialization
|
// Helper function for debouncing
|
||||||
$(document).ready(function() {
|
function debounce(func, wait) {
|
||||||
console.log("Blocks page initialized");
|
let timeout;
|
||||||
|
return function (...args) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Initialize notification badge
|
// Helper function to validate block height
|
||||||
initNotificationBadge();
|
function isValidBlockHeight(height) {
|
||||||
|
height = parseInt(height);
|
||||||
// Load the latest blocks on page load
|
if (isNaN(height) || height < 0) {
|
||||||
loadLatestBlocks();
|
showToast("Please enter a valid block height");
|
||||||
|
return false;
|
||||||
// Set up event listeners
|
}
|
||||||
$("#load-blocks").on("click", function() {
|
return height;
|
||||||
const height = $("#block-height").val();
|
}
|
||||||
if (height && !isNaN(height)) {
|
|
||||||
loadBlocksFromHeight(height);
|
// Helper function to add items to cache with size management
|
||||||
} else {
|
function addToCache(height, data) {
|
||||||
showToast("Please enter a valid block height");
|
blocksCache[height] = data;
|
||||||
}
|
|
||||||
});
|
// Remove oldest entries if cache exceeds maximum size
|
||||||
|
const cacheKeys = Object.keys(blocksCache).map(Number).sort((a, b) => a - b);
|
||||||
$("#latest-blocks").on("click", loadLatestBlocks);
|
if (cacheKeys.length > MAX_CACHE_SIZE) {
|
||||||
|
const keysToRemove = cacheKeys.slice(0, cacheKeys.length - MAX_CACHE_SIZE);
|
||||||
// Handle Enter key on the block height input
|
keysToRemove.forEach(key => delete blocksCache[key]);
|
||||||
$("#block-height").on("keypress", function(e) {
|
}
|
||||||
if (e.which === 13) {
|
}
|
||||||
const height = $(this).val();
|
|
||||||
if (height && !isNaN(height)) {
|
// Clean up event handlers when refreshing or navigating
|
||||||
loadBlocksFromHeight(height);
|
function cleanupEventHandlers() {
|
||||||
} else {
|
$(window).off("click.blockModal");
|
||||||
showToast("Please enter a valid block height");
|
$(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 () {
|
||||||
|
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) {
|
||||||
|
loadBlocksFromHeight(height);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$("#latest-blocks").on("click", loadLatestBlocks);
|
||||||
|
|
||||||
|
// Handle Enter key on the block height input with debouncing
|
||||||
|
$("#block-height").on("keypress", debounce(function (e) {
|
||||||
|
if (e.which === 13) {
|
||||||
|
const height = isValidBlockHeight($(this).val());
|
||||||
|
if (height !== false) {
|
||||||
|
loadBlocksFromHeight(height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 300));
|
||||||
|
|
||||||
// Close the modal when clicking the X or outside the modal
|
// Close the modal when clicking the X or outside the modal
|
||||||
$(".block-modal-close").on("click", closeModal);
|
$(".block-modal-close").on("click", closeModal);
|
||||||
$(window).on("click", function(event) {
|
$(window).on("click.blockModal", function (event) {
|
||||||
if ($(event.target).hasClass("block-modal")) {
|
if ($(event.target).hasClass("block-modal")) {
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Initialize BitcoinMinuteRefresh if available
|
// Initialize BitcoinMinuteRefresh if available
|
||||||
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
|
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
|
||||||
BitcoinMinuteRefresh.initialize(loadLatestBlocks);
|
BitcoinMinuteRefresh.initialize(loadLatestBlocks);
|
||||||
console.log("BitcoinMinuteRefresh initialized with refresh function");
|
console.log("BitcoinMinuteRefresh initialized with refresh function");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Setup keyboard navigation for modals
|
||||||
|
setupModalKeyboardNavigation();
|
||||||
|
|
||||||
|
// Cleanup before unload
|
||||||
|
$(window).on('beforeunload', cleanupEventHandlers);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update unread notifications badge in navigation
|
// Update unread notifications badge in navigation
|
||||||
@ -79,8 +178,9 @@ function initNotificationBadge() {
|
|||||||
updateNotificationBadge();
|
updateNotificationBadge();
|
||||||
|
|
||||||
// Update every 60 seconds
|
// Update every 60 seconds
|
||||||
setInterval(updateNotificationBadge, 60000);
|
setInterval(updateNotificationBadge, REFRESH_INTERVAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper function to format timestamps as readable dates
|
// Helper function to format timestamps as readable dates
|
||||||
function formatTimestamp(timestamp) {
|
function formatTimestamp(timestamp) {
|
||||||
const date = new Date(timestamp * 1000);
|
const date = new Date(timestamp * 1000);
|
||||||
@ -91,7 +191,8 @@ function formatTimestamp(timestamp) {
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: true
|
hour12: true,
|
||||||
|
timeZone: window.dashboardTimezone || DEFAULT_TIMEZONE // Use global timezone setting
|
||||||
};
|
};
|
||||||
return date.toLocaleString('en-US', options);
|
return date.toLocaleString('en-US', options);
|
||||||
}
|
}
|
||||||
@ -109,6 +210,40 @@ function formatFileSize(bytes) {
|
|||||||
else return (bytes / 1048576).toFixed(2) + " MB";
|
else return (bytes / 1048576).toFixed(2) + " MB";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to create common info items
|
||||||
|
function createInfoItem(label, value, valueClass = '') {
|
||||||
|
const item = $("<div>", { class: "block-info-item" });
|
||||||
|
|
||||||
|
item.append($("<div>", {
|
||||||
|
class: "block-info-label",
|
||||||
|
text: label
|
||||||
|
}));
|
||||||
|
|
||||||
|
item.append($("<div>", {
|
||||||
|
class: `block-info-value ${valueClass}`,
|
||||||
|
text: value
|
||||||
|
}));
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function for creating detail items
|
||||||
|
function createDetailItem(label, value, valueClass = '') {
|
||||||
|
const item = $("<div>", { class: "block-detail-item" });
|
||||||
|
|
||||||
|
item.append($("<div>", {
|
||||||
|
class: "block-detail-label",
|
||||||
|
text: label
|
||||||
|
}));
|
||||||
|
|
||||||
|
item.append($("<div>", {
|
||||||
|
class: `block-detail-value ${valueClass}`,
|
||||||
|
text: value
|
||||||
|
}));
|
||||||
|
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper function to show toast messages
|
// Helper function to show toast messages
|
||||||
function showToast(message) {
|
function showToast(message) {
|
||||||
// Check if we already have a toast container
|
// Check if we already have a toast container
|
||||||
@ -125,7 +260,7 @@ function showToast(message) {
|
|||||||
}
|
}
|
||||||
}).appendTo("body");
|
}).appendTo("body");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a new toast
|
// Create a new toast
|
||||||
const toast = $("<div>", {
|
const toast = $("<div>", {
|
||||||
class: "toast",
|
class: "toast",
|
||||||
@ -142,16 +277,16 @@ function showToast(message) {
|
|||||||
transition: "opacity 0.3s ease"
|
transition: "opacity 0.3s ease"
|
||||||
}
|
}
|
||||||
}).appendTo(toastContainer);
|
}).appendTo(toastContainer);
|
||||||
|
|
||||||
// Show the toast
|
// Show the toast
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.css("opacity", 1);
|
toast.css("opacity", 1);
|
||||||
|
|
||||||
// Hide and remove the toast after 3 seconds
|
// Hide and remove the toast after the configured time
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
toast.css("opacity", 0);
|
toast.css("opacity", 0);
|
||||||
setTimeout(() => toast.remove(), 300);
|
setTimeout(() => toast.remove(), 300);
|
||||||
}, 3000);
|
}, TOAST_DISPLAY_TIME);
|
||||||
}, 100);
|
}, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -163,10 +298,10 @@ function getPoolColor(poolName) {
|
|||||||
// Define color mappings for common mining pools with Ocean pool featured prominently
|
// Define color mappings for common mining pools with Ocean pool featured prominently
|
||||||
const poolColors = {
|
const poolColors = {
|
||||||
// OCEAN pool with a distinctive bright cyan color for prominence
|
// OCEAN pool with a distinctive bright cyan color for prominence
|
||||||
'ocean': '#00ffff', // Bright Cyan for Ocean
|
'ocean': POOL_CONFIG.oceanColor,
|
||||||
'oceanpool': '#00ffff', // Bright Cyan for Ocean
|
'oceanpool': POOL_CONFIG.oceanColor,
|
||||||
'oceanxyz': '#00ffff', // Bright Cyan for Ocean
|
'oceanxyz': POOL_CONFIG.oceanColor,
|
||||||
'ocean.xyz': '#00ffff', // Bright Cyan for Ocean
|
'ocean.xyz': POOL_CONFIG.oceanColor,
|
||||||
|
|
||||||
// Other common mining pools with more muted colors
|
// Other common mining pools with more muted colors
|
||||||
'f2pool': '#1a9eff', // Blue
|
'f2pool': '#1a9eff', // Blue
|
||||||
@ -181,7 +316,7 @@ function getPoolColor(poolName) {
|
|||||||
'sbicrypto': '#cc9933', // Bronze
|
'sbicrypto': '#cc9933', // Bronze
|
||||||
'mara': '#8844cc', // Violet
|
'mara': '#8844cc', // Violet
|
||||||
'ultimuspool': '#09c7be', // Teal
|
'ultimuspool': '#09c7be', // Teal
|
||||||
'unknown': '#999999' // Grey for unknown pools
|
'unknown': POOL_CONFIG.defaultUnknownColor // Grey for unknown pools
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check for partial matches in pool names (for variations like "F2Pool" vs "F2pool.com")
|
// Check for partial matches in pool names (for variations like "F2Pool" vs "F2pool.com")
|
||||||
@ -203,6 +338,12 @@ function getPoolColor(poolName) {
|
|||||||
return `hsl(${hue}, 70%, 60%)`;
|
return `hsl(${hue}, 70%, 60%)`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to check if a pool is an Ocean pool
|
||||||
|
function isOceanPool(poolName) {
|
||||||
|
const normalizedName = poolName.toLowerCase();
|
||||||
|
return POOL_CONFIG.oceanPools.some(name => normalizedName.includes(name));
|
||||||
|
}
|
||||||
|
|
||||||
// Function to create a block card
|
// Function to create a block card
|
||||||
function createBlockCard(block) {
|
function createBlockCard(block) {
|
||||||
const timestamp = formatTimestamp(block.timestamp);
|
const timestamp = formatTimestamp(block.timestamp);
|
||||||
@ -216,20 +357,23 @@ function createBlockCard(block) {
|
|||||||
const poolColor = getPoolColor(poolName);
|
const poolColor = getPoolColor(poolName);
|
||||||
|
|
||||||
// Check if this is an Ocean pool block for special styling
|
// Check if this is an Ocean pool block for special styling
|
||||||
const isOceanPool = poolName.toLowerCase().includes('ocean');
|
const isPoolOcean = isOceanPool(poolName);
|
||||||
|
|
||||||
// Calculate total fees in BTC
|
// Calculate total fees in BTC
|
||||||
const totalFees = block.extras ? (block.extras.totalFees / 100000000).toFixed(8) : "N/A";
|
const totalFees = block.extras ? (block.extras.totalFees / SATOSHIS_PER_BTC).toFixed(8) : "N/A";
|
||||||
|
|
||||||
// Create the block card
|
// Create the block card with accessibility attributes
|
||||||
const blockCard = $("<div>", {
|
const blockCard = $("<div>", {
|
||||||
class: "block-card",
|
class: "block-card",
|
||||||
"data-height": block.height,
|
"data-height": block.height,
|
||||||
"data-hash": block.id
|
"data-hash": block.id,
|
||||||
|
tabindex: "0", // Make focusable
|
||||||
|
role: "button",
|
||||||
|
"aria-label": `Block ${block.height} mined by ${poolName} on ${timestamp}`
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply pool color border - with special emphasis for Ocean pool
|
// Apply pool color border - with special emphasis for Ocean pool
|
||||||
if (isOceanPool) {
|
if (isPoolOcean) {
|
||||||
// Give Ocean pool blocks a more prominent styling
|
// Give Ocean pool blocks a more prominent styling
|
||||||
blockCard.css({
|
blockCard.css({
|
||||||
"border": `2px solid ${poolColor}`,
|
"border": `2px solid ${poolColor}`,
|
||||||
@ -268,15 +412,6 @@ function createBlockCard(block) {
|
|||||||
class: "block-info"
|
class: "block-info"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add transaction count with conditional coloring based on count
|
|
||||||
const txCountItem = $("<div>", {
|
|
||||||
class: "block-info-item"
|
|
||||||
});
|
|
||||||
txCountItem.append($("<div>", {
|
|
||||||
class: "block-info-label",
|
|
||||||
text: "Transactions"
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Determine transaction count color based on thresholds
|
// Determine transaction count color based on thresholds
|
||||||
let txCountClass = "green"; // Default for high transaction counts (2000+)
|
let txCountClass = "green"; // Default for high transaction counts (2000+)
|
||||||
if (block.tx_count < 500) {
|
if (block.tx_count < 500) {
|
||||||
@ -285,25 +420,11 @@ function createBlockCard(block) {
|
|||||||
txCountClass = "yellow"; // Between 500 and 1999 transactions
|
txCountClass = "yellow"; // Between 500 and 1999 transactions
|
||||||
}
|
}
|
||||||
|
|
||||||
txCountItem.append($("<div>", {
|
// Add transaction count using helper
|
||||||
class: `block-info-value ${txCountClass}`,
|
blockInfo.append(createInfoItem("Transactions", formattedTxCount, txCountClass));
|
||||||
text: formattedTxCount
|
|
||||||
}));
|
|
||||||
blockInfo.append(txCountItem);
|
|
||||||
|
|
||||||
// Add size
|
// Add size using helper
|
||||||
const sizeItem = $("<div>", {
|
blockInfo.append(createInfoItem("Size", formattedSize, "white"));
|
||||||
class: "block-info-item"
|
|
||||||
});
|
|
||||||
sizeItem.append($("<div>", {
|
|
||||||
class: "block-info-label",
|
|
||||||
text: "Size"
|
|
||||||
}));
|
|
||||||
sizeItem.append($("<div>", {
|
|
||||||
class: "block-info-value white",
|
|
||||||
text: formattedSize
|
|
||||||
}));
|
|
||||||
blockInfo.append(sizeItem);
|
|
||||||
|
|
||||||
// Add miner/pool with custom color
|
// Add miner/pool with custom color
|
||||||
const minerItem = $("<div>", {
|
const minerItem = $("<div>", {
|
||||||
@ -320,13 +441,13 @@ function createBlockCard(block) {
|
|||||||
text: poolName,
|
text: poolName,
|
||||||
css: {
|
css: {
|
||||||
color: poolColor,
|
color: poolColor,
|
||||||
textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
|
textShadow: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
|
||||||
fontWeight: isOceanPool ? "bold" : "normal"
|
fontWeight: isPoolOcean ? "bold" : "normal"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a special indicator icon for Ocean pool
|
// Add a special indicator icon for Ocean pool
|
||||||
if (isOceanPool) {
|
if (isPoolOcean) {
|
||||||
minerValue.prepend($("<span>", {
|
minerValue.prepend($("<span>", {
|
||||||
html: "★ ",
|
html: "★ ",
|
||||||
css: { color: poolColor }
|
css: { color: poolColor }
|
||||||
@ -336,80 +457,76 @@ function createBlockCard(block) {
|
|||||||
minerItem.append(minerValue);
|
minerItem.append(minerValue);
|
||||||
blockInfo.append(minerItem);
|
blockInfo.append(minerItem);
|
||||||
|
|
||||||
// Add Avg Fee Rate
|
// Add Avg Fee Rate using helper
|
||||||
const feesItem = $("<div>", {
|
const feeRateText = block.extras && block.extras.avgFeeRate ? block.extras.avgFeeRate + " sat/vB" : "N/A";
|
||||||
class: "block-info-item"
|
blockInfo.append(createInfoItem("Avg Fee Rate", feeRateText, "yellow"));
|
||||||
});
|
|
||||||
feesItem.append($("<div>", {
|
|
||||||
class: "block-info-label",
|
|
||||||
text: "Avg Fee Rate"
|
|
||||||
}));
|
|
||||||
feesItem.append($("<div>", {
|
|
||||||
class: "block-info-value yellow",
|
|
||||||
text: block.extras && block.extras.avgFeeRate ? block.extras.avgFeeRate + " sat/vB" : "N/A"
|
|
||||||
}));
|
|
||||||
blockInfo.append(feesItem);
|
|
||||||
|
|
||||||
blockCard.append(blockInfo);
|
blockCard.append(blockInfo);
|
||||||
|
|
||||||
// Add event listener for clicking on the block card
|
// Add event listeners for clicking and keyboard on the block card
|
||||||
blockCard.on("click", function () {
|
blockCard.on("click", function () {
|
||||||
showBlockDetails(block);
|
showBlockDetails(block);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
blockCard.on("keypress", function (e) {
|
||||||
|
if (e.which === 13 || e.which === 32) { // Enter or Space key
|
||||||
|
showBlockDetails(block);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return blockCard;
|
return blockCard;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to load blocks from a specific height
|
// Function to load blocks from a specific height
|
||||||
function loadBlocksFromHeight(height) {
|
function loadBlocksFromHeight(height) {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
|
|
||||||
// Convert to integer
|
// Convert to integer
|
||||||
height = parseInt(height);
|
height = parseInt(height);
|
||||||
if (isNaN(height) || height < 0) {
|
if (isNaN(height) || height < 0) {
|
||||||
showToast("Please enter a valid block height");
|
showToast("Please enter a valid block height");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
isLoading = true;
|
isLoading = true;
|
||||||
currentStartHeight = height;
|
currentStartHeight = height;
|
||||||
|
|
||||||
// Check if we already have this data in cache
|
// Check if we already have this data in cache
|
||||||
if (blocksCache[height]) {
|
if (blocksCache[height]) {
|
||||||
displayBlocks(blocksCache[height]);
|
displayBlocks(blocksCache[height]);
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show loading state
|
// 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>');
|
$("#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
|
// Fetch blocks from the API
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: `${mempoolBaseUrl}/api/v1/blocks/${height}`,
|
url: `${mempoolBaseUrl}/api/v1/blocks/${height}`,
|
||||||
method: "GET",
|
method: "GET",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
success: function(data) {
|
success: function (data) {
|
||||||
// Cache the data
|
// Cache the data using helper
|
||||||
blocksCache[height] = data;
|
addToCache(height, data);
|
||||||
|
|
||||||
// Display the blocks
|
// Display the blocks
|
||||||
displayBlocks(data);
|
displayBlocks(data);
|
||||||
|
|
||||||
// Update latest block stats
|
// Update latest block stats
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
updateLatestBlockStats(data[0]);
|
updateLatestBlockStats(data[0]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(xhr, status, error) {
|
error: function (xhr, status, error) {
|
||||||
console.error("Error fetching blocks:", error);
|
console.error("Error fetching blocks:", error);
|
||||||
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
|
$("#blocks-grid").html('<div class="error">Error fetching blocks. Please try again later.</div>');
|
||||||
|
|
||||||
// Show error toast
|
// Show error toast
|
||||||
showToast("Failed to load blocks. Please try again later.");
|
showToast("Failed to load blocks. Please try again later.");
|
||||||
},
|
},
|
||||||
complete: function() {
|
complete: function () {
|
||||||
isLoading = false;
|
isLoading = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -434,7 +551,7 @@ function loadLatestBlocks() {
|
|||||||
// Cache the data (use the first block's height as the key)
|
// Cache the data (use the first block's height as the key)
|
||||||
if (data.length > 0) {
|
if (data.length > 0) {
|
||||||
currentStartHeight = data[0].height;
|
currentStartHeight = data[0].height;
|
||||||
blocksCache[currentStartHeight] = data;
|
addToCache(currentStartHeight, data);
|
||||||
|
|
||||||
// Update the block height input with the latest height
|
// Update the block height input with the latest height
|
||||||
$("#block-height").val(currentStartHeight);
|
$("#block-height").val(currentStartHeight);
|
||||||
@ -459,19 +576,22 @@ function loadLatestBlocks() {
|
|||||||
}).then(data => data.length > 0 ? data[0].height : null);
|
}).then(data => data.length > 0 ? data[0].height : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh blocks page every 60 seconds if there are new blocks
|
// Refresh blocks page every 60 seconds if there are new blocks - with smart refresh
|
||||||
setInterval(function () {
|
setInterval(function () {
|
||||||
console.log("Checking for new blocks at " + new Date().toLocaleTimeString());
|
console.log("Checking for new blocks at " + new Date().toLocaleTimeString());
|
||||||
loadLatestBlocks().then(latestHeight => {
|
loadLatestBlocks().then(latestHeight => {
|
||||||
if (latestHeight && latestHeight > currentStartHeight) {
|
if (latestHeight && latestHeight > currentStartHeight) {
|
||||||
console.log("New blocks detected, refreshing the page");
|
console.log("New blocks detected, loading latest blocks");
|
||||||
location.reload();
|
// Instead of reloading the page, just load the latest blocks
|
||||||
|
currentStartHeight = latestHeight;
|
||||||
|
loadLatestBlocks();
|
||||||
|
// Show a notification
|
||||||
|
showToast("New blocks detected! View updated.");
|
||||||
} else {
|
} else {
|
||||||
console.log("No new blocks detected");
|
console.log("No new blocks detected");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 60000);
|
}, REFRESH_INTERVAL);
|
||||||
|
|
||||||
|
|
||||||
// Function to update the latest block stats section
|
// Function to update the latest block stats section
|
||||||
function updateLatestBlockStats(block) {
|
function updateLatestBlockStats(block) {
|
||||||
@ -486,7 +606,7 @@ function updateLatestBlockStats(block) {
|
|||||||
// Pool info with color coding
|
// Pool info with color coding
|
||||||
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
|
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
|
||||||
const poolColor = getPoolColor(poolName);
|
const poolColor = getPoolColor(poolName);
|
||||||
const isOceanPool = poolName.toLowerCase().includes('ocean');
|
const isPoolOcean = isOceanPool(poolName);
|
||||||
|
|
||||||
// Clear previous content of the pool span
|
// Clear previous content of the pool span
|
||||||
const poolSpan = $("#latest-pool");
|
const poolSpan = $("#latest-pool");
|
||||||
@ -497,13 +617,13 @@ function updateLatestBlockStats(block) {
|
|||||||
text: poolName,
|
text: poolName,
|
||||||
css: {
|
css: {
|
||||||
color: poolColor,
|
color: poolColor,
|
||||||
textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
|
textShadow: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
|
||||||
fontWeight: isOceanPool ? "bold" : "normal"
|
fontWeight: isPoolOcean ? "bold" : "normal"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add star icon for Ocean pool
|
// Add star icon for Ocean pool
|
||||||
if (isOceanPool) {
|
if (isPoolOcean) {
|
||||||
poolElement.prepend($("<span>", {
|
poolElement.prepend($("<span>", {
|
||||||
html: "★ ",
|
html: "★ ",
|
||||||
css: { color: poolColor }
|
css: { color: poolColor }
|
||||||
@ -515,7 +635,7 @@ function updateLatestBlockStats(block) {
|
|||||||
|
|
||||||
// If this is the latest block from Ocean pool, add a subtle highlight to the stats card
|
// If this is the latest block from Ocean pool, add a subtle highlight to the stats card
|
||||||
const statsCard = $(".latest-block-stats").closest(".card");
|
const statsCard = $(".latest-block-stats").closest(".card");
|
||||||
if (isOceanPool) {
|
if (isPoolOcean) {
|
||||||
statsCard.css({
|
statsCard.css({
|
||||||
"border": `2px solid ${poolColor}`,
|
"border": `2px solid ${poolColor}`,
|
||||||
"box-shadow": `0 0 10px ${poolColor}`,
|
"box-shadow": `0 0 10px ${poolColor}`,
|
||||||
@ -541,21 +661,27 @@ function updateLatestBlockStats(block) {
|
|||||||
// Function to display the blocks in the grid
|
// Function to display the blocks in the grid
|
||||||
function displayBlocks(blocks) {
|
function displayBlocks(blocks) {
|
||||||
const blocksGrid = $("#blocks-grid");
|
const blocksGrid = $("#blocks-grid");
|
||||||
|
|
||||||
// Clear the grid
|
// Clear the grid
|
||||||
blocksGrid.empty();
|
blocksGrid.empty();
|
||||||
|
|
||||||
if (!blocks || blocks.length === 0) {
|
if (!blocks || blocks.length === 0) {
|
||||||
blocksGrid.html('<div class="no-blocks">No blocks found</div>');
|
blocksGrid.html('<div class="no-blocks">No blocks found</div>');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Use document fragment for batch DOM operations
|
||||||
|
const fragment = document.createDocumentFragment();
|
||||||
|
|
||||||
// Create a card for each block
|
// Create a card for each block
|
||||||
blocks.forEach(function(block) {
|
blocks.forEach(function (block) {
|
||||||
const blockCard = createBlockCard(block);
|
const blockCard = createBlockCard(block);
|
||||||
blocksGrid.append(blockCard);
|
fragment.appendChild(blockCard[0]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add all cards at once
|
||||||
|
blocksGrid.append(fragment);
|
||||||
|
|
||||||
// Add navigation controls if needed
|
// Add navigation controls if needed
|
||||||
addNavigationControls(blocks);
|
addNavigationControls(blocks);
|
||||||
}
|
}
|
||||||
@ -565,38 +691,40 @@ function addNavigationControls(blocks) {
|
|||||||
// Get the height of the first and last block in the current view
|
// Get the height of the first and last block in the current view
|
||||||
const firstBlockHeight = blocks[0].height;
|
const firstBlockHeight = blocks[0].height;
|
||||||
const lastBlockHeight = blocks[blocks.length - 1].height;
|
const lastBlockHeight = blocks[blocks.length - 1].height;
|
||||||
|
|
||||||
// Create navigation controls
|
// Create navigation controls
|
||||||
const navControls = $("<div>", {
|
const navControls = $("<div>", {
|
||||||
class: "block-navigation"
|
class: "block-navigation"
|
||||||
});
|
});
|
||||||
|
|
||||||
// Newer blocks button (if not already at the latest blocks)
|
// Newer blocks button (if not already at the latest blocks)
|
||||||
if (firstBlockHeight !== currentStartHeight) {
|
if (firstBlockHeight !== currentStartHeight) {
|
||||||
const newerButton = $("<button>", {
|
const newerButton = $("<button>", {
|
||||||
class: "block-button",
|
class: "block-button",
|
||||||
text: "Newer Blocks"
|
text: "Newer Blocks",
|
||||||
|
"aria-label": "Load newer blocks"
|
||||||
});
|
});
|
||||||
|
|
||||||
newerButton.on("click", function() {
|
newerButton.on("click", function () {
|
||||||
loadBlocksFromHeight(firstBlockHeight + 15);
|
loadBlocksFromHeight(firstBlockHeight + 15);
|
||||||
});
|
});
|
||||||
|
|
||||||
navControls.append(newerButton);
|
navControls.append(newerButton);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Older blocks button
|
// Older blocks button
|
||||||
const olderButton = $("<button>", {
|
const olderButton = $("<button>", {
|
||||||
class: "block-button",
|
class: "block-button",
|
||||||
text: "Older Blocks"
|
text: "Older Blocks",
|
||||||
|
"aria-label": "Load older blocks"
|
||||||
});
|
});
|
||||||
|
|
||||||
olderButton.on("click", function() {
|
olderButton.on("click", function () {
|
||||||
loadBlocksFromHeight(lastBlockHeight - 1);
|
loadBlocksFromHeight(lastBlockHeight - 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
navControls.append(olderButton);
|
navControls.append(olderButton);
|
||||||
|
|
||||||
// Add the navigation controls to the blocks grid
|
// Add the navigation controls to the blocks grid
|
||||||
$("#blocks-grid").append(navControls);
|
$("#blocks-grid").append(navControls);
|
||||||
}
|
}
|
||||||
@ -606,6 +734,12 @@ function showBlockDetails(block) {
|
|||||||
const modal = $("#block-modal");
|
const modal = $("#block-modal");
|
||||||
const blockDetails = $("#block-details");
|
const blockDetails = $("#block-details");
|
||||||
|
|
||||||
|
// Clean up previous handlers
|
||||||
|
cleanupEventHandlers();
|
||||||
|
|
||||||
|
// Re-add scoped handlers
|
||||||
|
setupModalKeyboardNavigation();
|
||||||
|
|
||||||
// Clear the details
|
// Clear the details
|
||||||
blockDetails.empty();
|
blockDetails.empty();
|
||||||
|
|
||||||
@ -636,7 +770,7 @@ function showBlockDetails(block) {
|
|||||||
}));
|
}));
|
||||||
headerSection.append(hashItem);
|
headerSection.append(hashItem);
|
||||||
|
|
||||||
// Add mempool.space link
|
// Add mempool.guide link
|
||||||
const linkItem = $("<div>", {
|
const linkItem = $("<div>", {
|
||||||
class: "block-detail-item"
|
class: "block-detail-item"
|
||||||
});
|
});
|
||||||
@ -649,7 +783,8 @@ function showBlockDetails(block) {
|
|||||||
href: `${mempoolBaseUrl}/block/${block.id}`,
|
href: `${mempoolBaseUrl}/block/${block.id}`,
|
||||||
target: "_blank",
|
target: "_blank",
|
||||||
class: "mempool-link",
|
class: "mempool-link",
|
||||||
text: "View on mempool.space",
|
text: "View on mempool.guide",
|
||||||
|
"aria-label": `View block ${block.height} on mempool.guide (opens in new window)`,
|
||||||
css: {
|
css: {
|
||||||
color: "#f7931a",
|
color: "#f7931a",
|
||||||
textDecoration: "none"
|
textDecoration: "none"
|
||||||
@ -672,19 +807,8 @@ function showBlockDetails(block) {
|
|||||||
|
|
||||||
headerSection.append(linkItem);
|
headerSection.append(linkItem);
|
||||||
|
|
||||||
// Add timestamp
|
// Add timestamp using helper
|
||||||
const timeItem = $("<div>", {
|
headerSection.append(createDetailItem("Timestamp", timestamp));
|
||||||
class: "block-detail-item"
|
|
||||||
});
|
|
||||||
timeItem.append($("<div>", {
|
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Timestamp"
|
|
||||||
}));
|
|
||||||
timeItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: timestamp
|
|
||||||
}));
|
|
||||||
headerSection.append(timeItem);
|
|
||||||
|
|
||||||
// Add merkle root
|
// Add merkle root
|
||||||
const merkleItem = $("<div>", {
|
const merkleItem = $("<div>", {
|
||||||
@ -736,7 +860,7 @@ function showBlockDetails(block) {
|
|||||||
}));
|
}));
|
||||||
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
|
const poolName = block.extras && block.extras.pool ? block.extras.pool.name : "Unknown";
|
||||||
const poolColor = getPoolColor(poolName);
|
const poolColor = getPoolColor(poolName);
|
||||||
const isOceanPool = poolName.toLowerCase().includes('ocean');
|
const isPoolOcean = isOceanPool(poolName);
|
||||||
|
|
||||||
// Apply special styling for Ocean pool in the modal
|
// Apply special styling for Ocean pool in the modal
|
||||||
const minerValue = $("<div>", {
|
const minerValue = $("<div>", {
|
||||||
@ -744,13 +868,13 @@ function showBlockDetails(block) {
|
|||||||
text: poolName,
|
text: poolName,
|
||||||
css: {
|
css: {
|
||||||
color: poolColor,
|
color: poolColor,
|
||||||
textShadow: isOceanPool ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
|
textShadow: isPoolOcean ? `0 0 8px ${poolColor}` : `0 0 6px ${poolColor}80`,
|
||||||
fontWeight: isOceanPool ? "bold" : "normal"
|
fontWeight: isPoolOcean ? "bold" : "normal"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add a special indicator icon for Ocean pool
|
// Add a special indicator icon for Ocean pool
|
||||||
if (isOceanPool) {
|
if (isPoolOcean) {
|
||||||
minerValue.prepend($("<span>", {
|
minerValue.prepend($("<span>", {
|
||||||
html: "★ ",
|
html: "★ ",
|
||||||
css: { color: poolColor }
|
css: { color: poolColor }
|
||||||
@ -770,62 +894,26 @@ function showBlockDetails(block) {
|
|||||||
minerItem.append(minerValue);
|
minerItem.append(minerValue);
|
||||||
miningSection.append(minerItem);
|
miningSection.append(minerItem);
|
||||||
|
|
||||||
// Rest of the function remains unchanged
|
// Add difficulty with helper
|
||||||
// Add difficulty
|
miningSection.append(createDetailItem(
|
||||||
const difficultyItem = $("<div>", {
|
"Difficulty",
|
||||||
class: "block-detail-item"
|
numberWithCommas(Math.round(block.difficulty))
|
||||||
});
|
));
|
||||||
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
|
// Add nonce with helper
|
||||||
const nonceItem = $("<div>", {
|
miningSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Nonce",
|
||||||
});
|
numberWithCommas(block.nonce)
|
||||||
nonceItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Nonce"
|
|
||||||
}));
|
|
||||||
nonceItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: numberWithCommas(block.nonce)
|
|
||||||
}));
|
|
||||||
miningSection.append(nonceItem);
|
|
||||||
|
|
||||||
// Add bits
|
// Add bits with helper
|
||||||
const bitsItem = $("<div>", {
|
miningSection.append(createDetailItem("Bits", block.bits));
|
||||||
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
|
// Add version with helper
|
||||||
const versionItem = $("<div>", {
|
miningSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Version",
|
||||||
});
|
"0x" + block.version.toString(16)
|
||||||
versionItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Version"
|
|
||||||
}));
|
|
||||||
versionItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: "0x" + block.version.toString(16)
|
|
||||||
}));
|
|
||||||
miningSection.append(versionItem);
|
|
||||||
|
|
||||||
blockDetails.append(miningSection);
|
blockDetails.append(miningSection);
|
||||||
|
|
||||||
@ -839,47 +927,23 @@ function showBlockDetails(block) {
|
|||||||
text: "Transaction Details"
|
text: "Transaction Details"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add transaction count
|
// Add transaction count with helper
|
||||||
const txCountItem = $("<div>", {
|
txSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Transaction Count",
|
||||||
});
|
numberWithCommas(block.tx_count)
|
||||||
txCountItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Transaction Count"
|
|
||||||
}));
|
|
||||||
txCountItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: numberWithCommas(block.tx_count)
|
|
||||||
}));
|
|
||||||
txSection.append(txCountItem);
|
|
||||||
|
|
||||||
// Add size
|
// Add size with helper
|
||||||
const sizeItem = $("<div>", {
|
txSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Size",
|
||||||
});
|
formatFileSize(block.size)
|
||||||
sizeItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Size"
|
|
||||||
}));
|
|
||||||
sizeItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: formatFileSize(block.size)
|
|
||||||
}));
|
|
||||||
txSection.append(sizeItem);
|
|
||||||
|
|
||||||
// Add weight
|
// Add weight with helper
|
||||||
const weightItem = $("<div>", {
|
txSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Weight",
|
||||||
});
|
numberWithCommas(block.weight) + " WU"
|
||||||
weightItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Weight"
|
|
||||||
}));
|
|
||||||
weightItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: numberWithCommas(block.weight) + " WU"
|
|
||||||
}));
|
|
||||||
txSection.append(weightItem);
|
|
||||||
|
|
||||||
blockDetails.append(txSection);
|
blockDetails.append(txSection);
|
||||||
|
|
||||||
@ -894,77 +958,31 @@ function showBlockDetails(block) {
|
|||||||
text: "Fee Details"
|
text: "Fee Details"
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Add total fees
|
// Add total fees with helper
|
||||||
const totalFeesItem = $("<div>", {
|
const totalFees = (block.extras.totalFees / SATOSHIS_PER_BTC).toFixed(8);
|
||||||
class: "block-detail-item"
|
feeSection.append(createDetailItem("Total Fees", totalFees + " BTC"));
|
||||||
});
|
|
||||||
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
|
// Add reward with helper
|
||||||
const rewardItem = $("<div>", {
|
const reward = (block.extras.reward / SATOSHIS_PER_BTC).toFixed(8);
|
||||||
class: "block-detail-item"
|
feeSection.append(createDetailItem("Block Reward", reward + " BTC"));
|
||||||
});
|
|
||||||
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
|
// Add median fee with helper
|
||||||
const medianFeeItem = $("<div>", {
|
feeSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Median Fee Rate",
|
||||||
});
|
block.extras.medianFee + " sat/vB"
|
||||||
medianFeeItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Median Fee Rate"
|
|
||||||
}));
|
|
||||||
medianFeeItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: block.extras.medianFee + " sat/vB"
|
|
||||||
}));
|
|
||||||
feeSection.append(medianFeeItem);
|
|
||||||
|
|
||||||
// Add average fee
|
// Add average fee with helper
|
||||||
const avgFeeItem = $("<div>", {
|
feeSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Average Fee",
|
||||||
});
|
numberWithCommas(block.extras.avgFee) + " sat"
|
||||||
avgFeeItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Average Fee"
|
|
||||||
}));
|
|
||||||
avgFeeItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: numberWithCommas(block.extras.avgFee) + " sat"
|
|
||||||
}));
|
|
||||||
feeSection.append(avgFeeItem);
|
|
||||||
|
|
||||||
// Add average fee rate
|
// Add average fee rate with helper
|
||||||
const avgFeeRateItem = $("<div>", {
|
feeSection.append(createDetailItem(
|
||||||
class: "block-detail-item"
|
"Average Fee Rate",
|
||||||
});
|
block.extras.avgFeeRate + " sat/vB"
|
||||||
avgFeeRateItem.append($("<div>", {
|
));
|
||||||
class: "block-detail-label",
|
|
||||||
text: "Average Fee Rate"
|
|
||||||
}));
|
|
||||||
avgFeeRateItem.append($("<div>", {
|
|
||||||
class: "block-detail-value",
|
|
||||||
text: block.extras.avgFeeRate + " sat/vB"
|
|
||||||
}));
|
|
||||||
feeSection.append(avgFeeRateItem);
|
|
||||||
|
|
||||||
// Add fee range with visual representation
|
// Add fee range with visual representation
|
||||||
if (block.extras.feeRange && block.extras.feeRange.length > 0) {
|
if (block.extras.feeRange && block.extras.feeRange.length > 0) {
|
||||||
@ -986,7 +1004,8 @@ function showBlockDetails(block) {
|
|||||||
|
|
||||||
// Add visual fee bar
|
// Add visual fee bar
|
||||||
const feeBarContainer = $("<div>", {
|
const feeBarContainer = $("<div>", {
|
||||||
class: "fee-bar-container"
|
class: "fee-bar-container",
|
||||||
|
"aria-label": "Fee rate range visualization"
|
||||||
});
|
});
|
||||||
|
|
||||||
const feeBar = $("<div>", {
|
const feeBar = $("<div>", {
|
||||||
@ -1007,11 +1026,15 @@ function showBlockDetails(block) {
|
|||||||
blockDetails.append(feeSection);
|
blockDetails.append(feeSection);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show the modal
|
// Show the modal with aria attributes
|
||||||
|
modal.attr("aria-hidden", "false");
|
||||||
modal.css("display", "block");
|
modal.css("display", "block");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to close the modal
|
// Function to close the modal
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
$("#block-modal").css("display", "none");
|
const modal = $("#block-modal");
|
||||||
|
modal.css("display", "none");
|
||||||
|
modal.attr("aria-hidden", "true");
|
||||||
|
cleanupEventHandlers();
|
||||||
}
|
}
|
||||||
|
1043
static/js/main.js
1043
static/js/main.js
File diff suppressed because it is too large
Load Diff
@ -7,10 +7,17 @@ const pageSize = 20;
|
|||||||
let hasMoreNotifications = true;
|
let hasMoreNotifications = true;
|
||||||
let isLoading = false;
|
let isLoading = false;
|
||||||
|
|
||||||
|
// Timezone configuration
|
||||||
|
let dashboardTimezone = 'America/Los_Angeles'; // Default
|
||||||
|
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
|
||||||
|
|
||||||
// Initialize when document is ready
|
// Initialize when document is ready
|
||||||
$(document).ready(() => {
|
$(document).ready(() => {
|
||||||
console.log("Notification page initializing...");
|
console.log("Notification page initializing...");
|
||||||
|
|
||||||
|
// Fetch timezone configuration
|
||||||
|
fetchTimezoneConfig();
|
||||||
|
|
||||||
// Set up filter buttons
|
// Set up filter buttons
|
||||||
$('.filter-button').click(function () {
|
$('.filter-button').click(function () {
|
||||||
$('.filter-button').removeClass('active');
|
$('.filter-button').removeClass('active');
|
||||||
@ -41,6 +48,34 @@ $(document).ready(() => {
|
|||||||
setInterval(updateNotificationTimestamps, 30000);
|
setInterval(updateNotificationTimestamps, 30000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fetch timezone configuration from server
|
||||||
|
function fetchTimezoneConfig() {
|
||||||
|
return fetch('/api/timezone')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.timezone) {
|
||||||
|
dashboardTimezone = data.timezone;
|
||||||
|
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
|
||||||
|
console.log(`Notifications page using timezone: ${dashboardTimezone}`);
|
||||||
|
|
||||||
|
// Store in localStorage for future use
|
||||||
|
try {
|
||||||
|
localStorage.setItem('dashboardTimezone', dashboardTimezone);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error storing timezone in localStorage:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update all timestamps with the new timezone
|
||||||
|
updateNotificationTimestamps();
|
||||||
|
return dashboardTimezone;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error fetching timezone config:', error);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Load notifications with current filter
|
// Load notifications with current filter
|
||||||
function loadNotifications() {
|
function loadNotifications() {
|
||||||
if (isLoading) return;
|
if (isLoading) return;
|
||||||
@ -104,14 +139,36 @@ function refreshNotifications() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update notification timestamps to relative time
|
// This refreshes all timestamps on the page periodically
|
||||||
function updateNotificationTimestamps() {
|
function updateNotificationTimestamps() {
|
||||||
$('.notification-item').each(function () {
|
$('.notification-item').each(function () {
|
||||||
const timestampStr = $(this).attr('data-timestamp');
|
const timestampStr = $(this).attr('data-timestamp');
|
||||||
if (timestampStr) {
|
if (timestampStr) {
|
||||||
const timestamp = new Date(timestampStr);
|
try {
|
||||||
const relativeTime = formatTimestamp(timestamp);
|
const timestamp = new Date(timestampStr);
|
||||||
$(this).find('.notification-time').text(relativeTime);
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -190,14 +247,28 @@ function createNotificationElement(notification) {
|
|||||||
iconElement.addClass('fa-bell');
|
iconElement.addClass('fa-bell');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append "Z" to indicate UTC if not present
|
// Important: Do not append "Z" here, as that can cause timezone issues
|
||||||
let utcTimestampStr = notification.timestamp;
|
// Create a date object from the notification timestamp
|
||||||
if (!utcTimestampStr.endsWith('Z')) {
|
let notificationDate;
|
||||||
utcTimestampStr += 'Z';
|
try {
|
||||||
}
|
// Parse the timestamp directly without modifications
|
||||||
const utcDate = new Date(utcTimestampStr);
|
notificationDate = new Date(notification.timestamp);
|
||||||
|
|
||||||
// Convert UTC date to Los Angeles time with a timezone name for clarity
|
// Validate the date object - if invalid, try alternative approach
|
||||||
|
if (isNaN(notificationDate.getTime())) {
|
||||||
|
console.warn("Invalid date from notification timestamp, trying alternative format");
|
||||||
|
|
||||||
|
// Try adding Z to make it explicit UTC if not already ISO format
|
||||||
|
if (!notification.timestamp.endsWith('Z') && !notification.timestamp.includes('+')) {
|
||||||
|
notificationDate = new Date(notification.timestamp + 'Z');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error parsing notification date:", e);
|
||||||
|
notificationDate = new Date(); // Fallback to current date
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format the timestamp using the configured timezone
|
||||||
const options = {
|
const options = {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@ -205,16 +276,25 @@ function createNotificationElement(notification) {
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: true
|
hour12: true,
|
||||||
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
||||||
};
|
};
|
||||||
const fullTimestamp = utcDate.toLocaleString('en-US', options);
|
|
||||||
|
|
||||||
// Append the full timestamp to the notification message
|
// Format full timestamp with configured timezone
|
||||||
|
let fullTimestamp;
|
||||||
|
try {
|
||||||
|
fullTimestamp = notificationDate.toLocaleString('en-US', options);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error formatting timestamp with timezone:", e);
|
||||||
|
fullTimestamp = notificationDate.toLocaleString('en-US'); // Fallback without timezone
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append the message and formatted timestamp
|
||||||
const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`;
|
const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`;
|
||||||
element.find('.notification-message').html(messageWithTimestamp);
|
element.find('.notification-message').html(messageWithTimestamp);
|
||||||
|
|
||||||
// Set metadata for relative time display
|
// Set metadata for relative time display
|
||||||
element.find('.notification-time').text(formatTimestamp(utcDate));
|
element.find('.notification-time').text(formatTimestamp(notificationDate));
|
||||||
element.find('.notification-category').text(notification.category);
|
element.find('.notification-category').text(notification.category);
|
||||||
|
|
||||||
// Set up action buttons
|
// Set up action buttons
|
||||||
@ -235,10 +315,21 @@ function createNotificationElement(notification) {
|
|||||||
return element;
|
return element;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format timestamp as relative time
|
|
||||||
function formatTimestamp(timestamp) {
|
function formatTimestamp(timestamp) {
|
||||||
|
// Ensure we have a valid date object
|
||||||
|
let dateObj = timestamp;
|
||||||
|
if (!(timestamp instanceof Date) || isNaN(timestamp.getTime())) {
|
||||||
|
try {
|
||||||
|
dateObj = new Date(timestamp);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Invalid timestamp in formatTimestamp:", e);
|
||||||
|
return "unknown time";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate time difference in local timezone context
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMs = now - timestamp;
|
const diffMs = now - dateObj;
|
||||||
const diffSec = Math.floor(diffMs / 1000);
|
const diffSec = Math.floor(diffMs / 1000);
|
||||||
const diffMin = Math.floor(diffSec / 60);
|
const diffMin = Math.floor(diffSec / 60);
|
||||||
const diffHour = Math.floor(diffMin / 60);
|
const diffHour = Math.floor(diffMin / 60);
|
||||||
@ -253,17 +344,14 @@ function formatTimestamp(timestamp) {
|
|||||||
} else if (diffDay < 30) {
|
} else if (diffDay < 30) {
|
||||||
return `${diffDay}d ago`;
|
return `${diffDay}d ago`;
|
||||||
} else {
|
} else {
|
||||||
// Format as date for older notifications
|
// Format as date for older notifications using configured timezone
|
||||||
const options = {
|
const options = {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
day: 'numeric',
|
day: 'numeric',
|
||||||
hour: '2-digit',
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
hour12: true
|
|
||||||
};
|
};
|
||||||
return timestamp.toLocaleDateString('en-US', options);
|
return dateObj.toLocaleDateString('en-US', options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
440
static/js/theme.js
Normal file
440
static/js/theme.js
Normal file
@ -0,0 +1,440 @@
|
|||||||
|
// 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);
|
@ -92,6 +92,40 @@ $(document).ready(function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Load timezone setting early
|
||||||
|
(function loadTimezoneEarly() {
|
||||||
|
// First try to get from localStorage for instant access
|
||||||
|
try {
|
||||||
|
const storedTimezone = localStorage.getItem('dashboardTimezone');
|
||||||
|
if (storedTimezone) {
|
||||||
|
window.dashboardTimezone = storedTimezone;
|
||||||
|
console.log(`Using cached timezone: ${storedTimezone}`);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error reading timezone from localStorage:", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Then fetch from server to ensure we have the latest setting
|
||||||
|
fetch('/api/timezone')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && data.timezone) {
|
||||||
|
window.dashboardTimezone = data.timezone;
|
||||||
|
console.log(`Set timezone from server: ${data.timezone}`);
|
||||||
|
|
||||||
|
// Cache for future use
|
||||||
|
try {
|
||||||
|
localStorage.setItem('dashboardTimezone', data.timezone);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error storing timezone in localStorage:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error("Error fetching timezone:", error);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
// Initialize page elements
|
// Initialize page elements
|
||||||
function initializePage() {
|
function initializePage() {
|
||||||
console.log("Initializing page elements...");
|
console.log("Initializing page elements...");
|
||||||
@ -321,20 +355,40 @@ function createWorkerCard(worker) {
|
|||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
|
// Format the last share using the proper method for timezone conversion
|
||||||
|
let formattedLastShare = 'N/A';
|
||||||
|
if (worker.last_share && typeof worker.last_share === 'string') {
|
||||||
|
// This is a more reliable method for timezone conversion
|
||||||
|
try {
|
||||||
|
// The worker.last_share is likely in format "YYYY-MM-DD HH:MM"
|
||||||
|
// We need to consider it as UTC and convert to the configured timezone
|
||||||
|
|
||||||
|
// Create a proper date object, ensuring UTC interpretation
|
||||||
|
const dateWithoutTZ = new Date(worker.last_share + 'Z'); // Adding Z to treat as UTC
|
||||||
|
|
||||||
|
// Format it according to the configured timezone
|
||||||
|
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
hour12: true,
|
||||||
|
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error formatting last share time:", e, worker.last_share);
|
||||||
|
formattedLastShare = worker.last_share; // Fallback to original value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
card.append(`
|
card.append(`
|
||||||
<div class="worker-stats">
|
<div class="worker-stats">
|
||||||
<div class="worker-stats-row">
|
<div class="worker-stats-row">
|
||||||
<div class="worker-stats-label">Last Share:</div>
|
<div class="worker-stats-label">Last Share:</div>
|
||||||
<div class="blue-glow">${typeof worker.last_share === 'string' ? worker.last_share.split(' ')[1] || worker.last_share : 'N/A'}</div>
|
<div class="blue-glow">${formattedLastShare}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="worker-stats-row">
|
<div class="worker-stats-row">
|
||||||
<div class="worker-stats-label">Earnings:</div>
|
<div class="worker-stats-label">Earnings:</div>
|
||||||
<div class="green-glow">${worker.earnings.toFixed(8)}</div>
|
<div class="green-glow">${worker.earnings.toFixed(8)}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="worker-stats-row">
|
|
||||||
<div class="worker-stats-label">Accept Rate:</div>
|
|
||||||
<div class="white-glow">${worker.acceptance_rate}%</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`);
|
`);
|
||||||
|
|
||||||
@ -386,7 +440,6 @@ function updateSummaryStats() {
|
|||||||
|
|
||||||
$('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`);
|
$('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`);
|
||||||
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`);
|
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`);
|
||||||
$('#avg-acceptance-rate').text(`${(workerData.avg_acceptance_rate || 0).toFixed(2)}%`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize mini chart
|
// Initialize mini chart
|
||||||
@ -474,6 +527,11 @@ function updateLastUpdated() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const timestamp = new Date(workerData.timestamp);
|
const timestamp = new Date(workerData.timestamp);
|
||||||
|
|
||||||
|
// Get the configured timezone with a fallback
|
||||||
|
const configuredTimezone = window.dashboardTimezone || 'America/Los_Angeles';
|
||||||
|
|
||||||
|
// Format with the configured timezone
|
||||||
const options = {
|
const options = {
|
||||||
year: 'numeric',
|
year: 'numeric',
|
||||||
month: 'short',
|
month: 'short',
|
||||||
@ -481,12 +539,22 @@ function updateLastUpdated() {
|
|||||||
hour: '2-digit',
|
hour: '2-digit',
|
||||||
minute: '2-digit',
|
minute: '2-digit',
|
||||||
second: '2-digit',
|
second: '2-digit',
|
||||||
hour12: true
|
hour12: true,
|
||||||
|
timeZone: configuredTimezone // Explicitly use the configured timezone
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Format the timestamp and update the DOM
|
||||||
|
const formattedTime = timestamp.toLocaleString('en-US', options);
|
||||||
|
|
||||||
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
|
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
|
||||||
timestamp.toLocaleString('en-US', options) + "<span id='terminal-cursor'></span>");
|
formattedTime + "<span id='terminal-cursor'></span>");
|
||||||
|
|
||||||
|
console.log(`Last updated timestamp using timezone: ${configuredTimezone}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error("Error formatting timestamp:", e);
|
console.error("Error formatting timestamp:", e);
|
||||||
|
// Fallback to basic timestamp if there's an error
|
||||||
|
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
|
||||||
|
new Date().toLocaleString() + "<span id='terminal-cursor'></span>");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>{% block title %}BTC-OS MINING DASHBOARD v 0.3{% endblock %}</title>
|
<title>{% block title %}BTC-OS MINING DASHBOARD {% endblock %}</title>
|
||||||
|
|
||||||
<!-- Common fonts -->
|
<!-- Common fonts -->
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
|
||||||
@ -17,10 +17,79 @@
|
|||||||
<!-- Common CSS -->
|
<!-- Common CSS -->
|
||||||
<link rel="stylesheet" href="/static/css/common.css">
|
<link rel="stylesheet" href="/static/css/common.css">
|
||||||
|
|
||||||
|
<!-- Custom CSS -->
|
||||||
|
<link rel="stylesheet" href="/static/css/theme-toggle.css">
|
||||||
|
|
||||||
|
<!-- Theme JS (added to ensure consistent application of theme) -->
|
||||||
|
<script src="/static/js/theme.js"></script>
|
||||||
|
|
||||||
<!-- Page-specific CSS -->
|
<!-- Page-specific CSS -->
|
||||||
{% block css %}{% endblock %}
|
{% block css %}{% endblock %}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Execute this immediately to preload theme
|
||||||
|
(function () {
|
||||||
|
const useDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
|
||||||
|
const themeClass = useDeepSea ? 'deepsea-theme' : 'bitcoin-theme';
|
||||||
|
|
||||||
|
// Apply theme class to html element
|
||||||
|
document.documentElement.classList.add(themeClass);
|
||||||
|
|
||||||
|
// Create and add loader
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Create loader element
|
||||||
|
const loader = document.createElement('div');
|
||||||
|
loader.id = 'theme-loader';
|
||||||
|
|
||||||
|
const icon = document.createElement('div');
|
||||||
|
icon.id = 'loader-icon';
|
||||||
|
icon.innerHTML = useDeepSea ? '🌊' : '₿';
|
||||||
|
|
||||||
|
const text = document.createElement('div');
|
||||||
|
text.id = 'loader-text';
|
||||||
|
text.textContent = 'Loading ' + (useDeepSea ? 'DeepSea' : 'Bitcoin') + ' Theme';
|
||||||
|
|
||||||
|
loader.appendChild(icon);
|
||||||
|
loader.appendChild(text);
|
||||||
|
document.body.appendChild(loader);
|
||||||
|
|
||||||
|
// Add fade-in effect for content once theme is loaded
|
||||||
|
setTimeout(function () {
|
||||||
|
document.body.style.visibility = 'visible';
|
||||||
|
|
||||||
|
// Fade out loader
|
||||||
|
loader.style.transition = 'opacity 0.5s ease';
|
||||||
|
loader.style.opacity = '0';
|
||||||
|
|
||||||
|
// Remove loader after fade
|
||||||
|
setTimeout(function () {
|
||||||
|
if (loader && loader.parentNode) {
|
||||||
|
loader.parentNode.removeChild(loader);
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
}, 300);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script>
|
||||||
|
// Add underwater effects for DeepSea theme
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Check if DeepSea theme is active
|
||||||
|
if (localStorage.getItem('useDeepSeaTheme') === 'true') {
|
||||||
|
// Create underwater light rays
|
||||||
|
const rays = document.createElement('div');
|
||||||
|
rays.className = 'underwater-rays';
|
||||||
|
document.body.appendChild(rays);
|
||||||
|
|
||||||
|
// Create digital noise
|
||||||
|
const noise = document.createElement('div');
|
||||||
|
noise.className = 'digital-noise';
|
||||||
|
document.body.appendChild(noise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<!-- Connection status indicator -->
|
<!-- Connection status indicator -->
|
||||||
<div id="connectionStatus"></div>
|
<div id="connectionStatus"></div>
|
||||||
@ -34,6 +103,11 @@
|
|||||||
<!-- Top right link -->
|
<!-- Top right link -->
|
||||||
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">MADE BY @DJO₿LEEZY</a>
|
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">MADE BY @DJO₿LEEZY</a>
|
||||||
|
|
||||||
|
<!-- Theme toggle button (new) -->
|
||||||
|
<button id="themeToggle" class="theme-toggle-btn">
|
||||||
|
<span>Toggle Theme</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
{% block last_updated %}
|
{% block last_updated %}
|
||||||
<p class="text-center" id="lastUpdated" style="color: #f7931a; text-transform: uppercase;"><strong>LAST UPDATED:</strong> {{ current_time }}<span id="terminal-cursor"></span></p>
|
<p class="text-center" id="lastUpdated" style="color: #f7931a; text-transform: uppercase;"><strong>LAST UPDATED:</strong> {{ current_time }}<span id="terminal-cursor"></span></p>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -57,6 +131,11 @@
|
|||||||
{% block congrats_message %}
|
{% block congrats_message %}
|
||||||
<div id="congratsMessage" style="display:none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; background: #f7931a; color: #000; padding: 10px; border-radius: 5px; box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);"></div>
|
<div id="congratsMessage" style="display:none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; background: #f7931a; color: #000; padding: 10px; border-radius: 5px; box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);"></div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<footer class="footer text-center">
|
||||||
|
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- External JavaScript libraries -->
|
<!-- External JavaScript libraries -->
|
||||||
@ -64,6 +143,32 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@1.1.0"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@1.1.0"></script>
|
||||||
|
|
||||||
|
<!-- Theme toggle initialization -->
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Initialize theme toggle button based on current theme
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
// Check current theme
|
||||||
|
const isDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
|
||||||
|
|
||||||
|
// Update button style based on theme
|
||||||
|
if (isDeepSea) {
|
||||||
|
themeToggle.style.borderColor = '#0088cc';
|
||||||
|
themeToggle.style.color = '#0088cc';
|
||||||
|
} else {
|
||||||
|
themeToggle.style.borderColor = '#f2a900';
|
||||||
|
themeToggle.style.color = '#f2a900';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click event listener
|
||||||
|
themeToggle.addEventListener('click', function () {
|
||||||
|
toggleTheme(); // This will now trigger a page refresh
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<!-- Page-specific JavaScript -->
|
<!-- Page-specific JavaScript -->
|
||||||
{% block javascript %}{% endblock %}
|
{% block javascript %}{% endblock %}
|
||||||
|
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD v 0.3{% endblock %}
|
{% block title %}BLOCKS - BTC-OS MINING DASHBOARD {% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/blocks.css">
|
<link rel="stylesheet" href="/static/css/blocks.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/theme-toggle.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}BLOCKCHAIN MONITOR v 0.1{% endblock %}
|
{% block header %}BLOCKCHAIN MONITOR v 0.1{% endblock %}
|
||||||
@ -63,7 +64,7 @@
|
|||||||
<div id="blocks-grid" class="blocks-grid">
|
<div id="blocks-grid" class="blocks-grid">
|
||||||
<!-- Blocks will be generated here via JavaScript -->
|
<!-- Blocks will be generated here via JavaScript -->
|
||||||
<div class="loader">
|
<div class="loader">
|
||||||
<span class="loader-text">Connecting to mempool.space API<span class="terminal-cursor"></span></span>
|
<span class="loader-text">Connecting to mempool.guide API<span class="terminal-cursor"></span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -6,170 +6,32 @@
|
|||||||
<title>Ocean.xyz Pool Miner - Initializing...</title>
|
<title>Ocean.xyz Pool Miner - Initializing...</title>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
|
||||||
<link rel="stylesheet" href="/static/css/boot.css">
|
<link rel="stylesheet" href="/static/css/boot.css">
|
||||||
<style>
|
<link rel="stylesheet" href="/static/css/theme-toggle.css">
|
||||||
/* Added styles for configuration form */
|
<!-- Add Theme JS -->
|
||||||
#config-form {
|
<script src="/static/js/theme.js"></script>
|
||||||
display: none;
|
|
||||||
margin-top: 20px;
|
|
||||||
background-color: rgba(0, 0, 0, 0.7);
|
|
||||||
border: 1px solid #f7931a;
|
|
||||||
padding: 15px;
|
|
||||||
max-width: 600px;
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: auto;
|
|
||||||
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-title {
|
|
||||||
color: #f7931a;
|
|
||||||
font-size: 22px;
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group {
|
|
||||||
margin-bottom: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 5px;
|
|
||||||
color: #f7931a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input {
|
|
||||||
width: 100%;
|
|
||||||
background-color: #111;
|
|
||||||
border: 1px solid #f7931a;
|
|
||||||
padding: 8px;
|
|
||||||
color: white;
|
|
||||||
font-family: 'VT323', monospace;
|
|
||||||
font-size: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-group input:focus {
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 5px #f7931a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
background-color: #f7931a;
|
|
||||||
border: none;
|
|
||||||
color: black;
|
|
||||||
padding: 8px 15px;
|
|
||||||
font-family: 'VT323', monospace;
|
|
||||||
font-size: 18px;
|
|
||||||
cursor: pointer;
|
|
||||||
min-width: 120px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn:hover {
|
|
||||||
background-color: #ffa642;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary {
|
|
||||||
background-color: #333;
|
|
||||||
color: #f7931a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-secondary:hover {
|
|
||||||
background-color: #444;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Make skip button more mobile-friendly */
|
|
||||||
#skip-button {
|
|
||||||
position: fixed;
|
|
||||||
bottom: 20px;
|
|
||||||
right: 20px;
|
|
||||||
z-index: 1000;
|
|
||||||
padding: 12px 20px;
|
|
||||||
font-size: 18px;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
#skip-button {
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
padding: 15px 25px;
|
|
||||||
font-size: 20px; /* Larger font size for better tap targets */
|
|
||||||
border-radius: 10px;
|
|
||||||
width: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
width: 100%;
|
|
||||||
padding: 12px;
|
|
||||||
font-size: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tooltip styles */
|
|
||||||
.tooltip {
|
|
||||||
position: relative;
|
|
||||||
display: inline-block;
|
|
||||||
margin-left: 5px;
|
|
||||||
cursor: help;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip .tooltip-text {
|
|
||||||
visibility: hidden;
|
|
||||||
width: 200px;
|
|
||||||
background-color: #000;
|
|
||||||
color: #fff;
|
|
||||||
text-align: center;
|
|
||||||
border: 1px solid #f7931a;
|
|
||||||
padding: 5px;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 1;
|
|
||||||
bottom: 125%;
|
|
||||||
left: 50%;
|
|
||||||
margin-left: -100px;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tooltip:hover .tooltip-text {
|
|
||||||
visibility: visible;
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Form success/error message */
|
|
||||||
#form-message {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 8px;
|
|
||||||
text-align: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-success {
|
|
||||||
background-color: rgba(50, 205, 50, 0.2);
|
|
||||||
border: 1px solid #32CD32;
|
|
||||||
color: #32CD32;
|
|
||||||
}
|
|
||||||
|
|
||||||
.message-error {
|
|
||||||
background-color: rgba(255, 0, 0, 0.2);
|
|
||||||
border: 1px solid #ff0000;
|
|
||||||
color: #ff0000;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<script>
|
||||||
|
// Add underwater effects for DeepSea theme
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Check if DeepSea theme is active
|
||||||
|
if (localStorage.getItem('useDeepSeaTheme') === 'true') {
|
||||||
|
// Create underwater light rays
|
||||||
|
const rays = document.createElement('div');
|
||||||
|
rays.className = 'underwater-rays';
|
||||||
|
document.body.appendChild(rays);
|
||||||
|
|
||||||
|
// Create digital noise
|
||||||
|
const noise = document.createElement('div');
|
||||||
|
noise.className = 'digital-noise';
|
||||||
|
document.body.appendChild(noise);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
<!-- Theme toggle button (new) -->
|
||||||
|
<button id="themeToggle" class="theme-toggle-btn">
|
||||||
|
<span>Toggle Theme</span>
|
||||||
|
</button>
|
||||||
<button id="skip-button">SKIP</button>
|
<button id="skip-button">SKIP</button>
|
||||||
<div id="debug-info"></div>
|
<div id="debug-info"></div>
|
||||||
<div id="loading-message">Loading mining data...</div>
|
<div id="loading-message">Loading mining data...</div>
|
||||||
@ -180,7 +42,7 @@
|
|||||||
██╔══██╗ ██║ ██║ ██║ ██║╚════██║
|
██╔══██╗ ██║ ██║ ██║ ██║╚════██║
|
||||||
██████╔╝ ██║ ╚██████╗ ╚██████╔╝███████║
|
██████╔╝ ██║ ╚██████╗ ╚██████╔╝███████║
|
||||||
╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
|
╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
|
||||||
v.21
|
v.21
|
||||||
</div>
|
</div>
|
||||||
<div id="terminal">
|
<div id="terminal">
|
||||||
<div id="terminal-content">
|
<div id="terminal-content">
|
||||||
@ -228,6 +90,40 @@
|
|||||||
</label>
|
</label>
|
||||||
<input type="number" id="power-usage" step="50" min="0" placeholder="13450" value="">
|
<input type="number" id="power-usage" step="50" min="0" placeholder="13450" value="">
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="network-fee">
|
||||||
|
Network Fee (%)
|
||||||
|
<span class="tooltip">
|
||||||
|
?
|
||||||
|
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input type="number" id="network-fee" step="0.1" min="0" max="10" placeholder="0.0" value="">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="timezone">
|
||||||
|
Timezone
|
||||||
|
<span class="tooltip">
|
||||||
|
?
|
||||||
|
<span class="tooltip-text">Your local timezone for displaying time information</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<select id="timezone" class="form-control">
|
||||||
|
<optgroup label="Common Timezones">
|
||||||
|
<option value="America/Los_Angeles">Los Angeles (Pacific Time)</option>
|
||||||
|
<option value="America/Denver">Denver (Mountain Time)</option>
|
||||||
|
<option value="America/Chicago">Chicago (Central Time)</option>
|
||||||
|
<option value="America/New_York">New York (Eastern Time)</option>
|
||||||
|
<option value="Europe/London">London (GMT/BST)</option>
|
||||||
|
<option value="Europe/Paris">Paris (Central European Time)</option>
|
||||||
|
<option value="Asia/Tokyo">Tokyo (Japan Standard Time)</option>
|
||||||
|
<option value="Australia/Sydney">Sydney (Australian Eastern Time)</option>
|
||||||
|
</optgroup>
|
||||||
|
<optgroup label="Other Timezones" id="other-timezones">
|
||||||
|
<!-- Will be populated by JavaScript -->
|
||||||
|
</optgroup>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
<div id="form-message"></div>
|
<div id="form-message"></div>
|
||||||
<div class="form-actions">
|
<div class="form-actions">
|
||||||
<button class="btn btn-secondary" id="use-defaults">Use Defaults</button>
|
<button class="btn btn-secondary" id="use-defaults">Use Defaults</button>
|
||||||
@ -236,6 +132,147 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Theme toggle initialization
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
// Initialize theme toggle button based on current theme
|
||||||
|
const themeToggle = document.getElementById('themeToggle');
|
||||||
|
if (themeToggle) {
|
||||||
|
// Check current theme
|
||||||
|
const isDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
|
||||||
|
|
||||||
|
// Update button style based on theme
|
||||||
|
if (isDeepSea) {
|
||||||
|
document.body.classList.add('deepsea-theme');
|
||||||
|
themeToggle.style.borderColor = '#0088cc';
|
||||||
|
themeToggle.style.color = '#0088cc';
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('deepsea-theme');
|
||||||
|
themeToggle.style.borderColor = '#f2a900';
|
||||||
|
themeToggle.style.color = '#f2a900';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click event listener
|
||||||
|
themeToggle.addEventListener('click', function () {
|
||||||
|
toggleTheme(); // This will now trigger a page refresh
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update terminal colors based on theme (boot.html specific)
|
||||||
|
function updateTerminalColors() {
|
||||||
|
const isDeepSeaTheme = localStorage.getItem('useDeepSeaTheme') === 'true';
|
||||||
|
if (isDeepSeaTheme) {
|
||||||
|
document.body.classList.add('deepsea-theme');
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('deepsea-theme');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize terminal colors
|
||||||
|
updateTerminalColors();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add a function to populate all available timezones
|
||||||
|
function populateTimezones() {
|
||||||
|
const otherTimezones = document.getElementById('other-timezones');
|
||||||
|
|
||||||
|
// Common timezone areas to include
|
||||||
|
const commonAreas = [
|
||||||
|
'Africa', 'America', 'Antarctica', 'Asia', 'Atlantic',
|
||||||
|
'Australia', 'Europe', 'Indian', 'Pacific'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Fetch the list of available timezones
|
||||||
|
fetch('/api/available_timezones')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (!data.timezones || !Array.isArray(data.timezones)) {
|
||||||
|
console.error('Invalid timezone data received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort timezones and filter to include only common areas
|
||||||
|
const sortedTimezones = data.timezones
|
||||||
|
.filter(tz => commonAreas.some(area => tz.startsWith(area + '/')))
|
||||||
|
.sort();
|
||||||
|
|
||||||
|
// Add options for each timezone (excluding those already in common list)
|
||||||
|
const commonOptions = Array.from(document.querySelectorAll('#timezone optgroup:first-child option'))
|
||||||
|
.map(opt => opt.value);
|
||||||
|
|
||||||
|
sortedTimezones.forEach(tz => {
|
||||||
|
if (!commonOptions.includes(tz)) {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = tz;
|
||||||
|
option.textContent = tz.replace('_', ' ');
|
||||||
|
otherTimezones.appendChild(option);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => console.error('Error fetching timezones:', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this when the page loads
|
||||||
|
document.addEventListener('DOMContentLoaded', populateTimezones);
|
||||||
|
|
||||||
|
// Load the current timezone from configuration
|
||||||
|
function loadTimezoneFromConfig() {
|
||||||
|
if (currentConfig && currentConfig.timezone) {
|
||||||
|
const timezoneSelect = document.getElementById('timezone');
|
||||||
|
|
||||||
|
// First, check if the option exists
|
||||||
|
let optionExists = false;
|
||||||
|
for (let i = 0; i < timezoneSelect.options.length; i++) {
|
||||||
|
if (timezoneSelect.options[i].value === currentConfig.timezone) {
|
||||||
|
timezoneSelect.selectedIndex = i;
|
||||||
|
optionExists = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the option doesn't exist yet (might be in the 'other' group being loaded)
|
||||||
|
// set a data attribute to select it when options are loaded
|
||||||
|
if (!optionExists) {
|
||||||
|
timezoneSelect.setAttribute('data-select-value', currentConfig.timezone);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call this after loading config
|
||||||
|
loadConfig().then(() => {
|
||||||
|
loadTimezoneFromConfig();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update saveConfig to include network fee
|
||||||
|
function saveConfig() {
|
||||||
|
const wallet = document.getElementById('wallet-address').value.trim();
|
||||||
|
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
|
||||||
|
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
|
||||||
|
const timezone = document.getElementById('timezone').value;
|
||||||
|
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
|
||||||
|
|
||||||
|
const updatedConfig = {
|
||||||
|
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
|
||||||
|
power_cost: powerCost,
|
||||||
|
power_usage: powerUsage,
|
||||||
|
timezone: timezone,
|
||||||
|
network_fee: networkFee
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch('/api/config', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify(updatedConfig)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save configuration');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Debug logging
|
// Debug logging
|
||||||
function updateDebug(message) {
|
function updateDebug(message) {
|
||||||
document.getElementById('debug-info').textContent = message;
|
document.getElementById('debug-info').textContent = message;
|
||||||
@ -269,40 +306,45 @@
|
|||||||
power_usage: 0.0
|
power_usage: 0.0
|
||||||
};
|
};
|
||||||
|
|
||||||
// Replace the current loadConfig function with this improved version
|
// Update loadConfig function to include network fee
|
||||||
function loadConfig() {
|
function loadConfig() {
|
||||||
// Always make a fresh request to get the latest config
|
return new Promise((resolve, reject) => {
|
||||||
fetch('/api/config?nocache=' + new Date().getTime()) // Add cache-busting parameter
|
fetch('/api/config?nocache=' + new Date().getTime())
|
||||||
.then(response => {
|
.then(response => {
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error('Failed to load configuration: ' + response.statusText);
|
throw new Error('Failed to load configuration: ' + response.statusText);
|
||||||
}
|
}
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
console.log("Loaded configuration:", data);
|
console.log("Loaded configuration:", data);
|
||||||
currentConfig = data;
|
currentConfig = data;
|
||||||
|
|
||||||
// After loading, always update the form fields with the latest values
|
// Update form fields with latest values
|
||||||
document.getElementById('wallet-address').value = currentConfig.wallet || "";
|
document.getElementById('wallet-address').value = currentConfig.wallet || "";
|
||||||
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
||||||
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
||||||
configLoaded = true;
|
document.getElementById('network-fee').value = currentConfig.network_fee || "";
|
||||||
})
|
configLoaded = true;
|
||||||
.catch(err => {
|
resolve(currentConfig);
|
||||||
console.error("Error loading config:", err);
|
})
|
||||||
// Use default values if loading fails
|
.catch(err => {
|
||||||
currentConfig = {
|
console.error("Error loading config:", err);
|
||||||
wallet: "yourwallethere",
|
// Use default values if loading fails
|
||||||
power_cost: 0.0,
|
currentConfig = {
|
||||||
power_usage: 0.0
|
wallet: "yourwallethere",
|
||||||
};
|
power_cost: 0.0,
|
||||||
|
power_usage: 0.0,
|
||||||
|
network_fee: 0.0
|
||||||
|
};
|
||||||
|
|
||||||
// Still update the form with default values
|
document.getElementById('wallet-address').value = currentConfig.wallet || "";
|
||||||
document.getElementById('wallet-address').value = currentConfig.wallet || "";
|
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
||||||
document.getElementById('power-cost').value = currentConfig.power_cost || "";
|
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
||||||
document.getElementById('power-usage').value = currentConfig.power_usage || "";
|
document.getElementById('network-fee').value = currentConfig.network_fee || "";
|
||||||
});
|
resolve(currentConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also update the save button event handler to reload the config after saving
|
// Also update the save button event handler to reload the config after saving
|
||||||
@ -328,33 +370,6 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Save configuration
|
|
||||||
function saveConfig() {
|
|
||||||
const wallet = document.getElementById('wallet-address').value.trim();
|
|
||||||
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
|
|
||||||
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
|
|
||||||
|
|
||||||
const updatedConfig = {
|
|
||||||
wallet: wallet || currentConfig.wallet,
|
|
||||||
power_cost: powerCost,
|
|
||||||
power_usage: powerUsage
|
|
||||||
};
|
|
||||||
|
|
||||||
return fetch('/api/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(updatedConfig)
|
|
||||||
})
|
|
||||||
.then(response => {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to save configuration');
|
|
||||||
}
|
|
||||||
return response.json();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Safety timeout: redirect after 120 seconds if boot not complete
|
// Safety timeout: redirect after 120 seconds if boot not complete
|
||||||
window.addEventListener('load', function () {
|
window.addEventListener('load', function () {
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
@ -384,29 +399,19 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Replace the current Use Defaults button event listener with this fixed version
|
// Update Use Defaults button handler
|
||||||
document.getElementById('use-defaults').addEventListener('click', function () {
|
document.getElementById('use-defaults').addEventListener('click', function () {
|
||||||
console.log("Use Defaults button clicked");
|
// 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;
|
||||||
|
|
||||||
// Always use the hardcoded default values, not the currentConfig
|
// Visual feedback
|
||||||
const defaultWallet = "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9";
|
|
||||||
const defaultPowerCost = 0.0;
|
|
||||||
const defaultPowerUsage = 0.0;
|
|
||||||
|
|
||||||
console.log("Setting to default values");
|
|
||||||
|
|
||||||
// Apply the hardcoded default values to the form fields
|
|
||||||
document.getElementById('wallet-address').value = defaultWallet;
|
|
||||||
document.getElementById('power-cost').value = defaultPowerCost;
|
|
||||||
document.getElementById('power-usage').value = defaultPowerUsage;
|
|
||||||
|
|
||||||
// Show visual feedback that the button was clicked
|
|
||||||
const btn = document.getElementById('use-defaults');
|
const btn = document.getElementById('use-defaults');
|
||||||
const originalText = btn.textContent;
|
const originalText = btn.textContent;
|
||||||
btn.textContent = "Defaults Applied";
|
btn.textContent = "Defaults Applied";
|
||||||
btn.style.backgroundColor = "#32CD32";
|
btn.style.backgroundColor = "#32CD32";
|
||||||
|
|
||||||
// Reset the button after a short delay
|
|
||||||
setTimeout(function () {
|
setTimeout(function () {
|
||||||
btn.textContent = originalText;
|
btn.textContent = originalText;
|
||||||
btn.style.backgroundColor = "";
|
btn.style.backgroundColor = "";
|
||||||
@ -498,10 +503,14 @@
|
|||||||
}
|
}
|
||||||
setTimeout(processNextMessage, 500);
|
setTimeout(processNextMessage, 500);
|
||||||
} else {
|
} else {
|
||||||
// If user selects 'N', just redirect to dashboard
|
// If user selects 'N', show configuration form directly without boot messages
|
||||||
outputElement.innerHTML += "N\n\nDASHBOARD INITIALIZATION ABORTED.\n";
|
outputElement.innerHTML += "N\n\nDASHBOARD INITIALIZATION ABORTED.\n";
|
||||||
outputElement.innerHTML += "\nUsing default configuration values.\n";
|
outputElement.innerHTML += "\nPlease configure your mining setup:\n";
|
||||||
setTimeout(redirectToDashboard, 2000);
|
|
||||||
|
// Short pause and then show the configuration form
|
||||||
|
setTimeout(function () {
|
||||||
|
document.getElementById('config-form').style.display = 'block';
|
||||||
|
}, 1000);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setTimeout(redirectToDashboard, 1000);
|
setTimeout(redirectToDashboard, 1000);
|
||||||
@ -598,7 +607,7 @@
|
|||||||
// Fallback messages (used immediately)
|
// Fallback messages (used immediately)
|
||||||
function setupFallbackMessages() {
|
function setupFallbackMessages() {
|
||||||
bootMessages = [
|
bootMessages = [
|
||||||
{ text: "BITCOIN OS - MINING SYSTEM - v21.000.000\n", speed: 25, delay: 300 },
|
{ text: "BITCOIN OS - MINING CONTROL SYSTEM - v21.000.000\n", speed: 25, delay: 300 },
|
||||||
{ text: "Copyright (c) 2009-2025 Satoshi Nakamoto\n", speed: 20, delay: 250 },
|
{ text: "Copyright (c) 2009-2025 Satoshi Nakamoto\n", speed: 20, delay: 250 },
|
||||||
{ text: "All rights reserved.\n\n", speed: 25, delay: 300 },
|
{ text: "All rights reserved.\n\n", speed: 25, delay: 300 },
|
||||||
{ text: "INITIALIZING SYSTEM...\n", speed: 25, delay: 300 },
|
{ text: "INITIALIZING SYSTEM...\n", speed: 25, delay: 300 },
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}BTC-OS Mining Dashboard v 0.3{% endblock %}
|
{% block title %}BTC-OS Mining Dashboard {% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/dashboard.css">
|
<link rel="stylesheet" href="/static/css/dashboard.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/theme-toggle.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block dashboard_active %}active{% endblock %}
|
{% block dashboard_active %}active{% endblock %}
|
||||||
@ -40,18 +41,11 @@
|
|||||||
<span id="last_share" class="metric-value">{{ metrics.total_last_share or "N/A" }}</span>
|
<span id="last_share" class="metric-value">{{ metrics.total_last_share or "N/A" }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Pool Fees:</strong>
|
<strong>Blocks Found:</strong>
|
||||||
<span id="pool_fees_percentage" class="metric-value">
|
<span id="blocks_found" class="metric-value white">
|
||||||
{% if metrics and metrics.pool_fees_percentage is defined %}
|
{{ metrics.blocks_found if metrics and metrics.blocks_found else "0" }}
|
||||||
{{ metrics.pool_fees_percentage }}%
|
|
||||||
{% if metrics.pool_fees_percentage >= 0.9 and metrics.pool_fees_percentage <= 1.3 %}
|
|
||||||
<span class="fee-star">★</span> <span class="datum-label">DATUM</span>
|
|
||||||
{% endif %}
|
|
||||||
{% else %}
|
|
||||||
N/A
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
</span>
|
||||||
<span id="indicator_pool_fees_percentage"></span>
|
<span id="indicator_blocks_found"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -99,11 +93,18 @@
|
|||||||
<span id="indicator_est_time_to_payout"></span>
|
<span id="indicator_est_time_to_payout"></span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Blocks Found:</strong>
|
<strong>Pool Fees:</strong>
|
||||||
<span id="blocks_found" class="metric-value white">
|
<span id="pool_fees_percentage" class="metric-value">
|
||||||
{{ metrics.blocks_found if metrics and metrics.blocks_found else "0" }}
|
{% if metrics and metrics.pool_fees_percentage is defined and metrics.pool_fees_percentage is not none %}
|
||||||
|
{{ metrics.pool_fees_percentage }}%
|
||||||
|
{% if metrics.pool_fees_percentage is not none and metrics.pool_fees_percentage >= 0.9 and metrics.pool_fees_percentage <= 1.3 %}
|
||||||
|
<span class="fee-star">★</span> <span class="datum-label">DATUM</span> <span class="fee-star">★</span>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<span id="indicator_blocks_found"></span>
|
<span id="indicator_pool_fees_percentage"></span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -117,7 +118,7 @@
|
|||||||
<div class="card-header">Pool Hashrates</div>
|
<div class="card-header">Pool Hashrates</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
<strong>Pool Total Hashrate:</strong>
|
<strong>Pool Hashrate:</strong>
|
||||||
<span id="pool_total_hashrate" class="metric-value white">
|
<span id="pool_total_hashrate" class="metric-value white">
|
||||||
{% if metrics and metrics.pool_total_hashrate and metrics.pool_total_hashrate_unit %}
|
{% if metrics and metrics.pool_total_hashrate and metrics.pool_total_hashrate_unit %}
|
||||||
{{ metrics.pool_total_hashrate }} {{ metrics.pool_total_hashrate_unit[:-2]|upper ~ metrics.pool_total_hashrate_unit[-2:] }}
|
{{ metrics.pool_total_hashrate }} {{ metrics.pool_total_hashrate_unit[:-2]|upper ~ metrics.pool_total_hashrate_unit[-2:] }}
|
||||||
@ -200,17 +201,6 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">Network Stats</div>
|
<div class="card-header">Network Stats</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
|
||||||
<strong>Block Number:</strong>
|
|
||||||
<span id="block_number" class="metric-value white">
|
|
||||||
{% if metrics and metrics.block_number %}
|
|
||||||
{{ metrics.block_number|commafy }}
|
|
||||||
{% else %}
|
|
||||||
N/A
|
|
||||||
{% endif %}
|
|
||||||
</span>
|
|
||||||
<span id="indicator_block_number"></span>
|
|
||||||
</p>
|
|
||||||
<p>
|
<p>
|
||||||
<strong>BTC Price:</strong>
|
<strong>BTC Price:</strong>
|
||||||
<span id="btc_price" class="metric-value yellow">
|
<span id="btc_price" class="metric-value yellow">
|
||||||
@ -222,6 +212,17 @@
|
|||||||
</span>
|
</span>
|
||||||
<span id="indicator_btc_price"></span>
|
<span id="indicator_btc_price"></span>
|
||||||
</p>
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Block Number:</strong>
|
||||||
|
<span id="block_number" class="metric-value white">
|
||||||
|
{% if metrics and metrics.block_number %}
|
||||||
|
{{ metrics.block_number|commafy }}
|
||||||
|
{% else %}
|
||||||
|
N/A
|
||||||
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<span id="indicator_block_number"></span>
|
||||||
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Network Hashrate:</strong>
|
<strong>Network Hashrate:</strong>
|
||||||
<span id="network_hashrate" class="metric-value white">
|
<span id="network_hashrate" class="metric-value white">
|
||||||
@ -256,7 +257,7 @@
|
|||||||
<div class="card-header">SATOSHI EARNINGS</div>
|
<div class="card-header">SATOSHI EARNINGS</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>
|
<p>
|
||||||
<strong>Daily Mined (Net):</strong>
|
<strong>Projected Daily (Net):</strong>
|
||||||
<span id="daily_mined_sats" class="metric-value yellow">
|
<span id="daily_mined_sats" class="metric-value yellow">
|
||||||
{% if metrics and metrics.daily_mined_sats %}
|
{% if metrics and metrics.daily_mined_sats %}
|
||||||
{{ metrics.daily_mined_sats|commafy }} SATS
|
{{ metrics.daily_mined_sats|commafy }} SATS
|
||||||
@ -267,7 +268,7 @@
|
|||||||
<span id="indicator_daily_mined_sats"></span>
|
<span id="indicator_daily_mined_sats"></span>
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
<strong>Monthly Mined (Net):</strong>
|
<strong>Projected Monthly (Net):</strong>
|
||||||
<span id="monthly_mined_sats" class="metric-value yellow">
|
<span id="monthly_mined_sats" class="metric-value yellow">
|
||||||
{% if metrics and metrics.monthly_mined_sats %}
|
{% if metrics and metrics.monthly_mined_sats %}
|
||||||
{{ metrics.monthly_mined_sats|commafy }} SATS
|
{{ metrics.monthly_mined_sats|commafy }} SATS
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD v 0.3{% endblock %}
|
{% block title %}NOTIFICATIONS - BTC-OS MINING DASHBOARD {% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/notifications.css">
|
<link rel="stylesheet" href="/static/css/notifications.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/theme-toggle.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}NOTIFICATION CENTER v 0.1{% endblock %}
|
{% block header %}NOTIFICATION CENTER v 0.1{% endblock %}
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
|
|
||||||
{% block title %}WORKERS - BTC-OS MINING DASHBOARD v 0.3{% endblock %}
|
{% block title %}WORKERS - BTC-OS MINING DASHBOARD {% endblock %}
|
||||||
|
|
||||||
{% block css %}
|
{% block css %}
|
||||||
<link rel="stylesheet" href="/static/css/workers.css">
|
<link rel="stylesheet" href="/static/css/workers.css">
|
||||||
|
<link rel="stylesheet" href="/static/css/theme-toggle.css">
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block header %}WORKERS OVERVIEW{% endblock %}
|
{% block header %}WORKERS OVERVIEW{% endblock %}
|
||||||
@ -66,17 +67,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="summary-stat-label">DAILY SATS</div>
|
<div class="summary-stat-label">DAILY SATS</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="summary-stat">
|
|
||||||
<div class="summary-stat-value white-glow" id="avg-acceptance-rate">
|
|
||||||
{% if avg_acceptance_rate is defined %}
|
|
||||||
{{ "%.2f"|format(avg_acceptance_rate) }}%
|
|
||||||
{% else %}
|
|
||||||
N/A
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<div class="summary-stat-label">ACCEPTANCE</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -5,6 +5,7 @@ import logging
|
|||||||
import random
|
import random
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from zoneinfo import ZoneInfo
|
from zoneinfo import ZoneInfo
|
||||||
|
from config import get_timezone
|
||||||
|
|
||||||
class WorkerService:
|
class WorkerService:
|
||||||
"""Service for retrieving and managing worker data."""
|
"""Service for retrieving and managing worker data."""
|
||||||
@ -47,9 +48,8 @@ class WorkerService:
|
|||||||
"hashrate_unit": "TH/s",
|
"hashrate_unit": "TH/s",
|
||||||
"total_earnings": 0.0,
|
"total_earnings": 0.0,
|
||||||
"daily_sats": 0,
|
"daily_sats": 0,
|
||||||
"avg_acceptance_rate": 0.0,
|
|
||||||
"hashrate_history": [],
|
"hashrate_history": [],
|
||||||
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_workers_data(self, cached_metrics, force_refresh=False):
|
def get_workers_data(self, cached_metrics, force_refresh=False):
|
||||||
@ -286,7 +286,7 @@ class WorkerService:
|
|||||||
dict: Default worker data
|
dict: Default worker data
|
||||||
"""
|
"""
|
||||||
is_online = status == "online"
|
is_online = status == "online"
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
|
current_time = datetime.now(ZoneInfo(get_timezone()))
|
||||||
|
|
||||||
# Generate some reasonable hashrate and other values
|
# Generate some reasonable hashrate and other values
|
||||||
hashrate = round(random.uniform(50, 100), 2) if is_online else 0
|
hashrate = round(random.uniform(50, 100), 2) if is_online else 0
|
||||||
@ -306,7 +306,6 @@ class WorkerService:
|
|||||||
"efficiency": round(random.uniform(80, 95), 1) if is_online else 0,
|
"efficiency": round(random.uniform(80, 95), 1) if is_online else 0,
|
||||||
"last_share": last_share,
|
"last_share": last_share,
|
||||||
"earnings": round(random.uniform(0.0001, 0.001), 8),
|
"earnings": round(random.uniform(0.0001, 0.001), 8),
|
||||||
"acceptance_rate": round(random.uniform(95, 99), 1),
|
|
||||||
"power_consumption": round(random.uniform(2000, 3500)) if is_online else 0,
|
"power_consumption": round(random.uniform(2000, 3500)) if is_online else 0,
|
||||||
"temperature": round(random.uniform(55, 75)) if is_online else 0
|
"temperature": round(random.uniform(55, 75)) if is_online else 0
|
||||||
}
|
}
|
||||||
@ -315,10 +314,10 @@ class WorkerService:
|
|||||||
"""
|
"""
|
||||||
Generate fallback worker data from cached metrics when real data can't be fetched.
|
Generate fallback worker data from cached metrics when real data can't be fetched.
|
||||||
Try to preserve real worker names if available.
|
Try to preserve real worker names if available.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
cached_metrics (dict): Cached metrics from the dashboard
|
cached_metrics (dict): Cached metrics from the dashboard
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Generated worker data
|
dict: Generated worker data
|
||||||
"""
|
"""
|
||||||
@ -326,12 +325,16 @@ class WorkerService:
|
|||||||
if not cached_metrics:
|
if not cached_metrics:
|
||||||
logging.warning("No cached metrics available for worker fallback data")
|
logging.warning("No cached metrics available for worker fallback data")
|
||||||
return self.generate_default_workers_data()
|
return self.generate_default_workers_data()
|
||||||
|
|
||||||
# Check if we have workers_hashing information
|
# Check if we have workers_hashing information
|
||||||
workers_count = cached_metrics.get("workers_hashing", 0)
|
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
|
||||||
# Force at least 1 worker if the count is 0
|
# Force at least 1 worker if the count is 0
|
||||||
if workers_count <= 0:
|
elif workers_count <= 0:
|
||||||
logging.warning("No workers reported in metrics, forcing 1 worker")
|
logging.warning("No workers reported in metrics, forcing 1 worker")
|
||||||
workers_count = 1
|
workers_count = 1
|
||||||
|
|
||||||
@ -436,9 +439,8 @@ class WorkerService:
|
|||||||
"hashrate_unit": hashrate_unit,
|
"hashrate_unit": hashrate_unit,
|
||||||
"total_earnings": total_earnings,
|
"total_earnings": total_earnings,
|
||||||
"daily_sats": daily_sats, # Fixed daily_sats value
|
"daily_sats": daily_sats, # Fixed daily_sats value
|
||||||
"avg_acceptance_rate": 98.8, # Default value
|
|
||||||
"hashrate_history": hashrate_history,
|
"hashrate_history": hashrate_history,
|
||||||
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
|
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
|
||||||
}
|
}
|
||||||
|
|
||||||
# Update cache
|
# Update cache
|
||||||
@ -482,7 +484,7 @@ class WorkerService:
|
|||||||
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
|
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
|
||||||
|
|
||||||
workers = []
|
workers = []
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
|
current_time = datetime.now(ZoneInfo(get_timezone()))
|
||||||
|
|
||||||
# Default total unpaid earnings if not provided
|
# Default total unpaid earnings if not provided
|
||||||
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
|
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
|
||||||
@ -509,10 +511,7 @@ class WorkerService:
|
|||||||
# Generate last share time (within last 5 minutes)
|
# Generate last share time (within last 5 minutes)
|
||||||
minutes_ago = random.randint(0, 5)
|
minutes_ago = random.randint(0, 5)
|
||||||
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
|
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
# Generate acceptance rate (95-100%)
|
|
||||||
acceptance_rate = round(random.uniform(95, 100), 1)
|
|
||||||
|
|
||||||
# Generate temperature (normal operating range)
|
# Generate temperature (normal operating range)
|
||||||
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
|
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
|
||||||
|
|
||||||
@ -531,7 +530,6 @@ class WorkerService:
|
|||||||
"efficiency": round(random.uniform(65, 95), 1),
|
"efficiency": round(random.uniform(65, 95), 1),
|
||||||
"last_share": last_share,
|
"last_share": last_share,
|
||||||
"earnings": 0, # Will be set after all workers are generated
|
"earnings": 0, # Will be set after all workers are generated
|
||||||
"acceptance_rate": acceptance_rate,
|
|
||||||
"power_consumption": model_info["power"],
|
"power_consumption": model_info["power"],
|
||||||
"temperature": temperature
|
"temperature": temperature
|
||||||
})
|
})
|
||||||
@ -570,7 +568,6 @@ class WorkerService:
|
|||||||
"efficiency": 0,
|
"efficiency": 0,
|
||||||
"last_share": last_share,
|
"last_share": last_share,
|
||||||
"earnings": 0, # Minimal earnings for offline workers
|
"earnings": 0, # Minimal earnings for offline workers
|
||||||
"acceptance_rate": round(random.uniform(95, 99), 1),
|
|
||||||
"power_consumption": 0,
|
"power_consumption": 0,
|
||||||
"temperature": 0
|
"temperature": 0
|
||||||
})
|
})
|
||||||
@ -635,7 +632,7 @@ class WorkerService:
|
|||||||
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
|
avg_hashrate = max(0.5, total_hashrate / online_count if online_count > 0 else 0)
|
||||||
|
|
||||||
workers = []
|
workers = []
|
||||||
current_time = datetime.now(ZoneInfo("America/Los_Angeles"))
|
current_time = datetime.now(ZoneInfo(get_timezone()))
|
||||||
|
|
||||||
# Default total unpaid earnings if not provided
|
# Default total unpaid earnings if not provided
|
||||||
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
|
if total_unpaid_earnings is None or total_unpaid_earnings <= 0:
|
||||||
@ -672,9 +669,6 @@ class WorkerService:
|
|||||||
minutes_ago = random.randint(0, 3)
|
minutes_ago = random.randint(0, 3)
|
||||||
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
|
last_share = (current_time - timedelta(minutes=minutes_ago)).strftime("%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
# Generate acceptance rate (95-100%)
|
|
||||||
acceptance_rate = round(random.uniform(95, 100), 1)
|
|
||||||
|
|
||||||
# Generate temperature (normal operating range)
|
# Generate temperature (normal operating range)
|
||||||
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
|
temperature = random.randint(55, 70) if model_info["type"] == "ASIC" else random.randint(45, 55)
|
||||||
|
|
||||||
@ -700,7 +694,6 @@ class WorkerService:
|
|||||||
"efficiency": round(random.uniform(65, 95), 1),
|
"efficiency": round(random.uniform(65, 95), 1),
|
||||||
"last_share": last_share,
|
"last_share": last_share,
|
||||||
"earnings": 0, # Will be set after all workers are generated
|
"earnings": 0, # Will be set after all workers are generated
|
||||||
"acceptance_rate": acceptance_rate,
|
|
||||||
"power_consumption": model_info["power"],
|
"power_consumption": model_info["power"],
|
||||||
"temperature": temperature
|
"temperature": temperature
|
||||||
})
|
})
|
||||||
@ -746,7 +739,6 @@ class WorkerService:
|
|||||||
"efficiency": 0,
|
"efficiency": 0,
|
||||||
"last_share": last_share,
|
"last_share": last_share,
|
||||||
"earnings": 0, # Minimal earnings for offline workers
|
"earnings": 0, # Minimal earnings for offline workers
|
||||||
"acceptance_rate": round(random.uniform(95, 99), 1),
|
|
||||||
"power_consumption": 0,
|
"power_consumption": 0,
|
||||||
"temperature": 0
|
"temperature": 0
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user