Implement venue timezone system to fix UTC midnight day change issues

- Add timezone_utils.py with system timezone auto-detection
- Configure venue timezone with system timezone as default fallback
- Update games_thread.py to use venue timezone for day calculations
- Update web dashboard API routes to use venue timezone for date filtering
- Add venue timezone configuration to web UI
- Fix day boundary issues where UTC midnight didn't match venue local time
- Ensure fixtures and bets are correctly associated with venue days

This resolves the problem where operations stored in UTC caused incorrect
day change calculations when midnight UTC didn't correspond to actual
midnight in the venue's timezone.
parent a00788be
...@@ -239,6 +239,9 @@ def collect_hidden_imports() -> List[str]: ...@@ -239,6 +239,9 @@ def collect_hidden_imports() -> List[str]:
'openpyxl', 'openpyxl',
'openpyxl.styles', 'openpyxl.styles',
'openpyxl.utils', 'openpyxl.utils',
# Timezone support
'pytz',
] ]
# Conditionally add ffmpeg module if available # Conditionally add ffmpeg module if available
......
...@@ -31,22 +31,37 @@ class ConfigManager: ...@@ -31,22 +31,37 @@ class ConfigManager:
# Create default settings # Create default settings
self._settings = AppSettings() self._settings = AppSettings()
self.save_settings(self._settings) self.save_settings(self._settings)
# Initialize venue timezone if not configured
self._initialize_venue_timezone()
# Validate settings # Validate settings
if not self._settings.validate(): if not self._settings.validate():
logger.error("Configuration validation failed") logger.error("Configuration validation failed")
return False return False
# Ensure directories exist # Ensure directories exist
self._settings.ensure_directories() self._settings.ensure_directories()
logger.info("Configuration manager initialized") logger.info("Configuration manager initialized")
return True return True
except Exception as e: except Exception as e:
logger.error(f"Failed to initialize configuration manager: {e}") logger.error(f"Failed to initialize configuration manager: {e}")
return False return False
def _initialize_venue_timezone(self):
"""Initialize venue timezone configuration"""
try:
# Set default venue timezone if not configured
if not self.get_config_value('venue_timezone'):
from ..utils.timezone_utils import get_default_venue_timezone
default_tz = get_default_venue_timezone()
self.set_config_value('venue_timezone', default_tz)
logger.info(f"Initialized venue timezone to: {default_tz}")
except Exception as e:
logger.error(f"Failed to initialize venue timezone: {e}")
def get_settings(self) -> AppSettings: def get_settings(self) -> AppSettings:
"""Get current application settings""" """Get current application settings"""
if self._settings is None: if self._settings is None:
......
This diff is collapsed.
"""
Timezone utilities for venue-aware date/time handling
"""
import os
import time
import logging
from datetime import datetime, timezone, timedelta
from typing import Optional
try:
import pytz
HAS_PYTZ = True
except ImportError:
HAS_PYTZ = False
logging.warning("pytz not available, timezone detection may be limited")
logger = logging.getLogger(__name__)
def get_system_timezone() -> str:
"""Auto-detect system's local timezone"""
try:
# Method 1: Use pytz for comprehensive detection
if HAS_PYTZ:
local_tz = pytz.timezone('UTC').localize(datetime.utcnow()).astimezone().tzinfo
# Find the IANA name
for tz_name in pytz.all_timezones:
try:
if pytz.timezone(tz_name) == local_tz:
return tz_name
except:
continue
# Method 2: Use dateutil
try:
from dateutil import tz
local_tz = tz.tzlocal()
now = datetime.now(local_tz)
# Try to match with pytz names
if HAS_PYTZ:
for tz_name in pytz.all_timezones:
try:
if pytz.timezone(tz_name) == local_tz:
return tz_name
except:
continue
# Fallback to offset representation
offset_seconds = local_tz.utcoffset(now).total_seconds()
offset_hours = offset_seconds / 3600
return f"UTC{offset_hours:+.0f}".replace('.0', '')
except ImportError:
pass
# Method 3: Basic system timezone detection
if hasattr(time, 'tzname') and time.tzname:
tz_name = time.tzname[0]
# Try to map common abbreviations to IANA names
tz_mapping = {
'EST': 'America/New_York',
'EDT': 'America/New_York',
'CST': 'America/Chicago',
'CDT': 'America/Chicago',
'MST': 'America/Denver',
'MDT': 'America/Denver',
'PST': 'America/Los_Angeles',
'PDT': 'America/Los_Angeles',
'GMT': 'Europe/London',
'BST': 'Europe/London',
'CET': 'Europe/Paris',
'CEST': 'Europe/Paris',
'JST': 'Asia/Tokyo',
'SGT': 'Asia/Singapore',
'IST': 'Asia/Kolkata',
'CAT': 'Africa/Johannesburg',
'SAST': 'Africa/Johannesburg',
'EAT': 'Africa/Nairobi',
'WAT': 'Africa/Lagos',
}
if tz_name in tz_mapping:
return tz_mapping[tz_name]
# If we can't map it, return as-is but log warning
logger.warning(f"Could not map timezone abbreviation '{tz_name}' to IANA name")
return tz_name
# Method 4: Offset-based detection
offset_seconds = -time.timezone if not time.daylight else -time.altzone
offset_hours = offset_seconds / 3600
return f"UTC{offset_hours:+.0f}".replace('.0', '')
except Exception as e:
logger.error(f"Failed to detect system timezone: {e}")
# Fallback to UTC as a safe default
return 'UTC'
def get_default_venue_timezone() -> str:
"""Get default venue timezone with auto-detection and environment override"""
# Check environment variable first (for containers/deployments)
env_tz = os.environ.get('VENUE_TIMEZONE')
if env_tz:
if validate_timezone(env_tz):
logger.info(f"Using venue timezone from environment: {env_tz}")
return env_tz
else:
logger.warning(f"Invalid timezone in VENUE_TIMEZONE: {env_tz}")
# Auto-detect from system
detected_tz = get_system_timezone()
logger.info(f"Auto-detected venue timezone: {detected_tz}")
return detected_tz
def validate_timezone(tz_name: str) -> bool:
"""Validate timezone name"""
if not HAS_PYTZ:
# Basic validation without pytz
try:
timezone(timedelta(hours=0)) # Just test if we can create a timezone
return True
except:
return False
try:
pytz.timezone(tz_name)
return True
except:
return False
def get_venue_timezone(db_manager) -> timezone:
"""Get configured venue timezone object"""
venue_tz_str = db_manager.get_config_value('venue_timezone', get_default_venue_timezone())
if HAS_PYTZ:
return pytz.timezone(venue_tz_str)
else:
# Fallback without pytz - assume UTC offset format
try:
if venue_tz_str.startswith('UTC'):
offset_str = venue_tz_str[3:] # Remove 'UTC'
offset_hours = float(offset_str)
return timezone(timedelta(hours=offset_hours))
else:
logger.warning(f"Non-UTC timezone '{venue_tz_str}' specified but pytz not available")
return timezone.utc
except:
logger.error(f"Invalid timezone format: {venue_tz_str}")
return timezone.utc
def get_today_venue_date(db_manager) -> datetime.date:
"""Get today's date in venue timezone"""
venue_tz = get_venue_timezone(db_manager)
if HAS_PYTZ:
return datetime.now(venue_tz).date()
else:
# Without pytz, assume venue_tz is already a timezone object
return datetime.now(venue_tz).date()
def utc_to_venue_datetime(utc_dt: datetime, db_manager) -> datetime:
"""Convert UTC datetime to venue local time"""
venue_tz = get_venue_timezone(db_manager)
if utc_dt.tzinfo is None:
utc_dt = utc_dt.replace(tzinfo=timezone.utc)
return utc_dt.astimezone(venue_tz)
def venue_to_utc_datetime(venue_dt: datetime, db_manager) -> datetime:
"""Convert venue local time to UTC"""
venue_tz = get_venue_timezone(db_manager)
if venue_dt.tzinfo is None:
if HAS_PYTZ:
venue_dt = venue_tz.localize(venue_dt)
else:
venue_dt = venue_dt.replace(tzinfo=venue_tz)
return venue_dt.astimezone(timezone.utc)
\ No newline at end of file
This diff is collapsed.
...@@ -240,6 +240,50 @@ ...@@ -240,6 +240,50 @@
</div> </div>
</div> </div>
<!-- Timezone Settings -->
<div class="card mb-4">
<div class="card-header">
<h5><i class="fas fa-clock"></i> Venue Timezone Settings</h5>
<small class="text-muted">Configure the timezone for your venue location (affects day change calculations)</small>
</div>
<div class="card-body">
<form id="timezone-config-form">
<div class="mb-3">
<label for="venue-timezone" class="form-label">Venue Timezone</label>
<select class="form-select" id="venue-timezone">
<option value="Africa/Kampala">East Africa Time (UTC+3) - Kampala, Nairobi, Addis Ababa</option>
<option value="Africa/Johannesburg">South Africa Standard Time (UTC+2) - Johannesburg, Pretoria</option>
<option value="Africa/Nairobi">East Africa Time (UTC+3) - Nairobi, Dar es Salaam</option>
<option value="Africa/Addis_Ababa">East Africa Time (UTC+3) - Addis Ababa</option>
<option value="Africa/Dar_es_Salaam">East Africa Time (UTC+3) - Dar es Salaam</option>
<option value="Africa/Lagos">West Africa Time (UTC+1) - Lagos, Abuja</option>
<option value="Africa/Cairo">Eastern European Time (UTC+2) - Cairo</option>
<option value="Europe/London">Greenwich Mean Time (UTC+0) - London</option>
<option value="America/New_York">Eastern Time (UTC-5/-4) - New York</option>
<option value="Asia/Dubai">Gulf Standard Time (UTC+4) - Dubai</option>
<option value="Asia/Shanghai">China Standard Time (UTC+8) - Shanghai</option>
<option value="Australia/Sydney">Australian Eastern Time (UTC+10) - Sydney</option>
<option value="Pacific/Auckland">New Zealand Time (UTC+12) - Auckland</option>
</select>
<div class="form-text">
<strong>Important:</strong> This timezone determines when "day change" occurs for fixtures and matches.
All dates are stored in UTC internally, but day boundaries are calculated using this venue timezone.
<br><small class="text-muted">Change requires application restart to take full effect.</small>
</div>
</div>
<div class="mb-3">
<label class="form-label">Current Venue Time</label>
<div class="form-control-plaintext">
<span class="badge bg-info text-dark fs-6" id="current-venue-time">Loading...</span>
</div>
<div class="form-text">Current time in the selected venue timezone</div>
</div>
<button type="submit" class="btn btn-primary">Save Venue Timezone</button>
</form>
<div id="timezone-status" class="mt-3"></div>
</div>
</div>
<!-- Currency Settings --> <!-- Currency Settings -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
...@@ -711,6 +755,8 @@ ...@@ -711,6 +755,8 @@
// Load current global betting mode and currency settings on page load // Load current global betting mode and currency settings on page load
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
loadBettingMode(); loadBettingMode();
loadTimezoneSettings();
setupTimezoneHandlers();
loadCurrencySettings(); loadCurrencySettings();
setupCurrencyHandlers(); setupCurrencyHandlers();
loadBarcodeSettings(); loadBarcodeSettings();
...@@ -779,6 +825,123 @@ ...@@ -779,6 +825,123 @@
}); });
}); });
// Timezone settings handlers
function setupTimezoneHandlers() {
const timezoneSelect = document.getElementById('venue-timezone');
// Handle timezone change to update current time preview
timezoneSelect.addEventListener('change', updateCurrentVenueTime);
// Handle form submission
document.getElementById('timezone-config-form').addEventListener('submit', function(e) {
e.preventDefault();
saveTimezoneSettings();
});
}
function updateCurrentVenueTime() {
const timezoneSelect = document.getElementById('venue-timezone');
const selectedTimezone = timezoneSelect.value;
const timeDisplay = document.getElementById('current-venue-time');
// Get current time in selected timezone
fetch('/api/timezone/current-time', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
timezone: selectedTimezone
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
timeDisplay.textContent = data.current_time;
timeDisplay.className = 'badge bg-success text-white fs-6';
} else {
timeDisplay.textContent = 'Error loading time';
timeDisplay.className = 'badge bg-danger text-white fs-6';
}
})
.catch(error => {
console.error('Error updating venue time:', error);
timeDisplay.textContent = 'Error loading time';
timeDisplay.className = 'badge bg-danger text-white fs-6';
});
}
function loadTimezoneSettings() {
fetch('/api/timezone-settings')
.then(response => response.json())
.then(data => {
if (data.success) {
const settings = data.settings;
document.getElementById('venue-timezone').value = settings.timezone || 'Africa/Johannesburg';
// Update current time display
updateCurrentVenueTime();
// Don't show any status on page load - only after user saves
} else {
// Use default
document.getElementById('venue-timezone').value = 'Africa/Johannesburg';
updateCurrentVenueTime();
document.getElementById('timezone-status').innerHTML =
'<div class="alert alert-info"><small><i class="fas fa-info-circle"></i> Using default venue timezone (South Africa)</small></div>';
}
})
.catch(error => {
console.error('Error loading timezone settings:', error);
// Use default
document.getElementById('venue-timezone').value = 'Africa/Johannesburg';
updateCurrentVenueTime();
document.getElementById('timezone-status').innerHTML =
'<div class="alert alert-warning"><small><i class="fas fa-exclamation-triangle"></i> Error loading timezone settings, using defaults</small></div>';
});
}
function saveTimezoneSettings() {
const statusDiv = document.getElementById('timezone-status');
const timezoneSelect = document.getElementById('venue-timezone');
const settings = {
timezone: timezoneSelect.value
};
statusDiv.innerHTML = '<div class="alert alert-info"><i class="fas fa-spinner fa-spin"></i> Saving venue timezone...</div>';
fetch('/api/timezone-settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(settings)
})
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + data.message + '<br><small>Application restart recommended for full effect.</small></div>';
// Update current time display
updateCurrentVenueTime();
// Auto-hide success message after 5 seconds
setTimeout(() => {
statusDiv.innerHTML = '<div class="alert alert-success"><small><i class="fas fa-check-circle"></i> Venue timezone saved</small></div>';
}, 5000);
} else {
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Failed to save venue timezone: ' + data.error + '</div>';
}
})
.catch(error => {
console.error('Error saving timezone settings:', error);
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Error saving venue timezone: ' + error.message + '</div>';
});
}
// Currency settings handlers // Currency settings handlers
function setupCurrencyHandlers() { function setupCurrencyHandlers() {
const symbolSelect = document.getElementById('currency-symbol'); const symbolSelect = document.getElementById('currency-symbol');
......
...@@ -31,6 +31,7 @@ psutil>=5.8.0 ...@@ -31,6 +31,7 @@ psutil>=5.8.0
click>=8.0.0 click>=8.0.0
watchdog>=3.0.0 watchdog>=3.0.0
netifaces>=0.11.0 netifaces>=0.11.0
pytz>=2023.0
# Video and image processing # Video and image processing
opencv-python>=4.5.0 opencv-python>=4.5.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment