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.
This commit is contained in:
DJObleezy 2025-04-23 21:56:25 -07:00
parent 4c4750cb24
commit 54957babc3
8 changed files with 1133 additions and 662 deletions

View File

@ -8,6 +8,11 @@ services:
- redis_data:/data
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
dashboard:
build: .
@ -19,11 +24,19 @@ services:
- 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
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 10s
retries: 3
volumes:
redis_data:
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 \

247
minify.py
View File

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

View File

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

View File

@ -7,9 +7,35 @@
const BitcoinMinuteRefresh = (function () {
// Constants
const STORAGE_KEY = 'bitcoin_last_refresh_time'; // For cross-page sync
const STORAGE_KEY = 'bitcoin_last_refresh_time';
const BITCOIN_COLOR = '#f7931a';
const DEEPSEA_COLOR = '#0088cc';
const DOM_IDS = {
TERMINAL: 'bitcoin-terminal',
STYLES: 'bitcoin-terminal-styles',
CLOCK: 'terminal-clock',
UPTIME_HOURS: 'uptime-hours',
UPTIME_MINUTES: 'uptime-minutes',
UPTIME_SECONDS: 'uptime-seconds',
MINIMIZED_UPTIME: 'minimized-uptime-value',
SHOW_BUTTON: 'bitcoin-terminal-show'
};
const STORAGE_KEYS = {
THEME: 'useDeepSeaTheme',
COLLAPSED: 'bitcoin_terminal_collapsed',
SERVER_OFFSET: 'serverTimeOffset',
SERVER_START: 'serverStartTime',
REFRESH_EVENT: 'bitcoin_refresh_event'
};
const SELECTORS = {
HEADER: '.terminal-header',
TITLE: '.terminal-title',
TIMER: '.uptime-timer',
SEPARATORS: '.uptime-separator',
UPTIME_TITLE: '.uptime-title',
MINI_LABEL: '.mini-uptime-label',
TERMINAL_DOT: '.terminal-dot'
};
// Private variables
let terminalElement = null;
@ -20,102 +46,103 @@ const BitcoinMinuteRefresh = (function () {
let isInitialized = false;
let refreshCallback = null;
let currentThemeColor = BITCOIN_COLOR; // Default Bitcoin color
let dragListenersAdded = false;
/**
* Logging helper function
* @param {string} message - Message to log
* @param {string} level - Log level (log, warn, error)
*/
function log(message, level = 'log') {
const prefix = "BitcoinMinuteRefresh: ";
if (level === 'error') {
console.error(prefix + message);
} else if (level === 'warn') {
console.warn(prefix + message);
} else {
console.log(prefix + message);
}
}
/**
* Helper function to set multiple styles on an element
* @param {Element} element - The DOM element to style
* @param {Object} styles - Object with style properties
*/
function applyStyles(element, styles) {
Object.keys(styles).forEach(key => {
element.style[key] = styles[key];
});
}
/**
* Apply the current theme color
*/
function applyThemeColor() {
// Check if theme toggle is set to DeepSea
const isDeepSeaTheme = localStorage.getItem('useDeepSeaTheme') === 'true';
const isDeepSeaTheme = localStorage.getItem(STORAGE_KEYS.THEME) === 'true';
currentThemeColor = isDeepSeaTheme ? DEEPSEA_COLOR : BITCOIN_COLOR;
// Don't try to update DOM elements if they don't exist yet
if (!terminalElement) return;
// Update terminal colors
if (isDeepSeaTheme) {
terminalElement.style.borderColor = DEEPSEA_COLOR;
terminalElement.style.color = DEEPSEA_COLOR;
terminalElement.style.boxShadow = `0 0 5px rgba(0, 136, 204, 0.3)`;
// Define color values based on theme
const rgbValues = isDeepSeaTheme ? '0, 136, 204' : '247, 147, 26';
// Update header border
const headerElement = terminalElement.querySelector('.terminal-header');
if (headerElement) {
headerElement.style.borderColor = DEEPSEA_COLOR;
}
// Create theme config
const themeConfig = {
color: currentThemeColor,
borderColor: currentThemeColor,
boxShadow: `0 0 5px rgba(${rgbValues}, 0.3)`,
textShadow: `0 0 5px rgba(${rgbValues}, 0.8)`,
borderColorRGBA: `rgba(${rgbValues}, 0.5)`,
textShadowStrong: `0 0 8px rgba(${rgbValues}, 0.8)`
};
// Update terminal title
const titleElement = terminalElement.querySelector('.terminal-title');
if (titleElement) {
titleElement.style.color = DEEPSEA_COLOR;
titleElement.style.textShadow = '0 0 5px rgba(0, 136, 204, 0.8)';
}
// Apply styles to terminal
applyStyles(terminalElement, {
borderColor: themeConfig.color,
color: themeConfig.color,
boxShadow: themeConfig.boxShadow
});
// Update uptime timer border
const uptimeTimer = terminalElement.querySelector('.uptime-timer');
if (uptimeTimer) {
uptimeTimer.style.borderColor = `rgba(0, 136, 204, 0.5)`;
}
// Update header border
const headerElement = terminalElement.querySelector(SELECTORS.HEADER);
if (headerElement) {
headerElement.style.borderColor = themeConfig.color;
}
// Update uptime separators
const separators = terminalElement.querySelectorAll('.uptime-separator');
separators.forEach(sep => {
sep.style.textShadow = '0 0 8px rgba(0, 136, 204, 0.8)';
// Update terminal title
const titleElement = terminalElement.querySelector(SELECTORS.TITLE);
if (titleElement) {
applyStyles(titleElement, {
color: themeConfig.color,
textShadow: themeConfig.textShadow
});
}
// Update uptime title
const uptimeTitle = terminalElement.querySelector('.uptime-title');
if (uptimeTitle) {
uptimeTitle.style.textShadow = '0 0 5px rgba(0, 136, 204, 0.8)';
}
// Update uptime timer border
const uptimeTimer = terminalElement.querySelector(SELECTORS.TIMER);
if (uptimeTimer) {
uptimeTimer.style.borderColor = themeConfig.borderColorRGBA;
}
// Update minimized view
const miniLabel = terminalElement.querySelector('.mini-uptime-label');
if (miniLabel) {
miniLabel.style.color = DEEPSEA_COLOR;
}
} else {
// Reset to Bitcoin theme
terminalElement.style.borderColor = BITCOIN_COLOR;
terminalElement.style.color = BITCOIN_COLOR;
terminalElement.style.boxShadow = `0 0 5px rgba(247, 147, 26, 0.3)`;
// Update uptime separators
const separators = terminalElement.querySelectorAll(SELECTORS.SEPARATORS);
separators.forEach(sep => {
sep.style.textShadow = themeConfig.textShadowStrong;
});
// Update header border
const headerElement = terminalElement.querySelector('.terminal-header');
if (headerElement) {
headerElement.style.borderColor = BITCOIN_COLOR;
}
// Update uptime title
const uptimeTitle = terminalElement.querySelector(SELECTORS.UPTIME_TITLE);
if (uptimeTitle) {
uptimeTitle.style.textShadow = themeConfig.textShadow;
}
// Update terminal title
const titleElement = terminalElement.querySelector('.terminal-title');
if (titleElement) {
titleElement.style.color = BITCOIN_COLOR;
titleElement.style.textShadow = '0 0 5px rgba(247, 147, 26, 0.8)';
}
// Update uptime timer border
const uptimeTimer = terminalElement.querySelector('.uptime-timer');
if (uptimeTimer) {
uptimeTimer.style.borderColor = `rgba(247, 147, 26, 0.5)`;
}
// Update uptime separators
const separators = terminalElement.querySelectorAll('.uptime-separator');
separators.forEach(sep => {
sep.style.textShadow = '0 0 8px rgba(247, 147, 26, 0.8)';
});
// Update uptime title
const uptimeTitle = terminalElement.querySelector('.uptime-title');
if (uptimeTitle) {
uptimeTitle.style.textShadow = '0 0 5px rgba(247, 147, 26, 0.8)';
}
// Update minimized view
const miniLabel = terminalElement.querySelector('.mini-uptime-label');
if (miniLabel) {
miniLabel.style.color = BITCOIN_COLOR;
}
// Update minimized view
const miniLabel = terminalElement.querySelector(SELECTORS.MINI_LABEL);
if (miniLabel) {
miniLabel.style.color = themeConfig.color;
}
}
@ -125,23 +152,34 @@ const BitcoinMinuteRefresh = (function () {
function setupThemeChangeListener() {
// Listen for theme change events from localStorage
window.addEventListener('storage', function (e) {
if (e.key === 'useDeepSeaTheme') {
if (e.key === STORAGE_KEYS.THEME) {
applyThemeColor();
}
});
}
/**
* Debounce function to limit execution frequency
*/
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
/**
* Add dragging functionality to the terminal
*/
function addDraggingBehavior() {
// Find the terminal element
const terminal = document.getElementById('bitcoin-terminal') ||
const terminal = document.getElementById(DOM_IDS.TERMINAL) ||
document.querySelector('.bitcoin-terminal') ||
document.getElementById('retro-terminal-bar');
if (!terminal) {
console.warn('Terminal element not found for drag behavior');
log('Terminal element not found for drag behavior', 'warn');
return;
}
@ -155,7 +193,7 @@ const BitcoinMinuteRefresh = (function () {
if (window.innerWidth < 768) return;
// Don't handle drag if clicking on controls
if (e.target.closest('.terminal-dot')) return;
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
isDragging = true;
terminal.classList.add('dragging');
@ -177,8 +215,8 @@ const BitcoinMinuteRefresh = (function () {
e.preventDefault(); // Prevent text selection
}
// Function to handle mouse move (dragging)
function handleMouseMove(e) {
// Function to handle mouse move (dragging) with debounce for better performance
const handleMouseMove = debounce(function (e) {
if (!isDragging) return;
// Calculate the horizontal movement - vertical stays fixed
@ -193,7 +231,7 @@ const BitcoinMinuteRefresh = (function () {
terminal.style.left = newLeft + 'px';
terminal.style.right = 'auto'; // Remove right positioning
terminal.style.transform = 'none'; // Remove transformations
}
}, 10);
// Function to handle mouse up (drag end)
function handleMouseUp() {
@ -204,7 +242,7 @@ const BitcoinMinuteRefresh = (function () {
}
// Find the terminal header for dragging
const terminalHeader = terminal.querySelector('.terminal-header');
const terminalHeader = terminal.querySelector(SELECTORS.HEADER);
if (terminalHeader) {
terminalHeader.addEventListener('mousedown', handleMouseDown);
} else {
@ -212,27 +250,88 @@ const BitcoinMinuteRefresh = (function () {
terminal.addEventListener('mousedown', handleMouseDown);
}
// Add mousemove and mouseup listeners to document
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Add touch support for mobile/tablet
function handleTouchStart(e) {
if (window.innerWidth < 768) return;
if (e.target.closest(SELECTORS.TERMINAL_DOT)) return;
// Handle window resize to keep terminal visible
window.addEventListener('resize', function () {
if (window.innerWidth < 768) {
// Reset position for mobile view
terminal.style.left = '50%';
terminal.style.right = 'auto';
terminal.style.transform = 'translateX(-50%)';
const touch = e.touches[0];
isDragging = true;
terminal.classList.add('dragging');
startX = touch.clientX;
const style = window.getComputedStyle(terminal);
if (style.left !== 'auto') {
startLeft = parseInt(style.left) || 0;
} else {
// Ensure terminal stays visible in desktop view
const maxLeft = window.innerWidth - terminal.offsetWidth;
const currentLeft = parseInt(window.getComputedStyle(terminal).left) || 0;
if (currentLeft > maxLeft) {
terminal.style.left = maxLeft + 'px';
}
startLeft = window.innerWidth - (parseInt(style.right) || 0) - terminal.offsetWidth;
}
});
e.preventDefault();
}
function handleTouchMove(e) {
if (!isDragging) return;
const touch = e.touches[0];
const deltaX = touch.clientX - startX;
let newLeft = startLeft + deltaX;
const maxLeft = window.innerWidth - terminal.offsetWidth;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
terminal.style.left = newLeft + 'px';
terminal.style.right = 'auto';
terminal.style.transform = 'none';
e.preventDefault();
}
function handleTouchEnd() {
if (isDragging) {
isDragging = false;
terminal.classList.remove('dragging');
}
}
if (terminalHeader) {
terminalHeader.addEventListener('touchstart', handleTouchStart);
} else {
terminal.addEventListener('touchstart', handleTouchStart);
}
// Add event listeners only once to prevent memory leaks
if (!dragListenersAdded) {
// Add mousemove and mouseup listeners to document
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
// Add touch event listeners
document.addEventListener('touchmove', handleTouchMove, { passive: false });
document.addEventListener('touchend', handleTouchEnd);
// Handle window resize to keep terminal visible
window.addEventListener('resize', function () {
if (window.innerWidth < 768) {
// Reset position for mobile view
terminal.style.left = '50%';
terminal.style.right = 'auto';
terminal.style.transform = 'translateX(-50%)';
} else {
// Ensure terminal stays visible in desktop view
const maxLeft = window.innerWidth - terminal.offsetWidth;
const currentLeft = parseInt(window.getComputedStyle(terminal).left) || 0;
if (currentLeft > maxLeft) {
terminal.style.left = maxLeft + 'px';
}
}
});
// Mark listeners as added
dragListenersAdded = true;
}
}
/**
@ -241,7 +340,7 @@ const BitcoinMinuteRefresh = (function () {
function createTerminalElement() {
// Container element
terminalElement = document.createElement('div');
terminalElement.id = 'bitcoin-terminal';
terminalElement.id = DOM_IDS.TERMINAL;
terminalElement.className = 'bitcoin-terminal';
// Terminal content - simplified for uptime-only
@ -259,23 +358,23 @@ const BitcoinMinuteRefresh = (function () {
<div class="status-dot connected"></div>
<span>LIVE</span>
</div>
<span id="terminal-clock" class="terminal-clock">00:00:00</span>
<span id="${DOM_IDS.CLOCK}" class="terminal-clock">00:00:00</span>
</div>
<div id="uptime-timer" class="uptime-timer">
<div class="uptime-title">UPTIME</div>
<div class="uptime-display">
<div class="uptime-value">
<span id="uptime-hours" class="uptime-number">00</span>
<span id="${DOM_IDS.UPTIME_HOURS}" class="uptime-number">00</span>
<span class="uptime-label">H</span>
</div>
<div class="uptime-separator">:</div>
<div class="uptime-value">
<span id="uptime-minutes" class="uptime-number">00</span>
<span id="${DOM_IDS.UPTIME_MINUTES}" class="uptime-number">00</span>
<span class="uptime-label">M</span>
</div>
<div class="uptime-separator">:</div>
<div class="uptime-value">
<span id="uptime-seconds" class="uptime-number">00</span>
<span id="${DOM_IDS.UPTIME_SECONDS}" class="uptime-number">00</span>
<span class="uptime-label">S</span>
</div>
</div>
@ -284,7 +383,7 @@ const BitcoinMinuteRefresh = (function () {
<div class="terminal-minimized">
<div class="minimized-uptime">
<span class="mini-uptime-label">UPTIME</span>
<span id="minimized-uptime-value">00:00:00</span>
<span id="${DOM_IDS.MINIMIZED_UPTIME}">00:00:00</span>
</div>
<div class="minimized-status-dot connected"></div>
</div>
@ -300,12 +399,12 @@ const BitcoinMinuteRefresh = (function () {
uptimeElement = document.getElementById('uptime-timer');
// Check if terminal was previously collapsed
if (localStorage.getItem('bitcoin_terminal_collapsed') === 'true') {
if (localStorage.getItem(STORAGE_KEYS.COLLAPSED) === 'true') {
terminalElement.classList.add('collapsed');
}
// Add custom styles if not already present
if (!document.getElementById('bitcoin-terminal-styles')) {
if (!document.getElementById(DOM_IDS.STYLES)) {
addStyles();
}
}
@ -316,7 +415,11 @@ const BitcoinMinuteRefresh = (function () {
function addStyles() {
// Use the currentThemeColor variable instead of hardcoded colors
const styleElement = document.createElement('style');
styleElement.id = 'bitcoin-terminal-styles';
styleElement.id = DOM_IDS.STYLES;
// Generate RGB values for dynamic colors
const rgbValues = currentThemeColor === DEEPSEA_COLOR ? '0, 136, 204' : '247, 147, 26';
styleElement.textContent = `
/* Terminal Container */
.bitcoin-terminal {
@ -332,7 +435,7 @@ const BitcoinMinuteRefresh = (function () {
overflow: hidden;
padding: 8px;
transition: all 0.3s ease;
box-shadow: 0 0 5px rgba(${currentThemeColor === DEEPSEA_COLOR ? '0, 136, 204' : '247, 147, 26'}, 0.3);
box-shadow: 0 0 5px rgba(${rgbValues}, 0.3);
}
/* Terminal Header */
@ -340,10 +443,10 @@ const BitcoinMinuteRefresh = (function () {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid #f7931a;
border-bottom: 1px solid ${currentThemeColor};
padding-bottom: 5px;
margin-bottom: 8px;
cursor: pointer; /* Add pointer (hand) cursor on hover */
cursor: grab; /* Add grab cursor on hover */
}
/* Apply grabbing cursor during active drag */
@ -353,10 +456,10 @@ const BitcoinMinuteRefresh = (function () {
}
.terminal-title {
color: #f7931a;
color: ${currentThemeColor};
font-weight: bold;
font-size: 1.1rem;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
text-shadow: 0 0 5px rgba(${rgbValues}, 0.8);
animation: terminal-flicker 4s infinite;
}
@ -420,7 +523,7 @@ const BitcoinMinuteRefresh = (function () {
.terminal-clock {
font-size: 1rem;
font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
text-shadow: 0 0 5px rgba(${rgbValues}, 0.5);
}
/* Uptime Display - Modern Digital Clock Style (Horizontal) */
@ -430,7 +533,7 @@ const BitcoinMinuteRefresh = (function () {
align-items: center;
padding: 5px;
background-color: #111;
border: 1px solid rgba(247, 147, 26, 0.5);
border: 1px solid rgba(${rgbValues}, 0.5);
margin-top: 5px;
}
@ -457,7 +560,7 @@ const BitcoinMinuteRefresh = (function () {
display: inline-block;
text-align: center;
letter-spacing: 2px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
text-shadow: 0 0 8px rgba(${rgbValues}, 0.8);
color: #dee2e6;
}
@ -471,7 +574,7 @@ const BitcoinMinuteRefresh = (function () {
font-size: 1.4rem;
font-weight: bold;
padding: 0 2px;
text-shadow: 0 0 8px rgba(247, 147, 26, 0.8);
text-shadow: 0 0 8px rgba(${rgbValues}, 0.8);
}
.uptime-title {
@ -479,16 +582,16 @@ const BitcoinMinuteRefresh = (function () {
font-weight: bold;
text-transform: uppercase;
letter-spacing: 2px;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.8);
text-shadow: 0 0 5px rgba(${rgbValues}, 0.8);
margin-bottom: 3px;
}
/* Show button */
#bitcoin-terminal-show {
#${DOM_IDS.SHOW_BUTTON} {
position: fixed;
bottom: 10px;
right: 10px;
background-color: #f7931a;
background-color: ${currentThemeColor};
color: #000;
border: none;
padding: 8px 12px;
@ -496,7 +599,7 @@ const BitcoinMinuteRefresh = (function () {
cursor: pointer;
z-index: 9999;
display: none;
box-shadow: 0 0 10px rgba(247, 147, 26, 0.5);
box-shadow: 0 0 10px rgba(${rgbValues}, 0.5);
}
/* CRT scanline effect */
@ -562,13 +665,13 @@ const BitcoinMinuteRefresh = (function () {
letter-spacing: 1px;
opacity: 0.7;
margin-left: 45px;
color: #f7931a;
color: ${currentThemeColor};
}
#minimized-uptime-value {
#${DOM_IDS.MINIMIZED_UPTIME} {
font-size: 0.9rem;
font-weight: bold;
text-shadow: 0 0 5px rgba(247, 147, 26, 0.5);
text-shadow: 0 0 5px rgba(${rgbValues}, 0.5);
margin-left: 45px;
color: #dee2e6;
}
@ -665,12 +768,12 @@ const BitcoinMinuteRefresh = (function () {
});
// Update clock in normal view
const clockElement = document.getElementById('terminal-clock');
const clockElement = document.getElementById(DOM_IDS.CLOCK);
if (clockElement) {
clockElement.textContent = timeString;
}
} catch (e) {
console.error("BitcoinMinuteRefresh: Error updating clock:", e);
log("Error updating clock: " + e.message, 'error');
}
}
@ -688,40 +791,67 @@ const BitcoinMinuteRefresh = (function () {
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((diff % (1000 * 60)) / 1000);
// Update the main uptime display with digital clock style
const uptimeHoursElement = document.getElementById('uptime-hours');
const uptimeMinutesElement = document.getElementById('uptime-minutes');
const uptimeSecondsElement = document.getElementById('uptime-seconds');
// Format numbers with leading zeros
const formattedTime = {
hours: String(hours).padStart(2, '0'),
minutes: String(minutes).padStart(2, '0'),
seconds: String(seconds).padStart(2, '0')
};
if (uptimeHoursElement) {
uptimeHoursElement.textContent = String(hours).padStart(2, '0');
}
if (uptimeMinutesElement) {
uptimeMinutesElement.textContent = String(minutes).padStart(2, '0');
}
if (uptimeSecondsElement) {
uptimeSecondsElement.textContent = String(seconds).padStart(2, '0');
}
// Update the main uptime display with digital clock style
const elements = {
hours: document.getElementById(DOM_IDS.UPTIME_HOURS),
minutes: document.getElementById(DOM_IDS.UPTIME_MINUTES),
seconds: document.getElementById(DOM_IDS.UPTIME_SECONDS),
minimized: document.getElementById(DOM_IDS.MINIMIZED_UPTIME)
};
// Update each element if it exists
if (elements.hours) elements.hours.textContent = formattedTime.hours;
if (elements.minutes) elements.minutes.textContent = formattedTime.minutes;
if (elements.seconds) elements.seconds.textContent = formattedTime.seconds;
// Update the minimized uptime display
const minimizedUptimeElement = document.getElementById('minimized-uptime-value');
if (minimizedUptimeElement) {
minimizedUptimeElement.textContent = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`;
if (elements.minimized) {
elements.minimized.textContent = `${formattedTime.hours}:${formattedTime.minutes}:${formattedTime.seconds}`;
}
} catch (e) {
console.error("BitcoinMinuteRefresh: Error updating uptime:", e);
log("Error updating uptime: " + e.message, 'error');
}
}
}
/**
* Start animation frame loop for smooth updates
*/
function startAnimationLoop() {
let lastUpdate = 0;
const updateInterval = 1000; // Update every second
function animationFrame(timestamp) {
// Only update once per second to save resources
if (timestamp - lastUpdate >= updateInterval) {
updateClock();
updateUptime();
lastUpdate = timestamp;
}
// Continue the animation loop
requestAnimationFrame(animationFrame);
}
// Start the loop
requestAnimationFrame(animationFrame);
}
/**
* Notify other tabs that data has been refreshed
*/
function notifyRefresh() {
const now = Date.now();
localStorage.setItem(STORAGE_KEY, now.toString());
localStorage.setItem('bitcoin_refresh_event', 'refresh-' + now);
console.log("BitcoinMinuteRefresh: Notified other tabs of refresh at " + new Date(now).toISOString());
localStorage.setItem(STORAGE_KEYS.REFRESH_EVENT, 'refresh-' + now);
log("Notified other tabs of refresh at " + new Date(now).toISOString());
}
/**
@ -735,11 +865,11 @@ const BitcoinMinuteRefresh = (function () {
applyThemeColor();
// Create the terminal element if it doesn't exist
if (!document.getElementById('bitcoin-terminal')) {
if (!document.getElementById(DOM_IDS.TERMINAL)) {
createTerminalElement();
} else {
// Get references to existing elements
terminalElement = document.getElementById('bitcoin-terminal');
terminalElement = document.getElementById(DOM_IDS.TERMINAL);
uptimeElement = document.getElementById('uptime-timer');
// Apply theme to existing element
@ -751,10 +881,10 @@ const BitcoinMinuteRefresh = (function () {
// Try to get stored server time information
try {
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0');
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0');
} catch (e) {
console.error("BitcoinMinuteRefresh: Error reading server time from localStorage:", e);
log("Error reading server time from localStorage: " + e.message, 'error');
}
// Clear any existing intervals
@ -762,11 +892,8 @@ const BitcoinMinuteRefresh = (function () {
clearInterval(uptimeInterval);
}
// Set up interval for updating clock and uptime display
uptimeInterval = setInterval(function () {
updateClock();
updateUptime();
}, 1000); // Update every second is sufficient for uptime display
// Use requestAnimationFrame for smoother animations
startAnimationLoop();
// Listen for storage events to sync across tabs
window.removeEventListener('storage', handleStorageChange);
@ -779,15 +906,15 @@ const BitcoinMinuteRefresh = (function () {
// Mark as initialized
isInitialized = true;
console.log("BitcoinMinuteRefresh: Initialized");
log("Initialized");
}
/**
* Handle storage changes for cross-tab synchronization
*/
function handleStorageChange(event) {
if (event.key === 'bitcoin_refresh_event') {
console.log("BitcoinMinuteRefresh: Detected refresh from another tab");
if (event.key === STORAGE_KEYS.REFRESH_EVENT) {
log("Detected refresh from another tab");
// If another tab refreshed, consider refreshing this one too
// But don't refresh if it was just refreshed recently (5 seconds)
@ -795,12 +922,12 @@ const BitcoinMinuteRefresh = (function () {
if (typeof refreshCallback === 'function' && Date.now() - lastRefreshTime > 5000) {
refreshCallback();
}
} else if (event.key === 'serverTimeOffset' || event.key === 'serverStartTime') {
} else if (event.key === STORAGE_KEYS.SERVER_OFFSET || event.key === STORAGE_KEYS.SERVER_START) {
try {
serverTimeOffset = parseFloat(localStorage.getItem('serverTimeOffset') || '0');
serverStartTime = parseFloat(localStorage.getItem('serverStartTime') || '0');
serverTimeOffset = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_OFFSET) || '0');
serverStartTime = parseFloat(localStorage.getItem(STORAGE_KEYS.SERVER_START) || '0');
} catch (e) {
console.error("BitcoinMinuteRefresh: Error reading updated server time:", e);
log("Error reading updated server time: " + e.message, 'error');
}
}
}
@ -810,7 +937,7 @@ const BitcoinMinuteRefresh = (function () {
*/
function handleVisibilityChange() {
if (!document.hidden) {
console.log("BitcoinMinuteRefresh: Page became visible, updating");
log("Page became visible, updating");
// Update immediately when page becomes visible
updateClock();
@ -834,13 +961,13 @@ const BitcoinMinuteRefresh = (function () {
serverStartTime = startTime;
// Store in localStorage for cross-page sharing
localStorage.setItem('serverTimeOffset', serverTimeOffset.toString());
localStorage.setItem('serverStartTime', serverStartTime.toString());
localStorage.setItem(STORAGE_KEYS.SERVER_OFFSET, serverTimeOffset.toString());
localStorage.setItem(STORAGE_KEYS.SERVER_START, serverStartTime.toString());
// Update the uptime immediately
updateUptime();
console.log("BitcoinMinuteRefresh: Server time updated - offset:", serverTimeOffset, "ms");
log("Server time updated - offset: " + serverTimeOffset + " ms");
}
/**
@ -850,7 +977,7 @@ const BitcoinMinuteRefresh = (function () {
if (!terminalElement) return;
terminalElement.classList.toggle('collapsed');
localStorage.setItem('bitcoin_terminal_collapsed', terminalElement.classList.contains('collapsed'));
localStorage.setItem(STORAGE_KEYS.COLLAPSED, terminalElement.classList.contains('collapsed'));
}
/**
@ -862,15 +989,15 @@ const BitcoinMinuteRefresh = (function () {
terminalElement.style.display = 'none';
// Create show button if it doesn't exist
if (!document.getElementById('bitcoin-terminal-show')) {
if (!document.getElementById(DOM_IDS.SHOW_BUTTON)) {
const showButton = document.createElement('button');
showButton.id = 'bitcoin-terminal-show';
showButton.id = DOM_IDS.SHOW_BUTTON;
showButton.textContent = 'Show Monitor';
showButton.onclick = showTerminal;
document.body.appendChild(showButton);
}
document.getElementById('bitcoin-terminal-show').style.display = 'block';
document.getElementById(DOM_IDS.SHOW_BUTTON).style.display = 'block';
}
/**
@ -880,7 +1007,10 @@ const BitcoinMinuteRefresh = (function () {
if (!terminalElement) return;
terminalElement.style.display = 'block';
document.getElementById('bitcoin-terminal-show').style.display = 'none';
const showButton = document.getElementById(DOM_IDS.SHOW_BUTTON);
if (showButton) {
showButton.style.display = 'none';
}
}
// Public API
@ -891,7 +1021,7 @@ const BitcoinMinuteRefresh = (function () {
toggleTerminal: toggleTerminal,
hideTerminal: hideTerminal,
showTerminal: showTerminal,
updateTheme: applyThemeColor // <-- Add this new method
updateTheme: applyThemeColor
};
})();

View File

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

View File

@ -201,17 +201,6 @@
<div class="card">
<div class="card-header">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">
@ -223,6 +212,17 @@
</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">