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.
This commit is contained in:
DJObleezy 2025-04-18 11:08:32 -07:00
parent 96a71ec80d
commit 97fe19d61d
13 changed files with 594 additions and 104 deletions

41
App.py
View File

@ -22,6 +22,7 @@ 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__)
@ -45,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')
@ -417,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,
@ -453,12 +454,12 @@ 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 %H:%M:%S %p")
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%Y-%m-%d %H:%M:%S %p")
return render_template("dashboard.html", metrics=default_metrics, current_time=current_time)
# If we have metrics, use them
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%Y-%m-%d %H:%M:%S %p")
return render_template("dashboard.html", metrics=cached_metrics, current_time=current_time)# api/time endpoint
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")
def api_metrics():
@ -467,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("%b %d, %Y, %I:%M:%S %p")
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p")
return render_template("blocks.html", current_time=current_time)
# --- 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
@ -507,8 +520,8 @@ def api_workers():
def api_time():
"""API endpoint for server time."""
return jsonify({ # correct time
"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()
})
# --- New Config Endpoints ---
@ -586,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:
@ -604,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}")
@ -637,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
@ -781,7 +794,7 @@ def api_clear_notifications():
@app.route("/notifications")
def notifications_page():
"""Serve the notifications page."""
current_time = datetime.now(ZoneInfo("America/Los_Angeles")).strftime("%b %d, %Y, %I:%M:%S %p")
current_time = datetime.now(ZoneInfo(get_timezone())).strftime("%b %d, %Y, %I:%M:%S %p")
return render_template("notifications.html", current_time=current_time)
@app.errorhandler(404)

View File

@ -2,4 +2,5 @@
"power_cost": 0.0,
"power_usage": 0.0,
"wallet": "yourwallethere"
"timezone": "America/Los_Angeles" // Add the timezone key
}

View File

