Compare commits

...

55 Commits
v0.4 ... main

Author SHA1 Message Date
DJObleezy
b9c04a39e8 Enhance pool fees display in dashboard
Updated the star rating display for pool fees in `dashboard.html`. When `metrics.pool_fees_percentage` is between 0.9 and 1.3, the number of stars shown has been increased from one to three, improving the visual representation of the metric.
2025-04-23 23:05:29 -07:00
DJObleezy
6ba7545278 Remove text-shadow for cleaner UI across stylesheets
This commit removes `text-shadow` properties from various CSS classes in `blocks.css`, `boot.css`, `common.css`, `dashboard.css`, `error.css`, `retro-refresh.css`, and `workers.css`, enhancing readability and reducing visual clutter.

In `main.js`, the inline styles for profit value elements are updated to eliminate `text-shadow`, simplifying styling logic.

Additionally, `theme.js` sees the removal of `text-shadow` from headers and interface elements, with adjustments made to box-shadow values for improved visual depth.

These changes reflect a design decision to create a more modern and streamlined interface.
2025-04-23 23:00:06 -07:00
DJObleezy
50b5241812 Remove version number from page titles
Updated titles in base.html, blocks.html, dashboard.html,
notifications.html, and workers.html to eliminate the
version number "v 0.3", creating a more concise and
uniform appearance across the application.
2025-04-23 22:03:29 -07:00
DJObleezy
54957babc3 Enhance services and improve code structure
- Added health check for Redis in `docker-compose.yml`.
- Introduced new environment variables for the dashboard service.
- Updated Redis dependency condition for the dashboard service.
- Modified Dockerfile to use Python 3.9.18 and streamlined directory creation.
- Enhanced `minify.py` with logging and improved error handling.
- Added methods in `OceanData` and `WorkerData` for better data handling.
- Improved error handling and logging in `NotificationService`.
- Refactored `BitcoinProgressBar.js` for better organization and theme support.
- Updated `blocks.js` with new helper functions for block data management.
- Enhanced `dashboard.html` for improved display of network stats.
2025-04-23 21:56:25 -07:00
DJObleezy
4c4750cb24 Enhance README.md with new features and API endpoints
Updated README.md to include:
- New **Error Handling** section for user-friendly error pages.
- Introduction of the **DeepSea Theme** with immersive effects and a toggle option.
- Added environment variables `NETWORK_FEE` and `TIMEZONE` in `docker-compose.yml`.
- New API endpoints for metrics, timezones, configuration, and health status.

These changes improve user experience, configuration options, and application resilience.
2025-04-23 20:06:14 -07:00
DJObleezy
2021583951 Add DeepSea theme styles and effects
Implemented new CSS styles for the DeepSea theme in `boot.css`, enhancing visual elements like colors, shadows, and animations for various components. Updated `common.css` to include a footer style and a theme loader for improved user experience. Removed inline styles from `base.html` and replaced them with references to the new styles. Added JavaScript to create dynamic underwater effects when the DeepSea theme is active. Updated footer to include a link to Ocean.xyz.
2025-04-23 20:00:27 -07:00
DJObleezy
00d3bbdee9 Update deployment instructions and chart indicator style
- Changed repository cloning instructions to use `DeepSea-Dashboard`.
- Adjusted the position of the `lowHashrateIndicator` in `main.js` from bottom to top right.
- Added a background color to the indicator for improved visibility.
2025-04-23 18:47:50 -07:00
DJObleezy
3ebef744dc
Update deployment_steps.md 2025-04-23 14:33:20 -07:00
DJObleezy
f302d35945
Update README.md 2025-04-23 14:32:24 -07:00
DJObleezy
28099c37ec
Update README.md 2025-04-23 14:31:03 -07:00
DJObleezy
2cb166b405
Update README.md 2025-04-23 14:28:15 -07:00
DJObleezy
259c877f69 Update project structure and enhance documentation
Added new services: `notification_service.py`, `minify.py`, and `theme-toggle.css`. Renamed `config.json` for clarity and updated `workers.html` to `workers dashboard.html`. Introduced `LICENSE.md` and improved project structure documentation. Adjusted formatting in the "Component Interactions" diagram for consistency.
2025-04-23 14:23:07 -07:00
DJObleezy
3d62c42bbf Update project structure with new services and files
Added notification_service.py and minify.py for enhanced functionality. Introduced docker-compose.yml for easier orchestration. Updated templates with notifications.html and added theme-toggle.css and notifications.js in static assets. Retained existing styles and functionalities. Moved project_structure.md, added LICENSE.md, and created logs/ directory for runtime logs.
2025-04-23 14:20:19 -07:00
DJObleezy
076fba75c8 Update README with new config.json URL
Changed the link to the configuration file in README.md
to point to the new repository for the Deepsea Dashboard.
This reflects the project's renaming and ensures users
access the correct configuration settings.
2025-04-23 14:16:46 -07:00
DJObleezy
a3acee1782 Enhance z-index and mobile styles for UI elements
- Added `z-index` to `body::before` for proper stacking.
- Implemented mobile-specific styles for `#skip-button`.
- Established higher `z-index` for `#config-form` with relative positioning.
2025-04-23 14:12:12 -07:00
DJObleezy
a71a6ce03a Add theme change listener and first startup handling
- Implement `setupThemeChangeListener` in `main.js` to detect theme changes across tabs, save font configurations, and recreate the chart with appropriate styles for mobile and desktop.
- Introduce new functions in `theme.js` to manage theme preferences on first startup, including setting the DeepSea theme as default and checking for previous app launches.
2025-04-23 13:55:19 -07:00
DJObleezy
f617342c23 Add theme loading styles and improve theme management
Updated `theme-toggle.css` with new styles for DeepSea and Bitcoin themes, including a loading screen. Introduced `isApplyingTheme` flag in `main.js` to manage theme application state. Modified `applyDeepSeaTheme` and `toggleTheme` functions in `theme.js` to enhance theme switching experience with dynamic loading messages. Enhanced `base.html` to preload styles and prevent flickering during theme transitions.
2025-04-23 13:26:07 -07:00
DJObleezy
2b09ad6c15 Add DeepSea Theme with ocean effects and glitch animations
Implemented CSS styles and animations for an "Ocean Wave Ripple Effect" and a "Retro Glitch Effect" in the new "DeepSea Theme". Added keyframes, background images, and opacity settings to create underwater light rays and digital noise. Included JavaScript to dynamically generate elements for these effects when the theme is active, enhancing user experience.
2025-04-23 10:12:44 -07:00
DJObleezy
f8514eb35f Changed default wallet
Update wallet address and improve user flow

- Changed the `WALLET` environment variable in `docker-compose.yml` to a new Bitcoin address.
- Updated the wallet address in the "Use Defaults" button handler in `boot.html`.
- Modified the logic for user selection of 'N' to display the configuration form directly instead of redirecting to the dashboard.
- Updated fallback messages to reflect the new title "MINING CONTROL SYSTEM".
2025-04-23 10:01:53 -07:00
DJObleezy
f2ddcdd63a
Update README.md 2025-04-23 09:55:04 -07:00
DJObleezy
f1bf5d0582
Update README.md 2025-04-23 09:54:19 -07:00
DJObleezy
5770c96bf7
Update README.md 2025-04-23 08:46:03 -07:00
DJObleezy
a02837b28c
Update README.md 2025-04-23 08:12:38 -07:00
DJObleezy
41883f3f9c Enhance Bitcoin logo styling in DeepSea theme
Added fixed height and flexbox properties to center the Bitcoin logo. Adjusted positioning of the DeepSea ASCII art for perfect centering within the logo area.
2025-04-23 07:40:16 -07:00
DJObleezy
cb24f54685 Shorten version info in deepsea theme
Updated the `#bitcoin-logo::before` pseudo-element to change the displayed version text from "DeepSea v.21" to "v.21", simplifying the version information.
2025-04-23 07:31:44 -07:00
DJObleezy
e02622d600 Enhance Bitcoin logo styling in DeepSea theme
- Added base styling for the Bitcoin logo with positioning and font settings.
- Updated logo styling to hide the original and accommodate new height.
- Introduced ASCII art for the Bitcoin logo with enhanced visual effects.
- Added "DeepSea" version info label for better branding visibility.
2025-04-23 07:26:40 -07:00
DJObleezy
a802880011 Enhance theme styling and mobile responsiveness
Updated `theme-toggle.css` for improved mobile styling of the theme toggle button, increasing padding and width. Reorganized CSS variables in `theme.js` for the DeepSea theme to enhance structure and readability. Adjusted color selectors for consistency across elements, including pool hashrate and navigation links, and modified button hover effects to utilize new primary color variables for a cohesive theme.
2025-04-23 06:58:56 -07:00
DJObleezy
df1678fac7 Increase font size for datum labels in dashboard.css
Updated the font size of the `.datum-label` class from `0.85em` to `0.95em` to enhance readability.
2025-04-22 22:38:03 -07:00
DJObleezy
6d3f873d6b Refactor text styling in dashboard.css
Removed padding, vertical alignment, and adjusted letter-spacing. Increased letter-spacing to 2px for improved text appearance.
2025-04-22 22:36:39 -07:00
DJObleezy
c7e2f0f4a9 Update .datum-label color to white
Changed the color of the `.datum-label` class from orange (`#ff9d00`) to white (`#ffffff`) for improved visibility.
2025-04-22 22:33:19 -07:00
DJObleezy
65d4deba5e Update theme toggle styles for better usability
Adjusted button border-radius for mobile view and added
a space in the content string for improved visual design.
2025-04-22 22:28:28 -07:00
DJObleezy
af3ea9607e
Update README.md 2025-04-22 21:47:53 -07:00
DJObleezy
eb95e6c6b5 Add theme toggle feature and configuration updates
- Introduced `theme-toggle.css` and `theme.js` to support a new theme toggle feature.
- Updated default configuration to include timezone and network fee.
- Enhanced command line arguments for network fee, timezone, and theme selection.
- Modified `create_config` to handle new configuration values from command line.
- Updated logging to reflect new network fee and timezone settings.
- Changed theme icons in `theme-toggle.css` for desktop and mobile views.
2025-04-22 20:39:45 -07:00
DJObleezy
f5e93f436b Implement DeepSea theme with CSS enhancements
Replaced the `applyDeepSeaTheme` function in `main.js` to apply a cohesive DeepSea theme, including extensive CSS variable definitions for UI elements.

In `theme.js`, updated styles to ensure visibility of pool hashrate text, enhanced button hover effects, and added direct DOM manipulation for consistent styling.

These changes improve the overall visual consistency and user experience of the application.
2025-04-22 20:10:10 -07:00
DJObleezy
e87993a252 Refactor theme toggle button positioning and styles
Updated the theme toggle button's positioning from `fixed` to `absolute` for consistency with `topRightLink`. Adjusted top and left positions, clarified media queries for desktop and mobile styles, and modified mobile button dimensions and padding. Reduced icon font size for better fit. These changes enhance the layout and responsiveness across different screen sizes.
2025-04-22 17:57:11 -07:00
DJObleezy
0d0a707019 Add responsive theme toggle and dynamic styling
Introduces a responsive theme toggle button with styles for desktop and mobile views in `theme-toggle.css`. Updates `BitcoinProgressBar.js` to support dynamic theme changes and adds a new `updateTheme` method. Enhances `main.js` for theme management based on user preferences in `localStorage`. Modifies `base.html` and other HTML files to include the theme toggle button and necessary scripts. Introduces `theme.js` for managing theme constants and applying the DeepSea theme.
2025-04-22 17:43:46 -07:00
DJObleezy
2142a7d2af Enhance generate_fallback_data method in WorkerService
Updated docstring to include argument and return type.
Added handling for None value in workers_count, defaulting to 1 worker.
Modified condition to use elif for clearer logic in worker count checks.
2025-04-22 14:06:11 -07:00
DJObleezy
231a56b18a Remove pool_luck metric and UI indicator
Updated the ArrowIndicator class to exclude the "pool_luck" metric while keeping "estimated_rewards_in_window_sats" and "workers_hashing". Removed the corresponding UI elements in the updateUI function, as the visual representation for "pool_luck" is no longer necessary.
2025-04-22 08:30:03 -07:00
DJObleezy
8c1c55c83f Remove duplicated saveConfig function from boot.html 2025-04-22 07:48:47 -07:00
DJObleezy
bdb9552576 Add network fee support to dashboard configuration
Updated the `reset_chart_data` function to include a new `network_fee` parameter in the `MiningDashboardService`. Modified `config.json` to add a default `network_fee` key. Enhanced `load_config` in `config.py` to handle the new parameter. Updated the `MiningDashboardService` constructor and `fetch_metrics` method to utilize the `network_fee` in calculations. Added a new input field for `network_fee` in `boot.html` and updated related JavaScript functions to manage this input. Improved the "Use Defaults" button functionality to reset the `network_fee` to its default value.
2025-04-22 07:43:57 -07:00
DJObleezy
b8321fe3b0 Enhance pool fees percentage display logic
Updated conditional checks in `dashboard.html` to ensure
`metrics.pool_fees_percentage` is not `none` before display.
Added validation for percentage range (0.9 to 1.3) to show
star icon and "DATUM" label, improving data accuracy and
presentation.
2025-04-22 06:22:59 -07:00
DJObleezy
ce3b186dc5 Cleaned up Earning Efficiency, Time To Block, and Block Odds in the Pool Hashrates card. Also fixed a display error on the Last Block line in the Payout Info card. 2025-04-21 22:54:45 -07:00
DJObleezy
eab3e89a11 Update spacing for block odds in UI
Increased margin-left for the `probSpan` element in the
`updateUI` function from 10px to 17px for better spacing.
Removed the inline margin-left style from the `block_odds_3hr`
span in `dashboard.html`, promoting a more consistent style
management through JavaScript.
2025-04-21 10:17:33 -07:00
DJObleezy
7267244e94 Enhance block finding metrics and UI display
Updated CSS for improved styling, added functions to calculate block finding probability and time, and modified UI to display these metrics based on the 24-hour hashrate. New HTML elements added to the dashboard for better user visibility of block odds.
2025-04-21 10:04:54 -07:00
DJObleezy
3bb74c37e7 Remove acceptance rate tracking from mining services
This commit removes the `avg_acceptance_rate` and `acceptance_rate` fields from both the `MiningDashboardService` and `WorkerService` classes. The changes simplify the data structures and calculations related to worker data, as acceptance rates are no longer tracked or displayed in the mining dashboard. This includes the removal of default values and random generation of acceptance rates for workers.
2025-04-20 06:20:38 -07:00
DJObleezy
9a9f9ae178 Remove acceptance rate display from worker stats
Eliminated the acceptance rate section from the `createWorkerCard` function in `workers.js`, including its corresponding HTML in `workers.html`. Updated the `updateSummaryStats` function to remove references to the average acceptance rate.
2025-04-19 16:57:05 -07:00
DJObleezy
4f52697185 Update mempool URL comment and dashboard metrics
Clarified the use of "mempool.guide" in `blocks.js` to align with Ocean.xyz ethos.

In `dashboard.html`, replaced the "Pool Fees" section with "Blocks Found," including logic to display the number of blocks found, defaulting to "0" if not defined. Removed associated pool fees logic and updated indicators accordingly.
2025-04-19 09:35:38 -07:00
DJObleezy
f6b3fdb094 Update data source from mempool.space to mempool.guide
This commit updates all references from "mempool.space" to "mempool.guide" in multiple files, including README.md, project_structure.md, blocks.js, and blocks.html.
2025-04-19 06:26:02 -07:00
DJObleezy
f1eb0e22b9
Update README.md 2025-04-18 19:42:47 -07:00
DJObleezy
ee469866d7
Update README.md 2025-04-18 13:27:08 -07:00
DJObleezy
6d07060b7e Add API for resetting chart data and update frontend
Implemented a new API endpoint `/api/reset-chart-data` in `App.py` to clear chart data history and save state to Redis. Updated the `resetDashboardChart` function in `main.js` to make an AJAX call to this endpoint, providing immediate user feedback. Removed previous logic for handling latest metrics to streamline the reset process.
2025-04-18 12:27:20 -07:00
DJObleezy
f166126525 Add timezone support to last updated timestamp formatting
Updated the `updateLastUpdated()` function in `workers.js` to include timezone configuration for formatting the last updated timestamp. Introduced a `configuredTimezone` variable with a default value of 'America/Los_Angeles'. The timestamp is now formatted using this timezone, and a console log statement indicates the timezone used. Added a fallback to the current date and time in case of formatting errors.
2025-04-18 11:28:18 -07:00
DJObleezy
97fe19d61d Add configurable timezone support throughout the app
Updated the application to use a configurable timezone instead of hardcoding "America/Los_Angeles". This change impacts the dashboard, API endpoints, and worker services. Timezone is now fetched from a configuration file or environment variable, enhancing flexibility in time display. New API endpoints for available timezones and the current configured timezone have been added. The frontend now allows users to select their timezone from a dropdown menu, which is stored in local storage for future use. Timestamps in the UI have been updated to reflect the selected timezone.
2025-04-18 11:08:35 -07:00
DJObleezy
96a71ec80d
Update README.md 2025-04-18 09:08:26 -07:00
DJObleezy
9a5c93036a Enhance .offline-dot style specificity
Added !important to the box-shadow property in the
.offline-dot class in common.css to ensure it takes
precedence over conflicting styles while keeping other
properties unchanged.
2025-04-17 19:43:28 -07:00
35 changed files with 4239 additions and 1191 deletions

70
App.py
View File

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

View File

@ -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:
![boot](https://github.com/user-attachments/assets/52d787ab-10d9-4c36-9cba-3ed8878dfa2b)
![dashboard](https://github.com/user-attachments/assets/1042b586-7f02-4514-83f6-1fee50c38b18) ![DeepSea Boot](https://github.com/user-attachments/assets/77222f13-1e95-48ee-a418-afd0e6b7a920)
![workers](https://github.com/user-attachments/assets/2d26dbd0-64b7-4f77-921c-c48ad2cb6122) ![DeepSea Config](https://github.com/user-attachments/assets/48fcc2a6-f56e-48b9-ac61-b27e9b4a6e41)
![blocks](https://github.com/user-attachments/assets/e38d6f17-5e89-4560-aeec-69a349fa12ba) ![DeepSea Dashboard](https://github.com/user-attachments/assets/f8f3671e-907a-456a-b8c6-5d9ecd07946c)
![notifications](https://github.com/user-attachments/assets/cb191fc5-fa85-49a6-a155-459c68008b8f)
--- ---
@ -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

View File

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

View File

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

View 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

View File

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

View File

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

View File

@ -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
View File

@ -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()

View File

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

View File

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

View File

@ -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 │
└───────────────────────────────┬────────────────────────────────────┘ └───────────────────────────────┬────────────────────────────────────┘
┌────────────────────────────────────────────┐ ┌────────────────────────────────────────────┐

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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();
} }

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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>");
} }
} }

View File

@ -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 %}

View File

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

View File

@ -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 },

View File

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

View File

@ -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 %}

View File

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

View File

@ -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
}) })