Compare commits

...

231 Commits
v0.3a ... 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
DJObleezy
eef2414ae2
Update README.md 2025-04-17 17:29:33 -07:00
DJObleezy
06f9d8a4b0
Update README.md 2025-04-17 17:19:18 -07:00
DJObleezy
c52001175b
Update README.md 2025-04-17 17:18:32 -07:00
DJObleezy
014b0acc24 Add fee indicator styling and conditional display
Implemented CSS styles for an optimal fee indicator, including a gold star and a label. Updated HTML to conditionally show the star and "DATUM" label when the pool fees percentage is between 0.9 and 1.3.
2025-04-17 15:27:30 -07:00
DJObleezy
982fe295d2 Add pool fees percentage metric to dashboard
Updated `MiningDashboardService` to calculate and display
the `pool_fees_percentage` metric, reflecting earnings lost
to pool fees. Enhanced error handling for earnings processing.
Updated styles in `dashboard.css` for the new metric and
added corresponding HTML elements in `dashboard.html` to
ensure proper display and conditional rendering.
2025-04-17 15:00:11 -07:00
DJObleezy
d98d496bd6 Limit worker data fetching to 10 pages
Updated the `get_all_worker_rows` method in the `MiningDashboardService` class to restrict the number of pages fetched to a maximum of 10. Enhanced logging to provide clearer information about the current page and maximum limit, and added a log message for when the maximum page limit is reached to improve visibility during data collection.
2025-04-17 10:05:51 -07:00
DJObleezy
0bb90b1aca 2025-04-17 08:45:03 -07:00
DJObleezy
4a3e0c96da Revert "Enhance pool info display in latest block stats"
This reverts commit 1554b0b0c5.
2025-04-17 08:45:03 -07:00
DJObleezy
17446bddc1
Delete ocean_scraper.py 2025-04-17 08:43:38 -07:00
DJObleezy
1d84bb08a8
Add files via upload 2025-04-16 23:23:07 -07:00
DJObleezy
4e7aace5d8 Refactor data retrieval to use web scraping
This commit removes the `OceanAPIClient` and introduces the `OceanScraper` for data retrieval in the mining dashboard application. Key changes include:
- Updated `App.py` to import `OceanScraper`.
- Enhanced `data_service.py` to reflect the transition to web scraping, including updates to the `MiningDashboardService` class.
- Improved methods for fetching metrics and worker data with better error handling and logging.
- Preserved the original web scraping method as a fallback.
- Removed the `ocean_api_client.py` file
- Added a new `ocean_scraper.py` file with comprehensive scraping functionality.
2025-04-16 22:05:12 -07:00
DJObleezy
60376e7395 Integrate Ocean API for enhanced metrics and worker data
Added `OceanAPIClient` to facilitate API interactions in `App.py`.
Modified `update_metrics_job` to include API status checks and fetch metrics.
Introduced `/api/check-api` endpoint for Ocean API health checks.
Updated `MiningDashboardService` to initialize the API client and fetch data directly from the Ocean API, with fallbacks to web scraping.
Refactored data retrieval methods to prioritize API calls and added error handling.
Enhanced logging for API interactions and created a new module `ocean_api_client.py` for encapsulating API logic.
Implemented retry mechanisms for API requests and updated data processing to align with the new API response structure.
2025-04-16 20:37:35 -07:00
DJObleezy
0a32b492b8 Improve API connectivity and error handling
Updated `MiningDashboardService` in `data_service.py` to enhance API connectivity testing and error handling. Introduced `_api_request_with_retry` for retry logic on API requests, and modified `_test_api_connectivity` to log detailed connectivity test information. Refactored multiple API calls to utilize the new retry method, improving reliability when fetching user hashrate data, pool stats, and other metrics.
2025-04-16 17:58:06 -07:00
DJObleezy
c9a2f927ff Improve API connectivity checks in MiningDashboardService
Updated the `_test_api_connectivity` method in `data_service.py` to include additional headers and enhanced logging. The method now attempts to ping a wallet-specific endpoint first, followed by a standard ping and a statsnap endpoint if necessary. Detailed error messages and a debug URL have been added for better troubleshooting, improving the overall robustness and clarity of the connectivity checks.
2025-04-16 17:46:47 -07:00
DJObleezy
f52b947633 Integrate Ocean.xyz API into MiningDashboardService
Updated `MiningDashboardService` in `data_service.py` to incorporate a new API from Ocean.xyz. Added base URL, connectivity test method, and new data-fetching methods. Existing methods modified to use the API when available, enhancing data retrieval efficiency. Improved error handling and logging, while retaining original web scraping methods as fallbacks.
2025-04-16 17:35:20 -07:00
DJObleezy
1554b0b0c5 Enhance pool info display in latest block stats
Updated the `updateLatestBlockStats` function to improve the presentation of pool information. Added color coding for pool names, highlighting "Ocean" pools with a star icon and special styling. The function now checks for pool name availability and adjusts the stats card styling accordingly, resetting to default for non-Ocean pools. Default display is set to "Unknown" if no pool information is available.
2025-04-15 21:53:11 -07:00
DJObleezy
d8f3972e03 Remove createBlockCard function from blocks.js
The `createBlockCard` function, which was responsible for generating block card elements displaying details such as timestamp, size, transaction count, miner/pool information, and average fee rate, has been completely removed. This change eliminates the functionality to display block cards in the codebase.
2025-04-15 21:49:44 -07:00
DJObleezy
9d5184e5c7 Add pool color mapping and enhance UI styling
Introduced a new `getPoolColor(poolName)` function to map mining pool names to specific colors, improving visual representation in the UI. Updated `createBlockCard(block)` and `showBlockDetails(block)` functions to utilize this new function, applying distinct styles for Ocean pools. These changes enhance user experience by providing clear, color-coded cues for different mining pools.
2025-04-15 21:44:45 -07:00
DJObleezy
5664e44cd9 Refactor daily stats posting logic
Updated the `_should_post_daily_stats` method to clarify that it checks for posting once per day at 12 PM. Simplified the logic to focus solely on this target time, requiring it to be a different day and within the first 5 minutes of 12 PM for posting. Adjusted the first-time posting condition to specifically check for 12 PM.
2025-04-15 21:31:19 -07:00
DJObleezy
1dec2ba35b Add explorer link to block details modal
This update introduces a new feature in `blocks.js` that adds an "Explorer Link" to view block details on the mempool.space website. The link is styled for visibility and includes an external link indicator. Additional comments were added for clarity, and minor adjustments were made to the existing code structure, ensuring the overall functionality of the block details modal remains intact.
2025-04-15 21:24:21 -07:00
DJObleezy
1eb17aed80 Refactor header styles and update last updated info
- Decreased font size of `h1` in `common.css` and added padding for better usability.
- Introduced hover effects for the top right link.
- Removed link from `h1` in `base.html` and added a block to display the last updated time.
2025-04-12 21:01:32 -07:00
DJObleezy
93175a7b40 Remove block mining animation SVG file
The entire SVG file for the block mining animation has been deleted. This includes all graphical elements, animations, and scripts related to the background, blockchain representation, Bitcoin logo, current block details, mining animation, status display, timestamp display, and CRT flicker animation.
2025-04-12 20:49:51 -07:00
DJObleezy
af89590b54
Update dashboard.html 2025-04-12 20:31:55 -07:00
DJObleezy
a2e253af93
Update main.js 2025-04-12 20:31:42 -07:00
DJObleezy
827f71651a
Update dashboard.css 2025-04-12 20:31:28 -07:00
DJObleezy
d9d239e06b Update dashboard return link in error page
Changed the return link from the root URL ("/") to the specific dashboard URL ("/dashboard") for better navigation.
2025-04-12 20:01:20 -07:00
DJObleezy
a4178d0a9b Removed Console 2025-04-12 19:59:57 -07:00
DJObleezy
695e4cfb95 Change unpaid earnings display to BTC format
Updated the `logCurrentStats` function to convert unpaid earnings from SATS to BTC. The new implementation divides the unpaid earnings by 100,000,000 and formats the result to 8 decimal places, improving clarity by using a more recognized cryptocurrency unit.
2025-04-12 17:37:20 -07:00
DJObleezy
13294b6d72 Improve log message formatting in logCurrentStats
Updated the `logCurrentStats` function to enhance the display of daily profit and unpaid earnings. Daily profit now defaults to '0.00' instead of 'CALCULATING...', and unpaid earnings are parsed as integers. Additionally, power consumption now shows '0 WATTS' instead of 'N/A W' when not available.
2025-04-12 17:29:37 -07:00
DJObleezy
6c7e986a80 Refactor logging mechanism for periodic stats
Updated `processLogQueue` to log periodic stats when the queue is empty. Replaced the switch-case structure in `logCurrentStats` with an array of log messages, which are randomized and queued for display. Added `shuffleArray` helper function and removed specific logging for unpaid balances to streamline the process.
2025-04-12 16:04:44 -07:00
DJObleezy
7646149bb9 Refactor metric update system to log update system
Renamed `metricUpdateQueue` to `logUpdateQueue` and updated related functions to reflect this change. Introduced `logInterval` for processing updates every 2 seconds. Modified efficiency calculation in `logCurrentStats` to use `metrics.hashrate_60sec` for accuracy.
2025-04-12 15:57:57 -07:00
DJObleezy
3a9213d52d Update Bitcoin Mining Console for real-time metrics
- Clarified comments to emphasize real-time data display.
- Modified `consoleSettings` to include `refreshInterval`.
- Changed initialization message for the console.
- Enhanced `setupEventSource` with connection logging and error handling.
- Updated `fetchMetrics` to log connection and error messages.
- Introduced `processMetricChanges` to handle significant metric updates.
- Added `logCurrentStats` for periodic logging of mining statistics.
- Implemented `queueMetricUpdate` to manage console message display.
- Retained and adjusted `adjustConsoleLayout` for proper layout on load.
- Updated HTML title and copyright information for branding.
2025-04-12 15:50:24 -07:00
DJObleezy
79c80e2cec Update console styles and version information
- Added white text color to `.stat-value` in console.css for better visibility.
- Updated Bitcoin Mining Terminal version to `21.0000` in console.html.
- Changed copyright notice to "BTC OS - Mining Operations".
- Removed current time display elements from the console.
2025-04-12 15:41:47 -07:00
DJObleezy
60d716f429 Adjust console layout for improved responsiveness
Modified `console.css` to change body and container heights for better spacing. Added maximum height and margins to `.console-container`. Updated `.console-wrapper` to have a calculated height based on available space.

Introduced `adjustConsoleLayout` function in `console.js` to dynamically adjust wrapper height based on viewport size, ensuring consistent layout on load and resize.
2025-04-12 13:58:59 -07:00
DJObleezy
a60d21521d Improve console layout and responsiveness
Updated `console.css` to enhance the layout and responsiveness of the console interface. Adjusted the console container to utilize the full viewport height while maintaining a controlled height for the console wrapper. Changed console output positioning to relative for better spacing and positioned the stats bar at the bottom for consistency. Added a new JavaScript function `adjustConsoleLayout` to dynamically calculate and set the height of the console wrapper based on viewport size, improving user experience across different screen sizes.
2025-04-12 13:53:27 -07:00
DJObleezy
f718647966 Improve console layout and update branding
Updated CSS for better height management and responsiveness.
Adjusted `.console-container`, `.console-wrapper`, and `.console-output` for improved layout stability and overflow handling.
Set minimum heights for `.console-stats` and added padding for better spacing.
Changed title from "BITCOIN MINING TERMINAL v1.0" to "BTC OS LOG TERMINAL v1.0".
2025-04-12 13:44:30 -07:00
DJObleezy
898652b754 Refactor console.js for enhanced metrics reporting
Updated consoleSettings to reduce hashRateFluctuation from 10% to 1%. Improved generateLog function comments and significantly enhanced generateSystemMessage to include real-time metrics on power consumption, system health, processing capacity, and revenue projections. Added random messages for variety and introduced alerts for offline workers and negative profitability, improving the logging system's functionality and responsiveness.
2025-04-12 13:34:15 -07:00
DJObleezy
308abcce5f Disable auto margin in console.css
Removed the `margin: 0 auto;` line and replaced it with a commented-out version to keep it for reference while disabling the margin setting.
2025-04-12 13:28:24 -07:00
DJObleezy
6a63add833 Enhance console styling with retro CRT theme
Updated `console.css` to implement a retro CRT theme, featuring new background colors, gradient effects, and flicker animations. Improved styles for console elements and added neon-inspired color classes for messages. Included media queries for mobile responsiveness.

Moved CSS link in `console.html` to a new block for better organization and structure.
2025-04-12 13:17:31 -07:00
DJObleezy
545e5a9d92 Add console asset mappings in setup.py
This commit introduces new file mappings for `console.css`, `console.js`, and `console.html` in the `setup.py` file. These additions ensure that the console-related assets are included in the project.
2025-04-12 13:06:51 -07:00
DJObleezy
886e595ef4 Add Bitcoin mining console page and related assets
This commit introduces a new route in `App.py` for a retro-styled console log page that displays real-time Bitcoin mining metrics. It includes a new CSS file, `console.css`, for styling with effects like CRT and text glitch animations. The `console.js` file is added to handle log generation, metrics fetching, and real-time updates. Additionally, a new `console.html` file is created to structure the console page, integrating the necessary styles and scripts.
2025-04-12 13:02:51 -07:00
DJObleezy
c3de5544ef Enhance negative profit styling with text shadow
Added a `text-shadow` property to the styling of
`dailyProfitElement` and `monthlyProfitElement`
to improve visibility when profit values are negative.
Existing styles for color and font-weight remain unchanged.
2025-04-11 22:56:49 -07:00
DJObleezy
6f5b2ec359 Improve style handling in updateUI function
Updated the `updateUI` function to use `setAttribute` for applying styles with `!important` to `dailyProfitElement` and `monthlyProfitElement`. Changed the reset logic to remove the entire style attribute instead of setting it to an empty string, ensuring complete style clearance for positive profit values.
2025-04-11 22:52:45 -07:00
DJObleezy
08034ea9a7 Enhance profit color visibility in updateUI function
Updated the `style.color` property for negative profit
elements in the `updateUI` function to include the
`!important` flag. This change ensures that the red color
(`#ff5555`) takes precedence over conflicting styles,
improving the visibility of negative profit indicators
in the user interface.
2025-04-11 22:47:01 -07:00
DJObleezy
9681077fbd Enhance UI updates for revenue and notifications
Updated `updateUI` to display daily revenue and power cost with conditional formatting for negative profits. Removed previous profit updates. Introduced `updateNotificationBadge` function to fetch unread notifications count via AJAX.
2025-04-11 22:37:00 -07:00
DJObleezy
fc7cc6e0c5
Update setup.py 2025-04-10 20:09:36 -07:00
DJObleezy
6cb74188b1
Update BitcoinProgressBar.js 2025-04-10 16:52:07 -07:00
DJObleezy
05033f12ad
Update main.js 2025-04-10 16:40:57 -07:00
DJObleezy
d491220a4d
Update App.py 2025-04-10 07:17:10 -07:00
DJObleezy
8cc012219b
Update workers.html 2025-04-10 06:42:18 -07:00
DJObleezy
287b9fde6b
Update App.py 2025-04-09 21:20:07 -07:00
DJObleezy
a98c488eb6
Update main.js 2025-04-09 13:17:20 -07:00
DJObleezy
4123007c34
Update README.md 2025-04-09 11:57:42 -07:00
DJObleezy
2919de9dae
Update README.md 2025-04-09 11:57:06 -07:00
DJObleezy
a0690b5739
Update README.md 2025-04-09 11:56:03 -07:00
DJObleezy
30907f26ff
Update README.md 2025-04-09 11:54:33 -07:00
DJObleezy
895487bbd4
Update README.md 2025-04-09 11:53:45 -07:00
DJObleezy
8c2fc76e35
Update README.md 2025-04-09 11:53:04 -07:00
DJObleezy
cee6eec403
Update README.md 2025-04-09 11:52:18 -07:00
DJObleezy
3f867264ac
Update README.md 2025-04-09 11:48:22 -07:00
DJObleezy
28785b915e
Update README.md 2025-04-09 11:47:13 -07:00
DJObleezy
4be19833d7
Add files via upload 2025-04-09 11:46:24 -07:00
DJObleezy
f73a1825b6
Delete worker_service.py 2025-04-09 11:45:17 -07:00
DJObleezy
c8a62971e2
Delete state_manager.py 2025-04-09 11:45:12 -07:00
DJObleezy
b64c482c99
Delete setup.py 2025-04-09 11:45:06 -07:00
DJObleezy
461b541a7a
Delete requirements.txt 2025-04-09 11:45:00 -07:00
DJObleezy
cd5919d167
Delete project_structure.md 2025-04-09 11:44:54 -07:00
DJObleezy
605d7f15c0
Delete notification_service.py 2025-04-09 11:44:48 -07:00
DJObleezy
64e41140a6
Delete models.py 2025-04-09 11:44:42 -07:00
DJObleezy
7996d288bf
Delete dockerfile 2025-04-09 11:44:36 -07:00
DJObleezy
674801a2d2
Delete deployment_steps.md 2025-04-09 11:44:31 -07:00
DJObleezy
541ff9a73a
Delete data_service.py 2025-04-09 11:44:24 -07:00
DJObleezy
b11b64c38f
Delete config.py 2025-04-09 11:44:18 -07:00
DJObleezy
484a087250
Delete config.json 2025-04-09 11:44:11 -07:00
DJObleezy
026b86c255
Delete LICENSE.md 2025-04-09 11:44:05 -07:00
DJObleezy
2b7b3d66f8
Delete README.md 2025-04-09 11:43:58 -07:00
DJObleezy
cc2cd6354f
Delete App.py 2025-04-09 11:43:33 -07:00
DJObleezy
4845f42fa4
Delete templates directory 2025-04-09 11:43:14 -07:00
DJObleezy
d374bc3ba1
Delete static directory 2025-04-09 11:43:06 -07:00
DJObleezy
35dd182eb2
Update main.js 2025-04-04 14:20:44 -07:00
DJObleezy
c7e0af6431
Update main.js 2025-04-02 13:39:57 -07:00
DJObleezy
1416cf2bf5
Update notification_service.py 2025-04-02 13:39:33 -07:00
DJObleezy
353c9567bb
Update dashboard.css 2025-04-02 13:39:09 -07:00
DJObleezy
504eb88077
Update App.py 2025-04-02 13:37:05 -07:00
DJObleezy
1a99c93ec9
Update main.js 2025-04-01 06:00:47 -07:00
DJObleezy
13a38d351e
Update README.md 2025-03-30 21:42:50 -07:00
DJObleezy
315de329c2
Update workers.js 2025-03-30 21:11:16 -07:00
DJObleezy
c1d32f4cfc
Update workers.html 2025-03-30 21:03:04 -07:00
DJObleezy
e21b7be7a0
Update workers.js 2025-03-30 21:02:45 -07:00
DJObleezy
a2a661797d
Update README.md 2025-03-30 18:33:00 -07:00
DJObleezy
cfce1191cd
Update README.md 2025-03-30 18:30:37 -07:00
DJObleezy
11c8255a18
Update README.md 2025-03-30 15:57:36 -07:00
DJObleezy
c4d399748d
Update boot.html 2025-03-30 15:49:30 -07:00
DJObleezy
58cbc2a02c
Update main.js 2025-03-30 15:25:13 -07:00
DJObleezy
b15dfc8691
Update worker_service.py 2025-03-30 10:15:22 -07:00
DJObleezy
9988913861
Update App.py 2025-03-30 10:11:24 -07:00
DJObleezy
f812a41bca
Update App.py 2025-03-30 10:10:11 -07:00
DJObleezy
2023256113
Update README.md 2025-03-30 09:04:50 -07:00
DJObleezy
ff84f8879a
Update README.md 2025-03-30 09:04:19 -07:00
DJObleezy
4bbfcb70fe
Update README.md 2025-03-30 09:03:10 -07:00
DJObleezy
bbe2cced4d
Update README.md 2025-03-30 09:02:04 -07:00
DJObleezy
830ea8e917
Update main.js 2025-03-30 09:00:40 -07:00
DJObleezy
2cc3d16830
Update main.js 2025-03-30 06:21:44 -07:00
DJObleezy
1c4a2cfbdf
Update setup.py 2025-03-29 21:32:04 -07:00
DJObleezy
fb5dd76368
Update README.md 2025-03-29 21:31:15 -07:00
DJObleezy
e5fe4974f5
Update App.py 2025-03-29 21:28:10 -07:00
DJObleezy
8b8cfa5ff1
Update boot.html 2025-03-29 21:27:27 -07:00
DJObleezy
ac52bb8579
Update config.py 2025-03-29 21:25:58 -07:00
DJObleezy
2a08efdfdb
Update config.json 2025-03-29 21:25:20 -07:00
DJObleezy
04d80008c7
Update README.md 2025-03-29 21:23:36 -07:00
DJObleezy
a785aac643
Update blocks.html 2025-03-29 20:52:16 -07:00
DJObleezy
9ad41b5fff
Update workers.css 2025-03-29 20:51:51 -07:00
DJObleezy
eea4de4d57
Update blocks.css 2025-03-29 20:51:39 -07:00
DJObleezy
115e59b8ed
Update main.js 2025-03-29 20:33:03 -07:00
DJObleezy
fd412b9f22
Update BitcoinProgressBar.js 2025-03-29 20:32:40 -07:00
DJObleezy
8d40fb83a8
Update blocks.js 2025-03-29 20:32:18 -07:00
DJObleezy
495e272843
Update workers.js 2025-03-29 20:31:35 -07:00
DJObleezy
f4a4da4679
Update data_service.py 2025-03-29 15:29:52 -07:00
DJObleezy
6331ea2737
Update main.js 2025-03-28 19:57:46 -07:00
DJObleezy
49f696b136
Update notification_service.py 2025-03-28 19:42:19 -07:00
DJObleezy
8b59c0e784
Update models.py 2025-03-28 19:35:20 -07:00
DJObleezy
bfacc24734
Update data_service.py 2025-03-28 19:34:53 -07:00
DJObleezy
cc349dd0cb
Update workers.html 2025-03-28 19:34:23 -07:00
DJObleezy
fe7d19c43f
Update worker_service.py 2025-03-28 19:34:03 -07:00
DJObleezy
9d8355eb7b
Update workers.js 2025-03-28 19:33:40 -07:00
DJObleezy
8e6af4043a
Update worker_service.py 2025-03-28 19:25:59 -07:00
DJObleezy
33a96600a4
Update worker_service.py 2025-03-28 19:21:55 -07:00
DJObleezy
f89b7b2825
Update worker_service.py 2025-03-28 19:17:46 -07:00
DJObleezy
9656368478
Update BitcoinProgressBar.js 2025-03-28 19:14:02 -07:00
DJObleezy
7aee1fe982
Update block-animation.js 2025-03-28 19:13:46 -07:00
DJObleezy
401655c3ae
Update main.js 2025-03-28 19:13:21 -07:00
DJObleezy
7b4bd5344c
Update notifications.js 2025-03-28 19:13:04 -07:00
DJObleezy
514644a3ef
Update workers.js 2025-03-28 19:12:37 -07:00
DJObleezy
d4981d91e2
Update base.html 2025-03-28 19:12:17 -07:00
DJObleezy
c54b618880
Update blocks.html 2025-03-28 19:11:57 -07:00
DJObleezy
c18a85693c
Update notifications.html 2025-03-28 19:10:59 -07:00
DJObleezy
62703f1b4e
Update workers.html 2025-03-28 19:10:43 -07:00
DJObleezy
1a9b09afae
Update App.py 2025-03-28 19:09:59 -07:00
DJObleezy
b651320bfb
Update data_service.py 2025-03-28 19:09:07 -07:00
DJObleezy
dd3d94f2b7
Update models.py 2025-03-28 19:08:27 -07:00
DJObleezy
2fec01a990
Update setup.py 2025-03-28 19:08:06 -07:00
DJObleezy
98b29aa521
Update state_manager.py 2025-03-28 19:07:29 -07:00
DJObleezy
2b05ec885a
Update worker_service.py 2025-03-28 19:07:03 -07:00
DJObleezy
651ed80bbd
Update BitcoinProgressBar.js 2025-03-27 16:09:33 -07:00
DJObleezy
cbcaaec237
Update retro-refresh.css 2025-03-27 16:09:13 -07:00
DJObleezy
b4b6995cc1
Update state_manager.py 2025-03-27 09:51:42 -07:00
DJObleezy
fbdade3837
Update notifications.css 2025-03-27 09:51:01 -07:00
DJObleezy
b8368cd537
Update workers.js 2025-03-27 09:50:19 -07:00
DJObleezy
8e2f912616
Update notifications.js 2025-03-27 09:50:07 -07:00
DJObleezy
44ffdba522
Update main.js 2025-03-27 09:49:55 -07:00
DJObleezy
f57392f10e
Update blocks.js 2025-03-27 09:49:41 -07:00
DJObleezy
1e85ebb48d
Update BitcoinProgressBar.js 2025-03-27 09:48:39 -07:00
DJObleezy
58397c2c9b
Update main.js 2025-03-26 11:04:09 -07:00
DJObleezy
b9993a12f1
Update state_manager.py 2025-03-26 10:32:43 -07:00
DJObleezy
7b7f386a2d
Update main.js 2025-03-26 08:16:30 -07:00
DJObleezy
dd10921534
Update notification_service.py 2025-03-26 08:02:21 -07:00
DJObleezy
65ebf2032e
Update state_manager.py 2025-03-26 08:02:03 -07:00
DJObleezy
c92c22cf04
Update notification_service.py 2025-03-25 20:44:46 -07:00
DJObleezy
b42fb2d16f
Update blocks.html 2025-03-25 13:50:10 -07:00
DJObleezy
9c952135b8
Update base.html 2025-03-25 13:49:56 -07:00
DJObleezy
d22874e975
Update dashboard.html 2025-03-25 13:49:44 -07:00
DJObleezy
f00cbb6755
Add files via upload 2025-03-25 13:34:54 -07:00
DJObleezy
6fffc50af1
Add files via upload 2025-03-25 13:34:35 -07:00
DJObleezy
0342b16b40
Add files via upload 2025-03-25 13:34:10 -07:00
DJObleezy
3566f2127c
Add files via upload 2025-03-25 13:33:54 -07:00
DJObleezy
8dca2c41a4
Update common.css 2025-03-25 13:33:26 -07:00
DJObleezy
d368f37541
Update base.html 2025-03-25 13:32:59 -07:00
DJObleezy
03fa18e361
Update main.js 2025-03-25 13:32:30 -07:00
DJObleezy
40371b2f38
Update App.py 2025-03-25 13:31:46 -07:00
DJObleezy
26e1780501
Update state_manager.py 2025-03-25 13:30:17 -07:00
DJObleezy
44c3db8512
Update blocks.js 2025-03-25 12:24:11 -07:00
DJObleezy
37f48c0c10
Update blocks.html 2025-03-25 12:23:51 -07:00
DJObleezy
ae7defaab2
Update README.md 2025-03-25 11:51:43 -07:00
DJObleezy
c4bcc65a99
Update README.md 2025-03-25 11:49:50 -07:00
DJObleezy
d19f3ee11c
Update common.css 2025-03-25 08:37:44 -07:00
DJObleezy
5e25b5a485
Update blocks.css 2025-03-25 08:34:20 -07:00
39 changed files with 7851 additions and 2947 deletions

181
App.py
View File

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

107
README.md
View File

@ -1,24 +1,28 @@
# Ocean.xyz Bitcoin Mining Dashboard
# DeepSea Dashboard
## A Comprehensive Monitoring Solution for Bitcoin Miners
## A Retro Mining Monitoring Solution
This open-source dashboard provides real-time monitoring for Ocean.xyz pool miners, offering detailed insights on hashrate, profitability, worker status, and network metrics. Designed with a retro terminal aesthetic and focused on reliability, it helps miners maintain complete oversight of their operations.
---
## Gallery:
![Boot Sequence](https://github.com/user-attachments/assets/8205e8c0-79ad-4780-bc50-237131373cf8)
![Main Dashboard](https://github.com/user-attachments/assets/33dafb93-38ef-4fee-aba1-3a7d38eca3c9)
![Workers Overview](https://github.com/user-attachments/assets/ae78c34c-fbdf-4186-9706-760a67eac44c)
![DeepSea Boot](https://github.com/user-attachments/assets/77222f13-1e95-48ee-a418-afd0e6b7a920)
![DeepSea Config](https://github.com/user-attachments/assets/48fcc2a6-f56e-48b9-ac61-b27e9b4a6e41)
![DeepSea Dashboard](https://github.com/user-attachments/assets/f8f3671e-907a-456a-b8c6-5d9ecd07946c)
---
## Key Features
### Real-Time Mining Metrics
- **Live Hashrate Tracking**: Monitor 60-second, 10-minute, 3-hour, and 24-hour average hashrates
- **Profitability Analysis**: View daily and monthly earnings in both BTC and USD
- **Financial Calculations**: Automatically calculate revenue, power costs, and net profit
- **Network Statistics**: Track current Bitcoin price, difficulty, and network hashrate
- **Payout Monitoring**: View unpaid balance and estimated time to next payout
- **Pool Fee Analysis**: Monitor pool fee percentages with visual indicator when optimal rates (0.9-1.3%) are detected
### Worker Management
- **Fleet Overview**: Comprehensive view of all mining devices in one interface
@ -36,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
- **Cross-Tab Synchronization**: Data consistency across multiple browser tabs
- **Server Health Monitoring**: Built-in watchdog processes ensure reliability
- **Error Handling**: Displays a user-friendly error page (`error.html`) for unexpected issues.
### Distinctive Design Elements
- **Retro Terminal Aesthetic**: Nostalgic interface with modern functionality
@ -43,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
- **Responsive Interface**: Adapts to desktop and mobile devices
### DeepSea Theme
- **Underwater Effects**: Light rays and digital noise create an immersive experience.
- **Retro Glitch Effects**: Subtle animations for a nostalgic feel.
- **Theme Toggle**: Switch between Bitcoin and DeepSea themes with a single click.
## Quick Start
### Installation
1. Clone the repository
```
git clone https://github.com/yourusername/bitcoin-mining-dashboard.git
cd bitcoin-mining-dashboard
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git
cd DeepSea-Dashboard
```
2. Install dependencies:
@ -63,35 +73,48 @@ This open-source dashboard provides real-time monitoring for Ocean.xyz pool mine
python setup.py
```
4. Configure your mining settings:
```json
{
"power_cost": 0.12,
"power_usage": 3450,
"wallet": "your-wallet-address"
}
```
5. Start the application:
4. Start the application:
```
python App.py
```
6. Open your browser at `http://localhost:5000`
### Docker Deployment
```bash
docker build -t bitcoin-mining-dashboard .
docker run -d -p 5000:5000 \
-e WALLET=your-wallet-address \
-e POWER_COST=0.12 \
-e POWER_USAGE=3450 \
bitcoin-mining-dashboard
```
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).
## Using docker-compose (with Redis)
The `docker-compose.yml` file makes it easy to deploy the dashboard and its dependencies.
### Steps to Deploy
1. **Start the services**:
Run the following command in the project root:
```
docker-compose up -d
```
2. **Access the dashboard**:
Open your browser at `http://localhost:5000`.
3. **Stop the services**:
To stop the services, run:
```
docker-compose down
```
### Customization
You can modify the following environment variables in the `docker-compose.yml` file:
- `WALLET`: Your Bitcoin wallet address.
- `POWER_COST`: Cost of power per kWh.
- `POWER_USAGE`: Power usage in watts.
- `NETWORK_FEE`: Additional fees beyond pool fees (e.g., firmware fees).
- `TIMEZONE`: Local timezone for displaying time information.
Redis data is stored in a persistent volume (`redis_data`), and application logs are saved in the `./logs` directory.
For more details, refer to the [docker-compose documentation](https://docs.docker.com/compose/).
## Dashboard Components
### Main Dashboard
@ -142,12 +165,18 @@ Built with a modern stack for reliability and performance:
- **Resilience**: Automatic recovery mechanisms and state persistence
- **Configuration**: Environment variables and JSON-based settings
## API Endpoints
- `/api/metrics`: Provides real-time mining metrics.
- `/api/available_timezones`: Returns a list of supported timezones.
- `/api/config`: Fetches or updates the mining configuration.
- `/api/health`: Returns the health status of the application.
## Project Structure
The project follows a modular architecture with clear separation of concerns:
```
bitcoin-mining-dashboard/
DeepSea-Dashboard/
├── App.py # Main application entry point
├── config.py # Configuration management
@ -156,9 +185,12 @@ bitcoin-mining-dashboard/
├── models.py # Data models
├── state_manager.py # Manager for persistent state
├── worker_service.py # Service for worker data management
├── notification_service.py # Service for notifications
├── minify.py # Script for minifying assets
├── setup.py # Setup script for organizing files
├── requirements.txt # Python dependencies
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
├── templates/ # HTML templates
│ ├── base.html # Base template with common elements
@ -166,6 +198,7 @@ bitcoin-mining-dashboard/
│ ├── dashboard.html # Main dashboard template
│ ├── workers.html # Workers dashboard template
│ ├── blocks.html # Bitcoin blocks template
│ ├── notifications.html # Notifications template
│ └── error.html # Error page template
├── static/ # Static assets
@ -175,18 +208,24 @@ bitcoin-mining-dashboard/
│ │ ├── workers.css # Workers page styles
│ │ ├── boot.css # Boot sequence styles
│ │ ├── blocks.css # Blocks page styles
│ │ ├── notifications.css # Notifications page styles
│ │ ├── error.css # Error page styles
│ │ └── retro-refresh.css # Floating refresh bar styles
│ │ ├── retro-refresh.css # Floating refresh bar styles
│ │ └── theme-toggle.css # Theme toggle styles
│ │
│ └── js/ # JavaScript files
│ ├── main.js # Main dashboard functionality
│ ├── workers.js # Workers page functionality
│ ├── blocks.js # Blocks page functionality
│ ├── notifications.js # Notifications functionality
│ ├── block-animation.js # Block mining animation
│ └── BitcoinProgressBar.js # System monitor functionality
│ ├── BitcoinProgressBar.js # System monitor functionality
│ └── theme.js # Theme toggle functionality
├── 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).
@ -200,6 +239,7 @@ For optimal performance:
3. Use the system monitor to verify connection status
4. Access the health endpoint at `/api/health` for diagnostics
5. For stale data issues, use the Force Refresh function
6. Use hotkey Shift+R to clear chart and Redis data (as needed, not required)
## License
@ -208,5 +248,6 @@ Available under the MIT License. This is an independent project not affiliated w
## Acknowledgments
- Ocean.xyz mining pool for their service
- mempool.guide
- The open-source community for their contributions
- Bitcoin protocol developers

View File

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

View File

@ -12,14 +12,13 @@ CONFIG_FILE = "config.json"
def load_config():
"""
Load configuration from file or return defaults if file doesn't exist.
Returns:
dict: Configuration dictionary with settings
"""
default_config = {
"power_cost": 0.0,
"power_usage": 0.0,
"wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9"
"wallet": "yourwallethere",
"timezone": "America/Los_Angeles",
"network_fee": 0.0 # Add default network fee
}
if os.path.exists(CONFIG_FILE):
@ -27,6 +26,12 @@ def load_config():
with open(CONFIG_FILE, "r") as f:
config = json.load(f)
logging.info(f"Configuration loaded from {CONFIG_FILE}")
# Ensure network_fee is present even in existing config files
if "network_fee" not in config:
config["network_fee"] = default_config["network_fee"]
logging.info("Added missing network_fee to config with default value")
return config
except Exception as e:
logging.error(f"Error loading config: {e}")
@ -35,6 +40,28 @@ def load_config():
return default_config
def get_timezone():
"""
Get the configured timezone with fallback to default.
Returns:
str: Timezone identifier
"""
# First check environment variable (for Docker)
import os
env_timezone = os.environ.get("TIMEZONE")
if env_timezone:
return env_timezone
# Then check config file
config = load_config()
timezone = config.get("timezone")
if timezone:
return timezone
# Default to Los Angeles
return "America/Los_Angeles"
def save_config(config):
"""
Save configuration to file.

View File

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

View File

@ -16,8 +16,8 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
1. Clone the repository:
```bash
git clone https://github.com/yourusername/bitcoin-mining-dashboard.git
cd bitcoin-mining-dashboard
git clone https://github.com/Djobleezy/DeepSea-Dashboard.git
cd DeepSea-Dashboard
```
2. Create a virtual environment (recommended):
@ -36,21 +36,12 @@ This guide provides comprehensive instructions for deploying the Bitcoin Mining
python setup.py
```
5. Configure your mining parameters in `config.json`:
```json
{
"power_cost": 0.12, // Cost of electricity per kWh
"power_usage": 3450, // Power consumption in watts
"wallet": "your-wallet-address" // Your Ocean.xyz wallet
}
```
6. Start the application:
5. Start the application:
```bash
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

42
docker-compose.yml Normal file
View File

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

View File

@ -1,4 +1,4 @@
FROM python:3.9-slim
FROM python:3.9.18-slim
WORKDIR /app
@ -17,8 +17,8 @@ COPY *.py .
COPY config.json .
COPY setup.py .
# Create necessary directories
RUN mkdir -p static/css static/js templates logs
# Create all necessary directories in one command
RUN mkdir -p static/css static/js templates logs /app/logs
# Copy static files and templates
COPY static/css/*.css static/css/
@ -29,7 +29,7 @@ COPY templates/*.html templates/
RUN python setup.py
# Run the minifier to process HTML templates
# RUN python minify.py
RUN python minify.py
# Create a non-root user for better security
RUN adduser --disabled-password --gecos '' appuser
@ -37,9 +37,6 @@ RUN adduser --disabled-password --gecos '' appuser
# Change ownership of the /app directory so appuser can write files
RUN chown -R appuser:appuser /app
# Create a directory for logs with proper permissions
RUN mkdir -p /app/logs && chown -R appuser:appuser /app/logs
# Switch to non-root user
USER appuser
@ -49,7 +46,6 @@ EXPOSE 5000
# Set environment variables
ENV FLASK_ENV=production
ENV PYTHONUNBUFFERED=1
ENV PYTHON_UNBUFFERED=1
# Add healthcheck
HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \

246
minify.py Normal file
View File

@ -0,0 +1,246 @@
#!/usr/bin/env python3
import os
import jsmin
import htmlmin
import logging
from pathlib import Path
# Set up logging
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
def minify_js_files():
"""Minify JavaScript files."""
js_dir = 'static/js'
min_dir = os.path.join(js_dir, 'min')
os.makedirs(min_dir, exist_ok=True)
minified_count = 0
skipped_count = 0
for js_file in os.listdir(js_dir):
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
try:
input_path = os.path.join(js_dir, js_file)
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
# Skip already minified files if they're newer than source
if os.path.exists(output_path) and \
os.path.getmtime(output_path) > os.path.getmtime(input_path):
logger.info(f"Skipping {js_file} (already up to date)")
skipped_count += 1
continue
with open(input_path, 'r', encoding='utf-8') as f:
js_content = f.read()
# Minify the content
minified = jsmin.jsmin(js_content)
# Write minified content
with open(output_path, 'w', encoding='utf-8') as f:
f.write(minified)
size_original = len(js_content)
size_minified = len(minified)
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
logger.info(f"Minified {js_file} - Reduced by {reduction:.1f}%")
minified_count += 1
except Exception as e:
logger.error(f"Error processing {js_file}: {e}")
logger.info(f"JavaScript minification: {minified_count} files minified, {skipped_count} files skipped")
return minified_count
def minify_css_files():
"""Minify CSS files using simple compression techniques."""
css_dir = 'static/css'
min_dir = os.path.join(css_dir, 'min')
os.makedirs(min_dir, exist_ok=True)
minified_count = 0
skipped_count = 0
for css_file in os.listdir(css_dir):
if css_file.endswith('.css') and not css_file.endswith('.min.css'):
try:
input_path = os.path.join(css_dir, css_file)
output_path = os.path.join(min_dir, css_file.replace('.css', '.min.css'))
# Skip already minified files if they're newer than source
if os.path.exists(output_path) and \
os.path.getmtime(output_path) > os.path.getmtime(input_path):
logger.info(f"Skipping {css_file} (already up to date)")
skipped_count += 1
continue
with open(input_path, 'r', encoding='utf-8') as f:
css_content = f.read()
# Simple CSS minification using string replacements
# Remove comments
import re
css_minified = re.sub(r'/\*[\s\S]*?\*/', '', css_content)
# Remove whitespace
css_minified = re.sub(r'\s+', ' ', css_minified)
# Remove spaces around selectors
css_minified = re.sub(r'\s*{\s*', '{', css_minified)
css_minified = re.sub(r'\s*}\s*', '}', css_minified)
css_minified = re.sub(r'\s*;\s*', ';', css_minified)
css_minified = re.sub(r'\s*:\s*', ':', css_minified)
css_minified = re.sub(r'\s*,\s*', ',', css_minified)
# Remove last semicolons
css_minified = re.sub(r';}', '}', css_minified)
with open(output_path, 'w', encoding='utf-8') as f:
f.write(css_minified)
size_original = len(css_content)
size_minified = len(css_minified)
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
logger.info(f"Minified {css_file} - Reduced by {reduction:.1f}%")
minified_count += 1
except Exception as e:
logger.error(f"Error processing {css_file}: {e}")
logger.info(f"CSS minification: {minified_count} files minified, {skipped_count} files skipped")
return minified_count
def minify_html_templates():
"""Minify HTML template files."""
templates_dir = 'templates'
minified_count = 0
skipped_count = 0
for html_file in os.listdir(templates_dir):
if html_file.endswith('.html'):
try:
input_path = os.path.join(templates_dir, html_file)
with open(input_path, 'r', encoding='utf-8') as f:
html_content = f.read()
# Minify HTML content while keeping important whitespace
minified = htmlmin.minify(html_content,
remove_comments=True,
remove_empty_space=True,
remove_all_empty_space=False,
reduce_boolean_attributes=True)
# Write back to the same file
with open(input_path, 'w', encoding='utf-8') as f:
f.write(minified)
size_original = len(html_content)
size_minified = len(minified)
reduction = (1 - size_minified / size_original) * 100 if size_original > 0 else 0
logger.info(f"Minified {html_file} - Reduced by {reduction:.1f}%")
minified_count += 1
except Exception as e:
logger.error(f"Error processing {html_file}: {e}")
logger.info(f"HTML minification: {minified_count} files minified, {skipped_count} files skipped")
return minified_count
def create_size_report():
"""Create a report of file sizes before and after minification."""
results = []
# Check JS files
js_dir = 'static/js'
min_dir = os.path.join(js_dir, 'min')
if os.path.exists(min_dir):
for js_file in os.listdir(js_dir):
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
orig_path = os.path.join(js_dir, js_file)
min_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
if os.path.exists(min_path):
orig_size = os.path.getsize(orig_path)
min_size = os.path.getsize(min_path)
reduction = (1 - min_size / orig_size) * 100 if orig_size > 0 else 0
results.append({
'file': js_file,
'type': 'JavaScript',
'original_size': orig_size,
'minified_size': min_size,
'reduction': reduction
})
# Check CSS files
css_dir = 'static/css'
min_dir = os.path.join(css_dir, 'min')
if os.path.exists(min_dir):
for css_file in os.listdir(css_dir):
if css_file.endswith('.css') and not css_file.endswith('.min.css'):
orig_path = os.path.join(css_dir, css_file)
min_path = os.path.join(min_dir, css_file.replace('.css', '.min.css'))
if os.path.exists(min_path):
orig_size = os.path.getsize(orig_path)
min_size = os.path.getsize(min_path)
reduction = (1 - min_size / orig_size) * 100 if orig_size > 0 else 0
results.append({
'file': css_file,
'type': 'CSS',
'original_size': orig_size,
'minified_size': min_size,
'reduction': reduction
})
# Print the report
total_orig = sum(item['original_size'] for item in results)
total_min = sum(item['minified_size'] for item in results)
total_reduction = (1 - total_min / total_orig) * 100 if total_orig > 0 else 0
logger.info("\n" + "="*50)
logger.info("MINIFICATION REPORT")
logger.info("="*50)
logger.info(f"{'File':<30} {'Type':<10} {'Original':<10} {'Minified':<10} {'Reduction'}")
logger.info("-"*70)
for item in results:
logger.info(f"{item['file']:<30} {item['type']:<10} "
f"{item['original_size']/1024:.1f}KB {item['minified_size']/1024:.1f}KB "
f"{item['reduction']:.1f}%")
logger.info("-"*70)
logger.info(f"{'TOTAL:':<30} {'':<10} {total_orig/1024:.1f}KB {total_min/1024:.1f}KB {total_reduction:.1f}%")
logger.info("="*50)
def main():
"""Main function to run minification tasks."""
import argparse
parser = argparse.ArgumentParser(description='Minify web assets')
parser.add_argument('--js', action='store_true', help='Minify JavaScript files')
parser.add_argument('--css', action='store_true', help='Minify CSS files')
parser.add_argument('--html', action='store_true', help='Minify HTML templates')
parser.add_argument('--all', action='store_true', help='Minify all assets')
parser.add_argument('--report', action='store_true', help='Generate size report only')
args = parser.parse_args()
# If no arguments, default to --all
if not (args.js or args.css or args.html or args.report):
args.all = True
if args.all or args.js:
minify_js_files()
if args.all or args.css:
minify_css_files()
if args.all or args.html:
minify_html_templates()
# Always generate the report at the end if any minification was done
if args.report or args.all or args.js or args.css:
create_size_report()
if __name__ == "__main__":
main()

View File

@ -2,11 +2,13 @@
Data models for the Bitcoin Mining Dashboard.
"""
from dataclasses import dataclass
from typing import Optional, Dict, List, Union, Any
import logging
@dataclass
class OceanData:
"""Data structure for Ocean.xyz pool mining data."""
# Keep original definitions with None default to maintain backward compatibility
pool_total_hashrate: float = None
pool_total_hashrate_unit: str = None
hashrate_24hr: float = None
@ -31,13 +33,49 @@ class OceanData:
blocks_found: str = None
total_last_share: str = "N/A"
last_block_earnings: str = None
pool_fees_percentage: float = None # Added missing attribute
def get_normalized_hashrate(self, timeframe: str = "3hr") -> float:
"""
Get a normalized hashrate value in TH/s regardless of original units.
Args:
timeframe: The timeframe to get ("24hr", "3hr", "10min", "5min", "60sec")
Returns:
float: Normalized hashrate in TH/s
"""
if timeframe == "24hr" and self.hashrate_24hr is not None:
return convert_to_ths(self.hashrate_24hr, self.hashrate_24hr_unit)
elif timeframe == "3hr" and self.hashrate_3hr is not None:
return convert_to_ths(self.hashrate_3hr, self.hashrate_3hr_unit)
elif timeframe == "10min" and self.hashrate_10min is not None:
return convert_to_ths(self.hashrate_10min, self.hashrate_10min_unit)
elif timeframe == "5min" and self.hashrate_5min is not None:
return convert_to_ths(self.hashrate_5min, self.hashrate_5min_unit)
elif timeframe == "60sec" and self.hashrate_60sec is not None:
return convert_to_ths(self.hashrate_60sec, self.hashrate_60sec_unit)
return 0.0
def to_dict(self) -> Dict[str, Any]:
"""Convert the OceanData object to a dictionary."""
return {k: v for k, v in self.__dict__.items()}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'OceanData':
"""Create an OceanData instance from a dictionary."""
filtered_data = {}
for k, v in data.items():
if k in cls.__annotations__:
filtered_data[k] = v
return cls(**filtered_data)
@dataclass
class WorkerData:
"""Data structure for individual worker information."""
name: str = None
status: str = "offline"
type: str = "ASIC" # ASIC or FPGA
type: str = "ASIC" # ASIC or Bitaxe
model: str = "Unknown"
hashrate_60sec: float = 0
hashrate_60sec_unit: str = "TH/s"
@ -50,6 +88,61 @@ class WorkerData:
power_consumption: float = 0
temperature: float = 0
def __post_init__(self):
"""
Validate worker data after initialization.
Ensures values are within acceptable ranges and formats.
"""
# Ensure hashrates are non-negative
if self.hashrate_60sec is not None and self.hashrate_60sec < 0:
self.hashrate_60sec = 0
if self.hashrate_3hr is not None and self.hashrate_3hr < 0:
self.hashrate_3hr = 0
# Ensure status is valid, but don't raise exceptions for backward compatibility
if self.status not in ["online", "offline"]:
logging.warning(f"Worker {self.name}: Invalid status '{self.status}', using 'offline'")
self.status = "offline"
# Ensure type is valid, but don't raise exceptions for backward compatibility
if self.type not in ["ASIC", "Bitaxe"]:
logging.warning(f"Worker {self.name}: Invalid type '{self.type}', using 'ASIC'")
self.type = "ASIC"
def get_normalized_hashrate(self, timeframe: str = "3hr") -> float:
"""
Get normalized hashrate in TH/s.
Args:
timeframe: The timeframe to get ("3hr" or "60sec")
Returns:
float: Normalized hashrate in TH/s
"""
if timeframe == "3hr":
return convert_to_ths(self.hashrate_3hr, self.hashrate_3hr_unit)
elif timeframe == "60sec":
return convert_to_ths(self.hashrate_60sec, self.hashrate_60sec_unit)
return 0.0
def to_dict(self) -> Dict[str, Any]:
"""Convert the WorkerData object to a dictionary."""
return {k: v for k, v in self.__dict__.items()}
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'WorkerData':
"""Create a WorkerData instance from a dictionary."""
filtered_data = {}
for k, v in data.items():
if k in cls.__annotations__:
filtered_data[k] = v
return cls(**filtered_data)
class HashRateConversionError(Exception):
"""Exception raised for errors in hashrate unit conversion."""
pass
def convert_to_ths(value, unit):
"""
Convert any hashrate unit to TH/s equivalent.

548
notification_service.py Normal file
View File

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

View File

@ -5,22 +5,29 @@ This document provides a comprehensive overview of the Bitcoin Mining Dashboard
## Directory Structure
```
bitcoin-mining-dashboard/
DeepSea-Dashboard/
├── App.py # Main application entry point and Flask routes
├── config.py # Configuration management utilities
├── config.json # User configuration file
├── data_service.py # Service for fetching mining/market data
├── models.py # Data models and conversion utilities
├── state_manager.py # Manager for persistent state and history
├── App.py # Main application entry point
├── config.py # Configuration management
├── config.json # Configuration file
├── data_service.py # Service for fetching mining data
├── models.py # Data models
├── state_manager.py # Manager for persistent state
├── worker_service.py # Service for worker data management
├── notification_service.py # Service for notifications
├── minify.py # Script for minifying assets
├── setup.py # Setup script for organizing files
├── requirements.txt # Python dependencies
├── Dockerfile # Docker configuration
├── docker-compose.yml # Docker Compose configuration
├── templates/ # HTML templates
│ ├── base.html # Base template with common elements
│ ├── boot.html # Boot sequence animation
│ ├── dashboard.html # Main dashboard template
│ ├── workers.html # Workers overview template
│ ├── workers.html # Workers dashboard template
│ ├── blocks.html # Bitcoin blocks template
│ ├── notifications.html # Notifications template
│ └── error.html # Error page template
├── static/ # Static assets
@ -30,23 +37,24 @@ bitcoin-mining-dashboard/
│ │ ├── workers.css # Workers page styles
│ │ ├── boot.css # Boot sequence styles
│ │ ├── blocks.css # Blocks page styles
│ │ ├── notifications.css # Notifications page styles
│ │ ├── error.css # Error page styles
│ │ └── retro-refresh.css # Floating system monitor styles
│ │ ├── retro-refresh.css # Floating refresh bar styles
│ │ └── theme-toggle.css # Theme toggle styles
│ │
│ └── js/ # JavaScript files
│ ├── main.js # Main dashboard functionality
│ ├── workers.js # Workers page functionality
│ ├── blocks.js # Blocks page functionality
│ ├── notifications.js # Notifications functionality
│ ├── block-animation.js # Block mining animation
│ └── BitcoinProgressBar.js # System monitor implementation
│ ├── BitcoinProgressBar.js # System monitor functionality
│ └── theme.js # Theme toggle functionality
├── logs/ # Application logs directory
├── requirements.txt # Python dependencies
├── Dockerfile # Docker configuration
├── setup.py # Setup script for organizing files
├── deployment_steps.md # Deployment documentation
├── project_structure.md # This document
└── README.md # Project overview and instructions
├── deployment_steps.md # Deployment guide
├── project_structure.md # Additional structure documentation
├── LICENSE.md # License information
└── logs/ # Application logs (generated at runtime)
```
## 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:
- **main.js**: Dashboard functionality, real-time updates, and chart rendering
- **workers.js**: Worker grid rendering, filtering, and mini-chart creation
- **blocks.js**: Block explorer with data fetching from mempool.space
- **blocks.js**: Block explorer with data fetching from mempool.guide
- **block-animation.js**: Interactive block mining animation
- **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

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

View File

@ -43,7 +43,7 @@ except ImportError:
DIRECTORIES = [
'static/css',
'static/js',
'static/img',
'static/js/min', # For minified JS files
'templates',
'logs',
'data' # For temporary data storage
@ -59,13 +59,16 @@ FILE_MAPPINGS = {
'error.css': 'static/css/error.css',
'retro-refresh.css': 'static/css/retro-refresh.css',
'blocks.css': 'static/css/blocks.css',
'notifications.css': 'static/css/notifications.css',
'theme-toggle.css': 'static/css/theme-toggle.css', # Added theme-toggle.css
# JS files
'main.js': 'static/js/main.js',
'workers.js': 'static/js/workers.js',
'blocks.js': 'static/js/blocks.js',
'block-animation.js': 'static/js/block-animation.js',
'BitcoinProgressBar.js': 'static/js/BitcoinProgressBar.js',
'notifications.js': 'static/js/notifications.js',
'theme.js': 'static/js/theme.js', # Added theme.js
# Template files
'base.html': 'templates/base.html',
@ -74,13 +77,16 @@ FILE_MAPPINGS = {
'boot.html': 'templates/boot.html',
'error.html': 'templates/error.html',
'blocks.html': 'templates/blocks.html',
'notifications.html': 'templates/notifications.html',
}
# Default configuration
DEFAULT_CONFIG = {
"power_cost": 0.0,
"power_usage": 0.0,
"wallet": "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9"
"wallet": "yourwallethere",
"timezone": "America/Los_Angeles", # Added default timezone
"network_fee": 0.0 # Added default network fee
}
def parse_arguments():
@ -90,9 +96,13 @@ def parse_arguments():
parser.add_argument('--wallet', type=str, help='Set your Ocean.xyz wallet address')
parser.add_argument('--power-cost', type=float, help='Set your electricity cost per kWh')
parser.add_argument('--power-usage', type=float, help='Set your power consumption in watts')
parser.add_argument('--network-fee', type=float, help='Set your network fee percentage') # Added network fee parameter
parser.add_argument('--timezone', type=str, help='Set your timezone (e.g., America/Los_Angeles)') # Added timezone parameter
parser.add_argument('--skip-checks', action='store_true', help='Skip dependency checks')
parser.add_argument('--force', action='store_true', help='Force file overwrite')
parser.add_argument('--config', type=str, help='Path to custom config.json')
parser.add_argument('--minify', action='store_true', help='Minify JavaScript files')
parser.add_argument('--theme', choices=['bitcoin', 'deepsea'], help='Set the default UI theme') # Added theme parameter
return parser.parse_args()
def create_directory_structure():
@ -158,6 +168,52 @@ def move_files(force=False):
return success
def minify_js_files():
"""Minify JavaScript files."""
logger.info("Minifying JavaScript files...")
try:
import jsmin
except ImportError:
logger.error("jsmin package not found. Installing...")
try:
subprocess.run([sys.executable, "-m", "pip", "install", "jsmin"], check=True)
import jsmin
logger.info("✓ jsmin package installed successfully")
except Exception as e:
logger.error(f"Failed to install jsmin: {str(e)}")
logger.error("Please run: pip install jsmin")
return False
js_dir = 'static/js'
min_dir = os.path.join(js_dir, 'min')
os.makedirs(min_dir, exist_ok=True)
minified_count = 0
for js_file in os.listdir(js_dir):
if js_file.endswith('.js') and not js_file.endswith('.min.js'):
input_path = os.path.join(js_dir, js_file)
output_path = os.path.join(min_dir, js_file.replace('.js', '.min.js'))
try:
with open(input_path, 'r') as f:
js_content = f.read()
# Minify the content
minified = jsmin.jsmin(js_content)
# Write minified content
with open(output_path, 'w') as f:
f.write(minified)
minified_count += 1
logger.debug(f"Minified {js_file}")
except Exception as e:
logger.error(f"Failed to minify {js_file}: {str(e)}")
logger.info(f"✓ JavaScript minification completed: {minified_count} files processed")
return True
def validate_wallet_address(wallet):
"""
Validate Bitcoin wallet address format.
@ -226,6 +282,19 @@ def create_config(args):
else:
logger.warning("Power usage cannot be negative, using default or existing value")
# Update config from command line arguments
if args.timezone:
config["timezone"] = args.timezone
if args.network_fee is not None:
if args.network_fee >= 0:
config["network_fee"] = args.network_fee
else:
logger.warning("Network fee cannot be negative, using default or existing value")
if args.theme:
config["theme"] = args.theme
# Save the configuration
try:
with open(config_file, 'w') as f:
@ -239,7 +308,9 @@ def create_config(args):
logger.info("Current configuration:")
logger.info(f" ├── Wallet address: {config['wallet']}")
logger.info(f" ├── Power cost: ${config['power_cost']} per kWh")
logger.info(f" └── Power usage: {config['power_usage']} watts")
logger.info(f" ├── Power usage: {config['power_usage']} watts")
logger.info(f" ├── Network fee: {config['network_fee']}%")
logger.info(f" └── Timezone: {config['timezone']}")
return True
@ -423,6 +494,11 @@ def main():
logger.error("Failed to create configuration file.")
return 1
# Minify JavaScript files if requested
if args.minify:
if not minify_js_files():
logger.warning("JavaScript minification failed, but continuing...")
# Check Redis if available
check_redis()

View File

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

View File

@ -57,6 +57,10 @@
flex-direction: column;
}
.stat-item strong {
color: #f7931a; /* Use the Bitcoin orange color for labels */
}
/* Blocks grid */
.blocks-container {
overflow-x: auto;
@ -108,7 +112,6 @@
font-size: 1.2rem;
font-weight: bold;
color: var(--primary-color);
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
}
.block-time {
@ -138,27 +141,22 @@
.block-info-value.yellow {
color: #ffd700;
text-shadow: 0 0 8px rgba(255, 215, 0, 0.6);
}
.block-info-value.green {
color: #32CD32;
text-shadow: 0 0 6px rgba(50, 205, 50, 0.6);
}
.block-info-value.blue {
color: #00dfff;
text-shadow: 0 0 6px rgba(0, 223, 255, 0.6);
}
.block-info-value.white {
color: #ffffff;
text-shadow: 0 0 6px rgba(255, 255, 255, 0.6);
}
.block-info-value.red {
color: #ff5555;
text-shadow: 0 0 6px rgba(255, 85, 85, 0.6);
}
/* Loader */
@ -217,7 +215,6 @@
padding: 0.5rem 1rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite;
font-family: var(--header-font);
display: flex;
@ -238,7 +235,6 @@
.block-modal-close:hover,
.block-modal-close:focus {
color: #ffa500;
text-shadow: 0 0 10px rgba(255, 165, 0, 0.8);
}
.block-modal-body {
@ -261,7 +257,6 @@
font-size: 1.1rem;
color: var(--primary-color);
margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
font-weight: bold;
}

View File

@ -1,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 */
body {
background: linear-gradient(135deg, #121212, #000000);
@ -8,7 +447,6 @@ body {
margin: 0;
padding: 10px;
overflow-x: hidden;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.4);
height: calc(100vh - 100px);
display: flex;
flex-direction: column;
@ -79,32 +517,26 @@ body::before {
/* Neon-inspired color classes */
.green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.blue {
color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
}
.yellow {
color: #ffd700 !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
}
.white {
color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
}
.red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.magenta {
color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
}
/* Bitcoin Logo styling with extra neon border */
@ -143,6 +575,7 @@ body::before {
font-size: 16px;
box-shadow: 0 0 8px rgba(247, 147, 26, 0.5);
transition: all 0.2s ease;
z-index: 50; /* Lower z-index value */
}
#skip-button:hover {
@ -150,6 +583,24 @@ body::before {
box-shadow: 0 0 12px rgba(247, 147, 26, 0.7);
}
/* Mobile-specific adjustments */
@media (max-width: 768px) {
#skip-button {
bottom: 25px;
right: 10px;
padding: 10px 18px; /* Larger touch target for mobile */
font-size: 18px;
height: 40px;
z-index: 50;
}
}
/* Add this to your CSS */
#config-form {
z-index: 100; /* Higher than the skip button */
position: relative; /* Needed for z-index to work properly */
}
/* Prompt Styling */
#prompt-container {
display: none;
@ -159,7 +610,6 @@ body::before {
#prompt-text {
color: #f7931a;
margin-right: 5px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline;
}
@ -175,7 +625,6 @@ body::before {
height: 33px;
padding: 0;
margin: 0;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
display: inline-block;
vertical-align: top;
}
@ -203,7 +652,6 @@ body::before {
#loading-message {
text-align: center;
margin-bottom: 10px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
}
#debug-info {

View File

@ -1,14 +1,85 @@
.footer {
margin-top: 30px;
padding: 10px 0;
color: grey;
font-size: 0.9rem;
border-top: 1px solid rgba(128, 128, 128, 0.2);
}
</style >
<!-- Preload theme to prevent flicker -->
<style id="theme-preload" >
/* Theme-aware loading state */
html.bitcoin-theme {
background-color: #111111;
}
html.deepsea-theme {
background-color: #0c141a;
}
#theme-loader {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 9999;
font-family: 'VT323', monospace;
}
html.bitcoin-theme #theme-loader {
background-color: #111111;
color: #f2a900;
}
html.deepsea-theme #theme-loader {
background-color: #0c141a;
color: #0088cc;
}
#loader-icon {
font-size: 48px;
margin-bottom: 20px;
animation: spin 2s infinite linear;
}
#loader-text {
font-size: 24px;
text-transform: uppercase;
}
@keyframes spin {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
/* Hide content during load */
body {
visibility: hidden;
}
/* Common styling elements shared across all pages */
:root {
--bg-color: #0a0a0a;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #1a1a1a);
--primary-color: #f7931a;
--accent-color: #00ffff;
--text-color: #ffffff;
--card-padding: 0.5rem;
--text-size-base: 16px;
--terminal-font: 'VT323', monospace;
--header-font: 'Orbitron', sans-serif;
--bg-color: #0a0a0a;
--bg-gradient: linear-gradient(135deg, #0a0a0a, #1a1a1a);
--primary-color: #f7931a;
--accent-color: #00ffff;
--text-color: #ffffff;
--card-padding: 0.5rem;
--text-size-base: 16px;
--terminal-font: 'VT323', monospace;
--header-font: 'Orbitron', sans-serif;
--text-transform: uppercase;
}
@media (min-width: 768px) {
@ -46,11 +117,12 @@ body::before {
}
body {
background: var(--bg-gradient);
color: var(--text-color);
padding-top: 0.5rem;
font-size: var(--text-size-base);
font-family: var(--terminal-font);
background: var(--bg-gradient);
color: var(--text-color);
padding-top: 0.5rem;
font-size: var(--text-size-base);
font-family: var(--terminal-font);
text-transform: uppercase;
}
h1 {
@ -59,7 +131,6 @@ h1 {
color: var(--primary-color);
font-family: var(--header-font);
letter-spacing: 1px;
text-shadow: 0 0 10px var(--primary-color);
animation: flicker 4s infinite;
}
@ -107,16 +178,23 @@ h1 {
right: 10px;
color: grey;
text-decoration: none;
font-size: 0.9rem;
text-shadow: 0 0 5px grey;
font-size: 0.7rem; /* Decreased font size */
padding: 5px 10px; /* Add padding for a larger clickable area */
transition: background-color 0.3s ease; /* Optional: Add hover effect */
}
#topRightLink:hover {
background-color: rgba(255, 255, 255, 0.1); /* Optional: Highlight on hover */
}
/* Card styles */
.card,
.card-header,
.card-body,
.card-footer {
border-radius: 0 !important;
text-transform: uppercase;
}
/* Enhanced card with scanlines */
@ -157,7 +235,6 @@ h1 {
padding: 0.3rem 0.5rem;
font-size: 1.1rem;
border-bottom: 1px solid var(--primary-color);
text-shadow: 0 0 5px var(--primary-color);
animation: flicker 4s infinite;
font-family: var(--header-font);
}
@ -179,7 +256,6 @@ h1 {
border-radius: 5px;
z-index: 9999;
font-size: 0.9rem;
text-shadow: 0 0 5px rgba(255, 0, 0, 0.8);
box-shadow: 0 0 10px rgba(255, 0, 0, 0.5);
}
@ -224,8 +300,8 @@ h1 {
border-radius: 50%;
margin-left: 0.5em;
position: relative;
top: -2px;
animation: glow 1s infinite;
top: -1px;
animation: glow 3s infinite;
box-shadow: 0 0 10px #32CD32, 0 0 20px #32CD32;
}
@ -235,14 +311,16 @@ h1 {
}
.offline-dot {
display: inline-block;
width: 8px;
height: 8px;
background: red;
border-radius: 50%;
margin-left: 0.5em;
animation: glowRed 1s infinite;
box-shadow: 0 0 10px red, 0 0 20px red;
display: inline-block;
width: 8px;
height: 8px;
background: red;
border-radius: 50%;
margin-left: 0.5em;
position: relative;
top: -1px;
animation: glow 3s infinite;
box-shadow: 0 0 10px red, 0 0 20px red !important;
}
@keyframes glowRed {
@ -253,58 +331,48 @@ h1 {
/* Color utility classes */
.green-glow, .status-green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.red-glow, .status-red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.yellow-glow {
color: #ffd700 !important;
text-shadow: 0 0 6px #ffd700, 0 0 12px #ffd700;
}
.blue-glow {
color: #00dfff !important;
text-shadow: 0 0 6px #00dfff, 0 0 12px #00dfff;
}
.white-glow {
color: #ffffff !important;
text-shadow: 0 0 6px #ffffff, 0 0 12px #ffffff;
}
/* Basic color classes for backward compatibility */
.green {
color: #39ff14 !important;
text-shadow: 0 0 10px #39ff14, 0 0 20px #39ff14;
}
.blue {
color: #00dfff !important;
text-shadow: 0 0 10px #00dfff, 0 0 20px #00dfff;
}
.yellow {
color: #ffd700 !important;
text-shadow: 0 0 8px #ffd700, 0 0 16px #ffd700;
font-weight: normal !important;
}
.white {
color: #ffffff !important;
text-shadow: 0 0 8px #ffffff, 0 0 16px #ffffff;
}
.red {
color: #ff2d2d !important;
text-shadow: 0 0 10px #ff2d2d, 0 0 20px #ff2d2d;
}
.magenta {
color: #ff2d95 !important;
text-shadow: 0 0 10px #ff2d95, 0 0 20px #ff2d95;
}
/* Bitcoin Progress Bar Styles */
@ -387,7 +455,6 @@ h1 {
font-size: 1rem;
color: var(--primary-color);
margin-top: 0.3rem;
text-shadow: 0 0 5px var(--primary-color);
text-align: center;
width: 100%;
}
@ -418,3 +485,17 @@ h1 {
margin-bottom: 0.5rem;
}
}
/* Navigation badges for notifications */
.nav-badge {
background-color: var(--primary-color);
color: var(--bg-color);
border-radius: 10px;
font-size: 0.7rem;
padding: 1px 5px;
min-width: 16px;
text-align: center;
display: none;
margin-left: 5px;
vertical-align: middle;
}

View File

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

View File

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

View File

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

View File

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

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

File diff suppressed because it is too large Load Diff

View File

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

503
static/js/notifications.js Normal file
View File

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

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

@ -3,9 +3,9 @@
// Global variables for workers dashboard
let workerData = null;
let refreshTimer;
let pageLoadTime = Date.now();
const pageLoadTime = Date.now();
let lastManualRefreshTime = 0;
let filterState = {
const filterState = {
currentFilter: 'all',
searchTerm: ''
};
@ -21,67 +21,54 @@ const MIN_REFRESH_INTERVAL = 10000; // Minimum 10 seconds between refreshes
// Hashrate Normalization Utilities
// Helper function to normalize hashrate to TH/s for consistent graphing
function normalizeHashrate(value, unit) {
function normalizeHashrate(value, unit = 'th/s') {
if (!value || isNaN(value)) return 0;
unit = (unit || 'th/s').toLowerCase();
if (unit.includes('ph/s')) {
return value * 1000; // Convert PH/s to TH/s
} else if (unit.includes('eh/s')) {
return value * 1000000; // Convert EH/s to TH/s
} else if (unit.includes('gh/s')) {
return value / 1000; // Convert GH/s to TH/s
} else if (unit.includes('mh/s')) {
return value / 1000000; // Convert MH/s to TH/s
} else if (unit.includes('kh/s')) {
return value / 1000000000; // Convert KH/s to TH/s
} else if (unit.includes('h/s') && !unit.includes('th/s') && !unit.includes('ph/s') &&
!unit.includes('eh/s') && !unit.includes('gh/s') && !unit.includes('mh/s') &&
!unit.includes('kh/s')) {
return value / 1000000000000; // Convert H/s to TH/s
} else {
// Assume TH/s if unit is not recognized
return value;
}
unit = unit.toLowerCase();
const unitConversion = {
'ph/s': 1000,
'eh/s': 1000000,
'gh/s': 1 / 1000,
'mh/s': 1 / 1000000,
'kh/s': 1 / 1000000000,
'h/s': 1 / 1000000000000
};
return unitConversion[unit] !== undefined ? value * unitConversion[unit] : value;
}
// Helper function to format hashrate values for display
function formatHashrateForDisplay(value, unit) {
if (isNaN(value) || value === null || value === undefined) return "N/A";
// Always normalize to TH/s first if unit is provided
let normalizedValue = unit ? normalizeHashrate(value, unit) : value;
const normalizedValue = unit ? normalizeHashrate(value, unit) : value;
const unitRanges = [
{ threshold: 1000000, unit: 'EH/s', divisor: 1000000 },
{ threshold: 1000, unit: 'PH/s', divisor: 1000 },
{ threshold: 1, unit: 'TH/s', divisor: 1 },
{ threshold: 0.001, unit: 'GH/s', divisor: 1 / 1000 },
{ threshold: 0, unit: 'MH/s', divisor: 1 / 1000000 }
];
// Select appropriate unit based on magnitude
if (normalizedValue >= 1000000) { // EH/s range
return (normalizedValue / 1000000).toFixed(2) + ' EH/s';
} else if (normalizedValue >= 1000) { // PH/s range
return (normalizedValue / 1000).toFixed(2) + ' PH/s';
} else if (normalizedValue >= 1) { // TH/s range
return normalizedValue.toFixed(2) + ' TH/s';
} else if (normalizedValue >= 0.001) { // GH/s range
return (normalizedValue * 1000).toFixed(2) + ' GH/s';
} else { // MH/s range or smaller
return (normalizedValue * 1000000).toFixed(2) + ' MH/s';
for (const range of unitRanges) {
if (normalizedValue >= range.threshold) {
return (normalizedValue / range.divisor).toFixed(2) + ' ' + range.unit;
}
}
return (normalizedValue * 1000000).toFixed(2) + ' MH/s';
}
// Initialize the page
$(document).ready(function () {
console.log("Worker page initializing...");
// Set up initial UI
initNotificationBadge();
initializePage();
// Get server time for uptime calculation
updateServerTime();
// Define global refresh function for BitcoinMinuteRefresh
window.manualRefresh = fetchWorkerData;
// Wait before initializing BitcoinMinuteRefresh to ensure DOM is ready
setTimeout(function () {
// Initialize BitcoinMinuteRefresh with our refresh function
setTimeout(() => {
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.initialize) {
BitcoinMinuteRefresh.initialize(window.manualRefresh);
console.log("BitcoinMinuteRefresh initialized with refresh function");
@ -90,10 +77,8 @@ $(document).ready(function () {
}
}, 500);
// Fetch worker data immediately on page load
fetchWorkerData();
// Set up filter button click handlers
$('.filter-button').click(function () {
$('.filter-button').removeClass('active');
$(this).addClass('active');
@ -101,26 +86,56 @@ $(document).ready(function () {
filterWorkers();
});
// Set up search input handler
$('#worker-search').on('input', function () {
filterState.searchTerm = $(this).val().toLowerCase();
filterWorkers();
});
});
// Load timezone setting early
(function loadTimezoneEarly() {
// First try to get from localStorage for instant access
try {
const storedTimezone = localStorage.getItem('dashboardTimezone');
if (storedTimezone) {
window.dashboardTimezone = storedTimezone;
console.log(`Using cached timezone: ${storedTimezone}`);
}
} catch (e) {
console.error("Error reading timezone from localStorage:", e);
}
// Then fetch from server to ensure we have the latest setting
fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
window.dashboardTimezone = data.timezone;
console.log(`Set timezone from server: ${data.timezone}`);
// Cache for future use
try {
localStorage.setItem('dashboardTimezone', data.timezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
}
})
.catch(error => {
console.error("Error fetching timezone:", error);
});
})();
// Initialize page elements
function initializePage() {
console.log("Initializing page elements...");
// Initialize mini chart for total hashrate if the element exists
if (document.getElementById('total-hashrate-chart')) {
initializeMiniChart();
}
// Show loading state
$('#worker-grid').html('<div class="text-center p-5"><i class="fas fa-spinner fa-spin"></i> Loading worker data...</div>');
// Add retry button (hidden by default)
if (!$('#retry-button').length) {
$('body').append('<button id="retry-button" style="position: fixed; bottom: 20px; left: 20px; z-index: 1000; background: #f7931a; color: black; border: none; padding: 8px 16px; display: none; border-radius: 4px; cursor: pointer;">Retry Loading Data</button>');
@ -134,11 +149,34 @@ function initializePage() {
}
}
// Update unread notifications badge in navigation
function updateNotificationBadge() {
$.ajax({
url: "/api/notifications/unread_count",
method: "GET",
success: function (data) {
const unreadCount = data.unread_count;
const badge = $("#nav-unread-badge");
if (unreadCount > 0) {
badge.text(unreadCount).show();
} else {
badge.hide();
}
}
});
}
// Initialize notification badge checking
function initNotificationBadge() {
updateNotificationBadge();
setInterval(updateNotificationBadge, 60000);
}
// Server time update via polling - enhanced to use shared storage
function updateServerTime() {
console.log("Updating server time...");
// First try to get stored values
try {
const storedOffset = localStorage.getItem('serverTimeOffset');
const storedStartTime = localStorage.getItem('serverStartTime');
@ -148,31 +186,26 @@ function updateServerTime() {
serverStartTime = parseFloat(storedStartTime);
console.log("Using stored server time offset:", serverTimeOffset, "ms");
// Only update BitcoinMinuteRefresh if it's initialized
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
}
return; // Don't fetch if we have valid values
return;
}
} catch (e) {
console.error("Error reading stored server time:", e);
}
// Fetch from API if needed
$.ajax({
url: "/api/time",
method: "GET",
timeout: 5000,
success: function (data) {
// Calculate the offset between server time and local time
serverTimeOffset = new Date(data.server_timestamp).getTime() - Date.now();
serverStartTime = new Date(data.server_start_time).getTime();
// Store in localStorage for cross-page sharing
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
localStorage.setItem('serverStartTime', serverStartTime.toString());
// Only update BitcoinMinuteRefresh if it's initialized
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.updateServerTime) {
BitcoinMinuteRefresh.updateServerTime(serverTimeOffset, serverStartTime);
}
@ -185,84 +218,89 @@ function updateServerTime() {
});
}
// Fetch worker data from API
// Utility functions to show/hide loader
function showLoader() {
$("#loader").show();
}
function hideLoader() {
$("#loader").hide();
}
// Fetch worker data from API with pagination, limiting to 10 pages
function fetchWorkerData(forceRefresh = false) {
console.log("Fetching worker data...");
// Track this as a manual refresh for throttling purposes
lastManualRefreshTime = Date.now();
$('#worker-grid').addClass('loading-fade');
showLoader();
// Choose API URL based on whether we're forcing a refresh
const apiUrl = `/api/workers${forceRefresh ? '?force=true' : ''}`;
const maxPages = 10;
const requests = [];
$.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
timeout: 15000, // 15 second timeout
success: function (data) {
if (!data || !data.workers || data.workers.length === 0) {
console.warn("No workers found in data response");
$('#worker-grid').html(`
<div class="text-center p-5">
<p>No workers found. Try refreshing the page.</p>
</div>
`);
return;
}
// Create requests for pages 1 through maxPages concurrently
for (let page = 1; page <= maxPages; page++) {
const apiUrl = `/api/workers?page=${page}${forceRefresh ? '&force=true' : ''}`;
requests.push($.ajax({
url: apiUrl,
method: 'GET',
dataType: 'json',
timeout: 15000
}));
}
workerData = data;
// Process all requests concurrently
Promise.all(requests)
.then(pages => {
let allWorkers = [];
let aggregatedData = null;
pages.forEach((data, i) => {
if (data && data.workers && data.workers.length > 0) {
allWorkers = allWorkers.concat(data.workers);
if (i === 0) {
aggregatedData = data; // preserve stats from first page
}
} else {
console.warn(`No workers found on page ${i + 1}`);
}
});
// Deduplicate workers if necessary (using worker.name as unique key)
const uniqueWorkers = allWorkers.filter((worker, index, self) =>
index === self.findIndex((w) => w.name === worker.name)
);
workerData = aggregatedData || {};
workerData.workers = uniqueWorkers;
// Notify BitcoinMinuteRefresh that we've refreshed the data
if (typeof BitcoinMinuteRefresh !== 'undefined' && BitcoinMinuteRefresh.notifyRefresh) {
BitcoinMinuteRefresh.notifyRefresh();
}
// Update UI with new data
updateWorkerGrid();
updateSummaryStats();
updateMiniChart();
updateLastUpdated();
// Hide retry button
$('#retry-button').hide();
// Reset connection retry count
connectionRetryCount = 0;
console.log("Worker data updated successfully");
},
error: function (xhr, status, error) {
console.error("Error fetching worker data:", error);
// Show error in worker grid
$('#worker-grid').html(`
<div class="text-center p-5 text-danger">
<i class="fas fa-exclamation-triangle"></i>
<p>Error loading worker data: ${error || 'Unknown error'}</p>
</div>
`);
// Show retry button
$('#retry-button').show();
// Implement exponential backoff for automatic retry
connectionRetryCount++;
const delay = Math.min(30000, 1000 * Math.pow(1.5, Math.min(5, connectionRetryCount)));
console.log(`Will retry in ${delay / 1000} seconds (attempt ${connectionRetryCount})`);
setTimeout(() => {
fetchWorkerData(true); // Force refresh on retry
}, delay);
},
complete: function () {
$('#worker-grid').removeClass('loading-fade');
}
});
})
.catch(error => {
console.error("Error fetching worker data:", error);
})
.finally(() => {
hideLoader();
});
}
// Refresh worker data every 60 seconds
setInterval(function () {
console.log("Refreshing worker data at " + new Date().toLocaleTimeString());
fetchWorkerData();
}, 60000);
// Update the worker grid with data
function updateWorkerGrid() {
console.log("Updating worker grid...");
@ -275,7 +313,6 @@ function updateWorkerGrid() {
const workerGrid = $('#worker-grid');
workerGrid.empty();
// Apply current filters before rendering
const filteredWorkers = filterWorkersData(workerData.workers);
if (filteredWorkers.length === 0) {
@ -288,107 +325,91 @@ function updateWorkerGrid() {
return;
}
// Generate worker cards
filteredWorkers.forEach(worker => {
// Create worker card
const card = $('<div class="worker-card"></div>');
// Add class based on status
if (worker.status === 'online') {
card.addClass('worker-card-online');
} else {
card.addClass('worker-card-offline');
}
// Add worker type badge
card.append(`<div class="worker-type">${worker.type}</div>`);
// Add worker name
card.append(`<div class="worker-name">${worker.name}</div>`);
// Add status badge
if (worker.status === 'online') {
card.append('<div class="status-badge status-badge-online">ONLINE</div>');
} else {
card.append('<div class="status-badge status-badge-offline">OFFLINE</div>');
}
// Add hashrate bar with normalized values for consistent display
const maxHashrate = 200; // TH/s - adjust based on your fleet
const normalizedHashrate = normalizeHashrate(
worker.hashrate_3hr,
worker.hashrate_3hr_unit || 'th/s'
);
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
// Format hashrate for display with appropriate unit
const formattedHashrate = formatHashrateForDisplay(
worker.hashrate_3hr,
worker.hashrate_3hr_unit || 'th/s'
);
card.append(`
<div class="worker-stats-row">
<div class="worker-stats-label">Hashrate (3hr):</div>
<div class="white-glow">${formattedHashrate}</div>
</div>
<div class="stats-bar-container">
<div class="stats-bar" style="width: ${hashratePercent}%"></div>
</div>
`);
// Add additional stats
card.append(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${typeof worker.last_share === 'string' ? worker.last_share.split(' ')[1] || worker.last_share : 'N/A'}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div>
<div class="green-glow">${worker.earnings.toFixed(8)}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Accept Rate:</div>
<div class="white-glow">${worker.acceptance_rate}%</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Temp:</div>
<div class="${worker.temperature > 65 ? 'red-glow' : 'white-glow'}">${worker.temperature > 0 ? worker.temperature + '°C' : 'N/A'}</div>
</div>
</div>
`);
// Add card to grid
const card = createWorkerCard(worker);
workerGrid.append(card);
});
}
// Create worker card element
function createWorkerCard(worker) {
const card = $('<div class="worker-card"></div>');
card.addClass(worker.status === 'online' ? 'worker-card-online' : 'worker-card-offline');
card.append(`<div class="worker-type">${worker.type}</div>`);
card.append(`<div class="worker-name">${worker.name}</div>`);
card.append(`<div class="status-badge ${worker.status === 'online' ? 'status-badge-online' : 'status-badge-offline'}">${worker.status.toUpperCase()}</div>`);
const maxHashrate = 125; // TH/s - adjust based on your fleet
const normalizedHashrate = normalizeHashrate(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
const hashratePercent = Math.min(100, (normalizedHashrate / maxHashrate) * 100);
const formattedHashrate = formatHashrateForDisplay(worker.hashrate_3hr, worker.hashrate_3hr_unit || 'th/s');
card.append(`
<div class="worker-stats-row">
<div class="worker-stats-label">Hashrate (3hr):</div>
<div class="white-glow">${formattedHashrate}</div>
</div>
<div class="stats-bar-container">
<div class="stats-bar" style="width: ${hashratePercent}%"></div>
</div>
`);
// Format the last share using the proper method for timezone conversion
let formattedLastShare = 'N/A';
if (worker.last_share && typeof worker.last_share === 'string') {
// This is a more reliable method for timezone conversion
try {
// The worker.last_share is likely in format "YYYY-MM-DD HH:MM"
// We need to consider it as UTC and convert to the configured timezone
// Create a proper date object, ensuring UTC interpretation
const dateWithoutTZ = new Date(worker.last_share + 'Z'); // Adding Z to treat as UTC
// Format it according to the configured timezone
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
});
} catch (e) {
console.error("Error formatting last share time:", e, worker.last_share);
formattedLastShare = worker.last_share; // Fallback to original value
}
}
card.append(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${formattedLastShare}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div>
<div class="green-glow">${worker.earnings.toFixed(8)}</div>
</div>
</div>
`);
return card;
}
// Filter worker data based on current filter state
function filterWorkersData(workers) {
if (!workers) return [];
return workers.filter(worker => {
// Default to empty string if name is undefined
const workerName = (worker.name || '').toLowerCase();
const isOnline = worker.status === 'online';
const workerType = (worker.type || '').toLowerCase();
// Check if worker matches filter
let matchesFilter = false;
if (filterState.currentFilter === 'all') {
matchesFilter = true;
} else if (filterState.currentFilter === 'online' && isOnline) {
matchesFilter = true;
} else if (filterState.currentFilter === 'offline' && !isOnline) {
matchesFilter = true;
} else if (filterState.currentFilter === 'asic' && workerType === 'asic') {
matchesFilter = true;
} else if (filterState.currentFilter === 'fpga' && workerType === 'fpga') {
matchesFilter = true;
}
const matchesFilter = filterState.currentFilter === 'all' ||
(filterState.currentFilter === 'online' && isOnline) ||
(filterState.currentFilter === 'offline' && !isOnline) ||
(filterState.currentFilter === 'asic' && workerType === 'asic') ||
(filterState.currentFilter === 'bitaxe' && workerType === 'bitaxe');
// Check if worker matches search term
const matchesSearch = filterState.searchTerm === '' || workerName.includes(filterState.searchTerm);
return matchesFilter && matchesSearch;
@ -398,41 +419,27 @@ function filterWorkersData(workers) {
// Apply filter to rendered worker cards
function filterWorkers() {
if (!workerData || !workerData.workers) return;
// Re-render the worker grid with current filters
updateWorkerGrid();
}
// Modified updateSummaryStats function with normalized hashrate display
// Update summary stats with normalized hashrate display
function updateSummaryStats() {
if (!workerData) return;
// Update worker counts
$('#workers-count').text(workerData.workers_total || 0);
$('#workers-online').text(workerData.workers_online || 0);
$('#workers-offline').text(workerData.workers_offline || 0);
// Update worker ring percentage
const onlinePercent = workerData.workers_total > 0 ?
workerData.workers_online / workerData.workers_total : 0;
const onlinePercent = workerData.workers_total > 0 ? workerData.workers_online / workerData.workers_total : 0;
$('.worker-ring').css('--online-percent', onlinePercent);
// Display normalized hashrate with appropriate unit
if (workerData.total_hashrate !== undefined) {
// Format with proper unit conversion
const formattedHashrate = formatHashrateForDisplay(
workerData.total_hashrate,
workerData.hashrate_unit || 'TH/s'
);
$('#total-hashrate').text(formattedHashrate);
} else {
$('#total-hashrate').text(`0.0 TH/s`);
}
const formattedHashrate = workerData.total_hashrate !== undefined ?
formatHashrateForDisplay(workerData.total_hashrate, workerData.hashrate_unit || 'TH/s') :
'0.0 TH/s';
$('#total-hashrate').text(formattedHashrate);
// Update other summary stats
$('#total-earnings').text(`${(workerData.total_earnings || 0).toFixed(8)} BTC`);
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} sats`);
$('#avg-acceptance-rate').text(`${(workerData.avg_acceptance_rate || 0).toFixed(2)}%`);
$('#daily-sats').text(`${numberWithCommas(workerData.daily_sats || 0)} SATS`);
}
// Initialize mini chart
@ -445,7 +452,6 @@ function initializeMiniChart() {
return;
}
// Generate some sample data to start
const labels = Array(24).fill('').map((_, i) => i);
const data = Array(24).fill(0).map(() => Math.random() * 100 + 700);
@ -495,32 +501,23 @@ function updateMiniChart() {
return;
}
// Extract hashrate data from history
const historyData = workerData.hashrate_history;
if (!historyData || historyData.length === 0) {
console.log("No hashrate history data available");
return;
}
// Get the normalized values for the chart
const values = historyData.map(item => {
const val = parseFloat(item.value) || 0;
const unit = item.unit || workerData.hashrate_unit || 'th/s';
return normalizeHashrate(val, unit);
});
const values = historyData.map(item => normalizeHashrate(parseFloat(item.value) || 0, item.unit || workerData.hashrate_unit || 'th/s'));
const labels = historyData.map(item => item.time);
// Update chart data
miniChart.data.labels = labels;
miniChart.data.datasets[0].data = values;
// Update y-axis range
const min = Math.min(...values.filter(v => v > 0)) || 0;
const max = Math.max(...values) || 1;
miniChart.options.scales.y.min = min * 0.9;
miniChart.options.scales.y.max = max * 1.1;
// Update the chart
miniChart.update('none');
}
@ -530,10 +527,34 @@ function updateLastUpdated() {
try {
const timestamp = new Date(workerData.timestamp);
// Get the configured timezone with a fallback
const configuredTimezone = window.dashboardTimezone || 'America/Los_Angeles';
// Format with the configured timezone
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZone: configuredTimezone // Explicitly use the configured timezone
};
// Format the timestamp and update the DOM
const formattedTime = timestamp.toLocaleString('en-US', options);
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
timestamp.toLocaleString() + "<span id='terminal-cursor'></span>");
formattedTime + "<span id='terminal-cursor'></span>");
console.log(`Last updated timestamp using timezone: ${configuredTimezone}`);
} catch (e) {
console.error("Error formatting timestamp:", e);
// Fallback to basic timestamp if there's an error
$("#lastUpdated").html("<strong>Last Updated:</strong> " +
new Date().toLocaleString() + "<span id='terminal-cursor'></span>");
}
}

View File

@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Ocean.xyz Pool Mining Dashboard{% endblock %}</title>
<title>{% block title %}BTC-OS MINING DASHBOARD {% endblock %}</title>
<!-- Common fonts -->
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700&family=VT323&display=swap" rel="stylesheet">
@ -17,32 +17,110 @@
<!-- Common CSS -->
<link rel="stylesheet" href="/static/css/common.css">
<!-- Custom CSS -->
<link rel="stylesheet" href="/static/css/theme-toggle.css">
<!-- Theme JS (added to ensure consistent application of theme) -->
<script src="/static/js/theme.js"></script>
<!-- Page-specific CSS -->
{% block css %}{% endblock %}
<script>
// Execute this immediately to preload theme
(function () {
const useDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
const themeClass = useDeepSea ? 'deepsea-theme' : 'bitcoin-theme';
// Apply theme class to html element
document.documentElement.classList.add(themeClass);
// Create and add loader
document.addEventListener('DOMContentLoaded', function () {
// Create loader element
const loader = document.createElement('div');
loader.id = 'theme-loader';
const icon = document.createElement('div');
icon.id = 'loader-icon';
icon.innerHTML = useDeepSea ? '🌊' : '₿';
const text = document.createElement('div');
text.id = 'loader-text';
text.textContent = 'Loading ' + (useDeepSea ? 'DeepSea' : 'Bitcoin') + ' Theme';
loader.appendChild(icon);
loader.appendChild(text);
document.body.appendChild(loader);
// Add fade-in effect for content once theme is loaded
setTimeout(function () {
document.body.style.visibility = 'visible';
// Fade out loader
loader.style.transition = 'opacity 0.5s ease';
loader.style.opacity = '0';
// Remove loader after fade
setTimeout(function () {
if (loader && loader.parentNode) {
loader.parentNode.removeChild(loader);
}
}, 500);
}, 300);
});
})();
</script>
</head>
<body>
<script>
// Add underwater effects for DeepSea theme
document.addEventListener('DOMContentLoaded', function () {
// Check if DeepSea theme is active
if (localStorage.getItem('useDeepSeaTheme') === 'true') {
// Create underwater light rays
const rays = document.createElement('div');
rays.className = 'underwater-rays';
document.body.appendChild(rays);
// Create digital noise
const noise = document.createElement('div');
noise.className = 'digital-noise';
document.body.appendChild(noise);
}
});
</script>
<div class="container-fluid">
<!-- Connection status indicator -->
<div id="connectionStatus"></div>
<!-- Top right link -->
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">Made by @DJO₿leezy</a>
<h1 class="text-center">
<a href="/" style="text-decoration:none; color:inherit;">
{% block header %}Ocean.xyz Pool Mining Dashboard v 0.3{% endblock %}
{% block header %}BTC-OS MINING DASHBOARD{% endblock %}
</a>
</h1>
<!-- Top right link -->
<a href="https://x.com/DJObleezy" id="topRightLink" target="_blank" rel="noopener noreferrer">MADE BY @DJO₿LEEZY</a>
<!-- Theme toggle button (new) -->
<button id="themeToggle" class="theme-toggle-btn">
<span>Toggle Theme</span>
</button>
{% block last_updated %}
<p class="text-center" id="lastUpdated" style="color: #f7931a"><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 %}
{% block navigation %}
<div class="navigation-links">
<a href="/dashboard" class="nav-link {% block dashboard_active %}{% endblock %}">Main Dashboard</a>
<a href="/workers" class="nav-link {% block workers_active %}{% endblock %}">Workers Overview</a>
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">Bitcoin Blocks</a>
<a href="/dashboard" class="nav-link {% block dashboard_active %}{% endblock %}">DASHBOARD</a>
<a href="/workers" class="nav-link {% block workers_active %}{% endblock %}">WORKERS</a>
<a href="/blocks" class="nav-link {% block blocks_active %}{% endblock %}">BLOCKS</a>
<a href="/notifications" class="nav-link {% block notifications_active %}{% endblock %}">
NOTIFICATIONS
<span id="nav-unread-badge" class="nav-badge"></span>
</a>
</div>
{% endblock %}
@ -53,6 +131,11 @@
{% block congrats_message %}
<div id="congratsMessage" style="display:none; position: fixed; top: 20px; left: 50%; transform: translateX(-50%); z-index: 1000; background: #f7931a; color: #000; padding: 10px; border-radius: 5px; box-shadow: 0 0 15px rgba(247, 147, 26, 0.7);"></div>
{% endblock %}
<!-- Footer -->
<footer class="footer text-center">
<p>Not affiliated with <a href="https://www.Ocean.xyz">Ocean.xyz</a></p>
</footer>
</div>
<!-- External JavaScript libraries -->
@ -60,6 +143,32 @@
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@1.1.0"></script>
<!-- Theme toggle initialization -->
<script>
document.addEventListener('DOMContentLoaded', function () {
// Initialize theme toggle button based on current theme
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
// Check current theme
const isDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
// Update button style based on theme
if (isDeepSea) {
themeToggle.style.borderColor = '#0088cc';
themeToggle.style.color = '#0088cc';
} else {
themeToggle.style.borderColor = '#f2a900';
themeToggle.style.color = '#f2a900';
}
// Add click event listener
themeToggle.addEventListener('click', function () {
toggleTheme(); // This will now trigger a page refresh
});
}
});
</script>
<!-- Page-specific JavaScript -->
{% block javascript %}{% endblock %}

View File

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

View File

@ -6,170 +6,32 @@
<title>Ocean.xyz Pool Miner - Initializing...</title>
<link href="https://fonts.googleapis.com/css2?family=VT323&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/static/css/boot.css">
<style>
/* Added styles for configuration form */
#config-form {
display: none;
margin-top: 20px;
background-color: rgba(0, 0, 0, 0.7);
border: 1px solid #f7931a;
padding: 15px;
max-width: 600px;
margin-left: auto;
margin-right: auto;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
}
.config-title {
color: #f7931a;
font-size: 22px;
text-align: center;
margin-bottom: 15px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
color: #f7931a;
}
.form-group input {
width: 100%;
background-color: #111;
border: 1px solid #f7931a;
padding: 8px;
color: white;
font-family: 'VT323', monospace;
font-size: 18px;
}
.form-group input:focus {
outline: none;
box-shadow: 0 0 5px #f7931a;
}
.form-actions {
display: flex;
justify-content: space-between;
margin-top: 20px;
}
.btn {
background-color: #f7931a;
border: none;
color: black;
padding: 8px 15px;
font-family: 'VT323', monospace;
font-size: 18px;
cursor: pointer;
min-width: 120px;
text-align: center;
}
.btn:hover {
background-color: #ffa642;
}
.btn-secondary {
background-color: #333;
color: #f7931a;
}
.btn-secondary:hover {
background-color: #444;
}
/* Make skip button more mobile-friendly */
#skip-button {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
padding: 12px 20px;
font-size: 18px;
border-radius: 8px;
}
@media (max-width: 768px) {
#skip-button {
bottom: 10px;
right: 10px;
padding: 15px 25px;
font-size: 20px; /* Larger font size for better tap targets */
border-radius: 10px;
width: auto;
}
.form-actions {
flex-direction: column;
gap: 10px;
}
.btn {
width: 100%;
padding: 12px;
font-size: 20px;
}
}
/* Tooltip styles */
.tooltip {
position: relative;
display: inline-block;
margin-left: 5px;
cursor: help;
}
.tooltip .tooltip-text {
visibility: hidden;
width: 200px;
background-color: #000;
color: #fff;
text-align: center;
border: 1px solid #f7931a;
padding: 5px;
position: absolute;
z-index: 1;
bottom: 125%;
left: 50%;
margin-left: -100px;
opacity: 0;
transition: opacity 0.3s;
font-size: 14px;
}
.tooltip:hover .tooltip-text {
visibility: visible;
opacity: 1;
}
/* Form success/error message */
#form-message {
margin-top: 10px;
padding: 8px;
text-align: center;
display: none;
}
.message-success {
background-color: rgba(50, 205, 50, 0.2);
border: 1px solid #32CD32;
color: #32CD32;
}
.message-error {
background-color: rgba(255, 0, 0, 0.2);
border: 1px solid #ff0000;
color: #ff0000;
}
</style>
<link rel="stylesheet" href="/static/css/theme-toggle.css">
<!-- Add Theme JS -->
<script src="/static/js/theme.js"></script>
</head>
<body>
<script>
// Add underwater effects for DeepSea theme
document.addEventListener('DOMContentLoaded', function () {
// Check if DeepSea theme is active
if (localStorage.getItem('useDeepSeaTheme') === 'true') {
// Create underwater light rays
const rays = document.createElement('div');
rays.className = 'underwater-rays';
document.body.appendChild(rays);
// Create digital noise
const noise = document.createElement('div');
noise.className = 'digital-noise';
document.body.appendChild(noise);
}
});
</script>
<!-- Theme toggle button (new) -->
<button id="themeToggle" class="theme-toggle-btn">
<span>Toggle Theme</span>
</button>
<button id="skip-button">SKIP</button>
<div id="debug-info"></div>
<div id="loading-message">Loading mining data...</div>
@ -180,7 +42,7 @@
██╔══██╗ ██║ ██║ ██║ ██║╚════██║
██████╔╝ ██║ ╚██████╗ ╚██████╔╝███████║
╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚══════╝
v.21
v.21
</div>
<div id="terminal">
<div id="terminal-content">
@ -226,7 +88,41 @@
<span class="tooltip-text">Total power consumption of your mining equipment</span>
</span>
</label>
<input type="number" id="power-usage" step="1" min="0" placeholder="3450" value="">
<input type="number" id="power-usage" step="50" min="0" placeholder="13450" value="">
</div>
<div class="form-group">
<label for="network-fee">
Network Fee (%)
<span class="tooltip">
?
<span class="tooltip-text">Additional fees beyond pool fee, like Firmware fees</span>
</span>
</label>
<input type="number" id="network-fee" step="0.1" min="0" max="10" placeholder="0.0" value="">
</div>
<div class="form-group">
<label for="timezone">
Timezone
<span class="tooltip">
?
<span class="tooltip-text">Your local timezone for displaying time information</span>
</span>
</label>
<select id="timezone" class="form-control">
<optgroup label="Common Timezones">
<option value="America/Los_Angeles">Los Angeles (Pacific Time)</option>
<option value="America/Denver">Denver (Mountain Time)</option>
<option value="America/Chicago">Chicago (Central Time)</option>
<option value="America/New_York">New York (Eastern Time)</option>
<option value="Europe/London">London (GMT/BST)</option>
<option value="Europe/Paris">Paris (Central European Time)</option>
<option value="Asia/Tokyo">Tokyo (Japan Standard Time)</option>
<option value="Australia/Sydney">Sydney (Australian Eastern Time)</option>
</optgroup>
<optgroup label="Other Timezones" id="other-timezones">
<!-- Will be populated by JavaScript -->
</optgroup>
</select>
</div>
<div id="form-message"></div>
<div class="form-actions">
@ -236,6 +132,147 @@
</div>
<script>
// Theme toggle initialization
document.addEventListener('DOMContentLoaded', function () {
// Initialize theme toggle button based on current theme
const themeToggle = document.getElementById('themeToggle');
if (themeToggle) {
// Check current theme
const isDeepSea = localStorage.getItem('useDeepSeaTheme') === 'true';
// Update button style based on theme
if (isDeepSea) {
document.body.classList.add('deepsea-theme');
themeToggle.style.borderColor = '#0088cc';
themeToggle.style.color = '#0088cc';
} else {
document.body.classList.remove('deepsea-theme');
themeToggle.style.borderColor = '#f2a900';
themeToggle.style.color = '#f2a900';
}
// Add click event listener
themeToggle.addEventListener('click', function () {
toggleTheme(); // This will now trigger a page refresh
});
}
// Update terminal colors based on theme (boot.html specific)
function updateTerminalColors() {
const isDeepSeaTheme = localStorage.getItem('useDeepSeaTheme') === 'true';
if (isDeepSeaTheme) {
document.body.classList.add('deepsea-theme');
} else {
document.body.classList.remove('deepsea-theme');
}
}
// Initialize terminal colors
updateTerminalColors();
});
// Add a function to populate all available timezones
function populateTimezones() {
const otherTimezones = document.getElementById('other-timezones');
// Common timezone areas to include
const commonAreas = [
'Africa', 'America', 'Antarctica', 'Asia', 'Atlantic',
'Australia', 'Europe', 'Indian', 'Pacific'
];
// Fetch the list of available timezones
fetch('/api/available_timezones')
.then(response => response.json())
.then(data => {
if (!data.timezones || !Array.isArray(data.timezones)) {
console.error('Invalid timezone data received');
return;
}
// Sort timezones and filter to include only common areas
const sortedTimezones = data.timezones
.filter(tz => commonAreas.some(area => tz.startsWith(area + '/')))
.sort();
// Add options for each timezone (excluding those already in common list)
const commonOptions = Array.from(document.querySelectorAll('#timezone optgroup:first-child option'))
.map(opt => opt.value);
sortedTimezones.forEach(tz => {
if (!commonOptions.includes(tz)) {
const option = document.createElement('option');
option.value = tz;
option.textContent = tz.replace('_', ' ');
otherTimezones.appendChild(option);
}
});
})
.catch(error => console.error('Error fetching timezones:', error));
}
// Call this when the page loads
document.addEventListener('DOMContentLoaded', populateTimezones);
// Load the current timezone from configuration
function loadTimezoneFromConfig() {
if (currentConfig && currentConfig.timezone) {
const timezoneSelect = document.getElementById('timezone');
// First, check if the option exists
let optionExists = false;
for (let i = 0; i < timezoneSelect.options.length; i++) {
if (timezoneSelect.options[i].value === currentConfig.timezone) {
timezoneSelect.selectedIndex = i;
optionExists = true;
break;
}
}
// If the option doesn't exist yet (might be in the 'other' group being loaded)
// set a data attribute to select it when options are loaded
if (!optionExists) {
timezoneSelect.setAttribute('data-select-value', currentConfig.timezone);
}
}
}
// Call this after loading config
loadConfig().then(() => {
loadTimezoneFromConfig();
});
// Update saveConfig to include network fee
function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const timezone = document.getElementById('timezone').value;
const networkFee = parseFloat(document.getElementById('network-fee').value) || 0;
const updatedConfig = {
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
power_cost: powerCost,
power_usage: powerUsage,
timezone: timezone,
network_fee: networkFee
};
return fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConfig)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to save configuration');
}
return response.json();
});
}
// Debug logging
function updateDebug(message) {
document.getElementById('debug-info').textContent = message;
@ -264,45 +301,50 @@
let bootComplete = false;
let configLoaded = false;
let currentConfig = {
wallet: "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9",
wallet: "yourwallethere",
power_cost: 0.0,
power_usage: 0.0
};
// Replace the current loadConfig function with this improved version
// Update loadConfig function to include network fee
function loadConfig() {
// Always make a fresh request to get the latest config
fetch('/api/config?nocache=' + new Date().getTime()) // Add cache-busting parameter
.then(response => {
if (!response.ok) {
throw new Error('Failed to load configuration: ' + response.statusText);
}
return response.json();
})
.then(data => {
console.log("Loaded configuration:", data);
currentConfig = data;
return new Promise((resolve, reject) => {
fetch('/api/config?nocache=' + new Date().getTime())
.then(response => {
if (!response.ok) {
throw new Error('Failed to load configuration: ' + response.statusText);
}
return response.json();
})
.then(data => {
console.log("Loaded configuration:", data);
currentConfig = data;
// After loading, always update the form fields with the latest values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
configLoaded = true;
})
.catch(err => {
console.error("Error loading config:", err);
// Use default values if loading fails
currentConfig = {
wallet: "bc1py5zmrtssheq3shd8cptpl5l5m3txxr5afynyg2gyvam6w78s4dlqqnt4v9",
power_cost: 0.0,
power_usage: 0.0
};
// Update form fields with latest values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || "";
configLoaded = true;
resolve(currentConfig);
})
.catch(err => {
console.error("Error loading config:", err);
// Use default values if loading fails
currentConfig = {
wallet: "yourwallethere",
power_cost: 0.0,
power_usage: 0.0,
network_fee: 0.0
};
// Still update the form with default values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
});
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
document.getElementById('network-fee').value = currentConfig.network_fee || "";
resolve(currentConfig);
});
});
}
// Also update the save button event handler to reload the config after saving
@ -328,41 +370,14 @@
});
});
// Save configuration
function saveConfig() {
const wallet = document.getElementById('wallet-address').value.trim();
const powerCost = parseFloat(document.getElementById('power-cost').value) || 0;
const powerUsage = parseFloat(document.getElementById('power-usage').value) || 0;
const updatedConfig = {
wallet: wallet || currentConfig.wallet,
power_cost: powerCost,
power_usage: powerUsage
};
return fetch('/api/config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(updatedConfig)
})
.then(response => {
if (!response.ok) {
throw new Error('Failed to save configuration');
}
return response.json();
});
}
// Safety timeout: redirect after 60 seconds if boot not complete
// Safety timeout: redirect after 120 seconds if boot not complete
window.addEventListener('load', function () {
setTimeout(function () {
if (!bootComplete && !waitingForUserInput) {
console.warn("Safety timeout reached - redirecting to dashboard");
redirectToDashboard();
}
}, 60000);
}, 120000);
});
// Configuration form event listeners
@ -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 () {
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
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
// Visual feedback
const btn = document.getElementById('use-defaults');
const originalText = btn.textContent;
btn.textContent = "Defaults Applied";
btn.style.backgroundColor = "#32CD32";
// Reset the button after a short delay
setTimeout(function () {
btn.textContent = originalText;
btn.style.backgroundColor = "";
@ -498,10 +503,14 @@
}
setTimeout(processNextMessage, 500);
} 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 += "\nUsing default configuration values.\n";
setTimeout(redirectToDashboard, 2000);
outputElement.innerHTML += "\nPlease configure your mining setup:\n";
// Short pause and then show the configuration form
setTimeout(function () {
document.getElementById('config-form').style.display = 'block';
}, 1000);
}
} catch (err) {
setTimeout(redirectToDashboard, 1000);
@ -577,10 +586,16 @@
}
}
// Skip button: immediately redirect
// Skip button: reveal configuration form only
skipButton.addEventListener('click', function () {
clearTimeout(timeoutId);
redirectToDashboard();
// Optionally, clear boot messages or hide elements related to boot sequence
outputElement.innerHTML = "";
// Hide any loading or prompt messages
loadingMessage.style.display = 'none';
promptContainer.style.display = 'none';
// Show the configuration form
configForm.style.display = 'block';
});
// Start the typing animation (hides loading message)
@ -592,7 +607,7 @@
// Fallback messages (used immediately)
function setupFallbackMessages() {
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: "All rights reserved.\n\n", speed: 25, delay: 300 },
{ text: "INITIALIZING SYSTEM...\n", speed: 25, delay: 300 },

View File

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

View File

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

View File

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

View File

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

View File

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