@ -19,7 +19,8 @@ def load_config():
default_config = {
"power_cost": 0.0,
"power_usage": 0.0,
"wallet": "yourwallethere"
"wallet": "yourwallethere",
"timezone": "America/Los_Angeles" # Add default timezone
}
if os.path.exists(CONFIG_FILE):
@ -35,6 +36,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,6 +12,7 @@ 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."""
@ -136,8 +137,8 @@ class MiningDashboardService:
metrics['estimated_rewards_in_window_sats'] = int(round(estimated_rewards_in_window * self.sats_per_btc))
# --- Add server timestamps to the response in Los Angeles Time ---
metrics["server_timestamp"] = datetime.now(ZoneInfo("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
@ -360,7 +361,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}")
@ -816,7 +817,7 @@ class MiningDashboardService:
'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")
@ -963,7 +964,7 @@ class MiningDashboardService:
'workers_offline': workers_offline,
'total_earnings': total_earnings,
'avg_acceptance_rate': 99.0,
'timestamp': datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
'timestamp': datetime.now(ZoneInfo(get_timezone())).isoformat()
}
logging.info(f"Successfully retrieved {len(workers)} workers across multiple pages")
return result

View File

@ -31,6 +31,7 @@ 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
@dataclass
class WorkerData:

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

View File

@ -537,13 +537,17 @@ const BitcoinMinuteRefresh = (function () {
function updateClock() {
try {
const now = new Date(Date.now() + (serverTimeOffset || 0));
let hours = now.getHours();
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const ampm = hours >= 12 ? 'PM' : 'AM';
hours = hours % 12;
hours = hours ? hours : 12; // the hour '0' should be '12'
const timeString = `${String(hours).padStart(2, '0')}:${minutes}:${seconds} ${ampm}`;
// Use the global timezone setting if available
const timeZone = window.dashboardTimezone || 'America/Los_Angeles';
// Format the time in the configured timezone
const timeString = now.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZone: timeZone
});
// Update clock in normal view
const clockElement = document.getElementById('terminal-clock');

View File

@ -10,6 +10,40 @@ let isLoading = false;
$(document).ready(function() {
console.log("Blocks page initialized");
// Load timezone setting early
(function loadTimezoneEarly() {
// First try to get from localStorage for instant access
try {
const storedTimezone = localStorage.getItem('dashboardTimezone');
if (storedTimezone) {
window.dashboardTimezone = storedTimezone;
console.log(`Using cached timezone: ${storedTimezone}`);
}
} catch (e) {
console.error("Error reading timezone from localStorage:", e);
}
// Then fetch from server to ensure we have the latest setting
fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
window.dashboardTimezone = data.timezone;
console.log(`Set timezone from server: ${data.timezone}`);
// Cache for future use
try {
localStorage.setItem('dashboardTimezone', data.timezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
}
})
.catch(error => {
console.error("Error fetching timezone:", error);
});
})();
// Initialize notification badge
initNotificationBadge();
@ -91,7 +125,8 @@ function formatTimestamp(timestamp) {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
hour12: true,
timeZone: window.dashboardTimezone || 'America/Los_Angeles' // Use global timezone setting
};
return date.toLocaleString('en-US', options);
}

View File

@ -313,6 +313,27 @@ class ArrowIndicator {
// Create the singleton instance
const arrowIndicator = new ArrowIndicator();
// Global timezone configuration
let dashboardTimezone = 'America/Los_Angeles'; // Default
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
// Fetch the configured timezone when the page loads
function fetchTimezoneConfig() {
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(`Using configured timezone: ${dashboardTimezone}`);
}
})
.catch(error => console.error('Error fetching timezone config:', error));
}
// Call this on page load
document.addEventListener('DOMContentLoaded', fetchTimezoneConfig);
// Global variables
let previousMetrics = {};
let latestMetrics = null;
@ -1006,7 +1027,7 @@ function updateChartWithNormalizedData(chart, data) {
// The options define Pacific Time and 12-hour format without AM/PM
try {
let formattedTime = timeDate.toLocaleTimeString('en-US', {
timeZone: 'America/Los_Angeles',
timeZone: dashboardTimezone,
hour: '2-digit',
minute: '2-digit',
hour12: true
@ -1058,7 +1079,7 @@ function updateChartWithNormalizedData(chart, data) {
try {
currentTime = now.toLocaleTimeString('en-US', {
timeZone: 'America/Los_Angeles',
timeZone: dashboardTimezone,
hour: '2-digit',
minute: '2-digit',
hour12: true
@ -1307,17 +1328,43 @@ function updateUI() {
updateElementText("estimated_rewards_in_window_sats", numberWithCommas(data.estimated_rewards_in_window_sats) + " SATS");
// Update last updated timestamp
const now = new Date(Date.now() + serverTimeOffset);
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
};
updateElementHTML("lastUpdated", "<strong>Last Updated:</strong> " + now.toLocaleString('en-US', options) + "<span id='terminal-cursor'></span>");
try {
// Get the configured timezone with fallback
const configuredTimezone = window.dashboardTimezone || 'America/Los_Angeles';
// Use server timestamp from metrics if available, otherwise use adjusted local time
const timestampToUse = latestMetrics && latestMetrics.server_timestamp ?
new Date(latestMetrics.server_timestamp) :
new Date(Date.now() + (serverTimeOffset || 0));
// Format with explicit timezone
const options = {
year: 'numeric',
month: 'short',
day: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true,
timeZone: configuredTimezone // Explicitly set timezone
};
// Update the lastUpdated element
updateElementHTML("lastUpdated",
"<strong>Last Updated:</strong> " +
timestampToUse.toLocaleString('en-US', options) +
"<span id='terminal-cursor'></span>");
console.log(`Last updated timestamp shown using timezone: ${configuredTimezone}`);
} catch (error) {
console.error("Error formatting last updated timestamp:", error);
// Fallback to basic timestamp if there's an error
const now = new Date();
updateElementHTML("lastUpdated",
"<strong>Last Updated:</strong> " +
now.toLocaleString() +
"<span id='terminal-cursor'></span>");
}
// Update chart with normalized data if it exists
if (trendChart) {
@ -1386,7 +1433,7 @@ function resetDashboardChart() {
// Get current time
const now = new Date();
let currentTime = now.toLocaleTimeString('en-US', {
timeZone: 'America/Los_Angeles',
timeZone: dashboardTimezone,
hour: '2-digit',
minute: '2-digit',
hour12: true
@ -1434,6 +1481,40 @@ $(document).ready(function () {
}, 100);
};
// 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);
});
})();
// Override the manualRefresh function to update the shared lastRefreshTime
const originalManualRefresh = manualRefresh;
window.manualRefresh = function () {

View File

@ -7,10 +7,17 @@ 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');
@ -41,6 +48,34 @@ $(document).ready(() => {
setInterval(updateNotificationTimestamps, 30000);
});
// Fetch timezone configuration from server
function fetchTimezoneConfig() {
return fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
dashboardTimezone = data.timezone;
window.dashboardTimezone = dashboardTimezone; // Make it globally accessible
console.log(`Notifications page using timezone: ${dashboardTimezone}`);
// Store in localStorage for future use
try {
localStorage.setItem('dashboardTimezone', dashboardTimezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
// Update all timestamps with the new timezone
updateNotificationTimestamps();
return dashboardTimezone;
}
})
.catch(error => {
console.error('Error fetching timezone config:', error);
return null;
});
}
// Load notifications with current filter
function loadNotifications() {
if (isLoading) return;
@ -104,14 +139,36 @@ function refreshNotifications() {
}
}
// Update notification timestamps to relative time
// This refreshes all timestamps on the page periodically
function updateNotificationTimestamps() {
$('.notification-item').each(function () {
const timestampStr = $(this).attr('data-timestamp');
if (timestampStr) {
const timestamp = new Date(timestampStr);
const relativeTime = formatTimestamp(timestamp);
$(this).find('.notification-time').text(relativeTime);
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);
}
}
});
}
@ -190,14 +247,28 @@ function createNotificationElement(notification) {
iconElement.addClass('fa-bell');
}
// Append "Z" to indicate UTC if not present
let utcTimestampStr = notification.timestamp;
if (!utcTimestampStr.endsWith('Z')) {
utcTimestampStr += 'Z';
}
const utcDate = new Date(utcTimestampStr);
// 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);
// Convert UTC date to Los Angeles time with a timezone name for clarity
// Validate the date object - if invalid, try alternative approach
if (isNaN(notificationDate.getTime())) {
console.warn("Invalid date from notification timestamp, trying alternative format");
// Try adding Z to make it explicit UTC if not already ISO format
if (!notification.timestamp.endsWith('Z') && !notification.timestamp.includes('+')) {
notificationDate = new Date(notification.timestamp + 'Z');
}
}
} catch (e) {
console.error("Error parsing notification date:", e);
notificationDate = new Date(); // Fallback to current date
}
// Format the timestamp using the configured timezone
const options = {
year: 'numeric',
month: 'short',
@ -205,16 +276,25 @@ function createNotificationElement(notification) {
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: true
hour12: true,
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
};
const fullTimestamp = utcDate.toLocaleString('en-US', options);
// Append the full timestamp to the notification message
// Format full timestamp with configured timezone
let fullTimestamp;
try {
fullTimestamp = notificationDate.toLocaleString('en-US', options);
} catch (e) {
console.error("Error formatting timestamp with timezone:", e);
fullTimestamp = notificationDate.toLocaleString('en-US'); // Fallback without timezone
}
// Append the message and formatted timestamp
const messageWithTimestamp = `${notification.message}<br><span class="full-timestamp">${fullTimestamp}</span>`;
element.find('.notification-message').html(messageWithTimestamp);
// Set metadata for relative time display
element.find('.notification-time').text(formatTimestamp(utcDate));
element.find('.notification-time').text(formatTimestamp(notificationDate));
element.find('.notification-category').text(notification.category);
// Set up action buttons
@ -235,10 +315,21 @@ function createNotificationElement(notification) {
return element;
}
// Format timestamp as relative time
function formatTimestamp(timestamp) {
// Ensure we have a valid date object
let dateObj = timestamp;
if (!(timestamp instanceof Date) || isNaN(timestamp.getTime())) {
try {
dateObj = new Date(timestamp);
} catch (e) {
console.error("Invalid timestamp in formatTimestamp:", e);
return "unknown time";
}
}
// Calculate time difference in local timezone context
const now = new Date();
const diffMs = now - timestamp;
const diffMs = now - dateObj;
const diffSec = Math.floor(diffMs / 1000);
const diffMin = Math.floor(diffSec / 60);
const diffHour = Math.floor(diffMin / 60);
@ -253,17 +344,14 @@ function formatTimestamp(timestamp) {
} else if (diffDay < 30) {
return `${diffDay}d ago`;
} else {
// Format as date for older notifications
// Format as date for older notifications using 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'
};
return timestamp.toLocaleDateString('en-US', options);
return dateObj.toLocaleDateString('en-US', options);
}
}

View File

@ -92,6 +92,40 @@ $(document).ready(function () {
});
});
// Load timezone setting early
(function loadTimezoneEarly() {
// First try to get from localStorage for instant access
try {
const storedTimezone = localStorage.getItem('dashboardTimezone');
if (storedTimezone) {
window.dashboardTimezone = storedTimezone;
console.log(`Using cached timezone: ${storedTimezone}`);
}
} catch (e) {
console.error("Error reading timezone from localStorage:", e);
}
// Then fetch from server to ensure we have the latest setting
fetch('/api/timezone')
.then(response => response.json())
.then(data => {
if (data && data.timezone) {
window.dashboardTimezone = data.timezone;
console.log(`Set timezone from server: ${data.timezone}`);
// Cache for future use
try {
localStorage.setItem('dashboardTimezone', data.timezone);
} catch (e) {
console.error("Error storing timezone in localStorage:", e);
}
}
})
.catch(error => {
console.error("Error fetching timezone:", error);
});
})();
// Initialize page elements
function initializePage() {
console.log("Initializing page elements...");
@ -321,11 +355,35 @@ function createWorkerCard(worker) {
</div>
`);
// Format the last share using the proper method for timezone conversion
let formattedLastShare = 'N/A';
if (worker.last_share && typeof worker.last_share === 'string') {
// This is a more reliable method for timezone conversion
try {
// The worker.last_share is likely in format "YYYY-MM-DD HH:MM"
// We need to consider it as UTC and convert to the configured timezone
// Create a proper date object, ensuring UTC interpretation
const dateWithoutTZ = new Date(worker.last_share + 'Z'); // Adding Z to treat as UTC
// Format it according to the configured timezone
formattedLastShare = dateWithoutTZ.toLocaleString('en-US', {
hour: '2-digit',
minute: '2-digit',
hour12: true,
timeZone: window.dashboardTimezone || 'America/Los_Angeles'
});
} catch (e) {
console.error("Error formatting last share time:", e, worker.last_share);
formattedLastShare = worker.last_share; // Fallback to original value
}
}
card.append(`
<div class="worker-stats">
<div class="worker-stats-row">
<div class="worker-stats-label">Last Share:</div>
<div class="blue-glow">${typeof worker.last_share === 'string' ? worker.last_share.split(' ')[1] || worker.last_share : 'N/A'}</div>
<div class="blue-glow">${formattedLastShare}</div>
</div>
<div class="worker-stats-row">
<div class="worker-stats-label">Earnings:</div>

View File

@ -167,6 +167,58 @@
border: 1px solid #ff0000;
color: #ff0000;
}
/* Add these styles to match the timezone dropdown with your other inputs */
.form-group select {
width: 100%;
background-color: #111;
border: 1px solid #f7931a;
padding: 8px;
color: white;
font-family: 'VT323', monospace;
font-size: 18px;
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
cursor: pointer;
}
/* Add a custom dropdown arrow */
.form-group select {
background-image: linear-gradient(45deg, transparent 50%, #f7931a 50%), linear-gradient(135deg, #f7931a 50%, transparent 50%);
background-position: calc(100% - 15px) 50%, calc(100% - 10px) 50%;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
padding-right: 25px; /* Space for the arrow */
}
.form-group select:focus {
outline: none;
box-shadow: 0 0 5px #f7931a;
}
/* Style the option groups */
.form-group select optgroup {
background-color: #111;
color: #f7931a;
font-family: 'VT323', monospace;
}
/* Style the options */
.form-group select option {
background-color: #222;
color: white;
padding: 8px;
}
/* Improve mobile appearance */
@media (max-width: 768px) {
.form-group select {
padding: 12px;
padding-right: 30px; /* More space for the dropdown arrow */
font-size: 20px;
}
}
</style>
</head>
<body>
@ -229,6 +281,30 @@
<input type="number" id="power-usage" step="50" min="0" placeholder="13450" value="">
</div>
<div id="form-message"></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 class="form-actions">
<button class="btn btn-secondary" id="use-defaults">Use Defaults</button>
<button class="btn" id="save-config">Save & Continue</button>
@ -236,6 +312,106 @@
</div>
<script>
// 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 the saveConfig function to include timezone
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 updatedConfig = {
wallet: wallet || currentConfig.wallet,
power_cost: powerCost,
power_usage: powerUsage,
timezone: timezone
};
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;
@ -271,38 +447,43 @@
// Replace the current loadConfig function with this improved version
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 a promise that resolves when the config is loaded
return new Promise((resolve, reject) => {
// 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;
// After loading, always update the form fields with the latest values
document.getElementById('wallet-address').value = currentConfig.wallet || "";
document.getElementById('power-cost').value = currentConfig.power_cost || "";
document.getElementById('power-usage').value = currentConfig.power_usage || "";
configLoaded = true;
})
.catch(err => {
console.error("Error loading config:", err);
// Use default values if loading fails
currentConfig = {
wallet: "yourwallethere",
power_cost: 0.0,
power_usage: 0.0
};
// 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;
resolve(currentConfig); // Resolve the promise with the config data
})
.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
};
// 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 || "";
});
// 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 || "";
resolve(currentConfig); // Resolve the promise with the default config
});
});
}
// Also update the save button event handler to reload the config after saving
@ -333,11 +514,13 @@
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 updatedConfig = {
wallet: wallet || currentConfig.wallet,
wallet: wallet || (currentConfig ? currentConfig.wallet : ""),
power_cost: powerCost,
power_usage: powerUsage
power_usage: powerUsage,
timezone: timezone
};
return fetch('/api/config', {

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."""
@ -49,7 +50,7 @@ class WorkerService:
"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):
@ -286,7 +287,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
@ -438,7 +439,7 @@ class WorkerService:
"daily_sats": daily_sats, # Fixed daily_sats value
"avg_acceptance_rate": 98.8, # Default value
"hashrate_history": hashrate_history,
"timestamp": datetime.now(ZoneInfo("America/Los_Angeles")).isoformat()
"timestamp": datetime.now(ZoneInfo(get_timezone())).isoformat()
}
# Update cache
@ -482,7 +483,7 @@ class WorkerService:
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:
@ -635,7 +636,7 @@ class WorkerService:
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: