Almost there!

parent e8a91c81
This diff is collapsed.
......@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent):
self.timer_duration_seconds = 0
self.current_fixture_id: Optional[str] = None
self.current_match_id: Optional[int] = None
self.pending_match_id: Optional[int] = None # Match prepared by START_INTRO
# Synchronization
self._timer_lock = threading.RLock()
......@@ -44,6 +45,7 @@ class MatchTimerComponent(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
self.message_bus.subscribe(self.name, MessageType.NEXT_MATCH, self._handle_next_match)
self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro)
logger.info("MatchTimer component initialized")
......@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent):
self._handle_custom_message(message)
elif message.type == MessageType.NEXT_MATCH:
self._handle_next_match(message)
elif message.type == MessageType.START_INTRO:
self._handle_start_intro(message)
except Exception as e:
logger.error(f"Failed to process message: {e}")
......@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent):
self.timer_start_time = None
self.current_fixture_id = None
self.current_match_id = None
self.pending_match_id = None
# Unregister from message bus
self.message_bus.unregister_component(self.name)
......@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent):
logger.error(f"Failed to handle custom message: {e}")
def _handle_next_match(self, message: Message):
"""Handle NEXT_MATCH message - start the next match in sequence"""
"""Handle NEXT_MATCH message - restart timer for next match interval"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Received NEXT_MATCH message for fixture {fixture_id}, match {match_id}")
logger.info("Previous match completed - restarting timer for next interval")
# Find and start the next match
match_info = self._find_and_start_next_match()
......@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent):
# Reset timer for next interval
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, match_info['fixture_id'])
logger.info(f"Timer restarted for {match_interval} minute interval")
else:
logger.info("No more matches to start, stopping timer")
self._stop_timer()
except Exception as e:
logger.error(f"Failed to handle NEXT_MATCH message: {e}")
# On error, try to restart timer anyway
try:
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, self.current_fixture_id)
except Exception as restart_e:
logger.error(f"Failed to restart timer after NEXT_MATCH error: {restart_e}")
def _handle_start_intro(self, message: Message):
"""Handle START_INTRO message - store the match_id for later MATCH_START"""
try:
fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id")
logger.info(f"Received START_INTRO message for fixture {fixture_id}, match {match_id}")
# Store the match_id for when timer expires
with self._timer_lock:
self.pending_match_id = match_id
self.current_fixture_id = fixture_id
logger.info(f"Stored pending match_id {match_id} for timer expiration")
except Exception as e:
logger.error(f"Failed to handle START_INTRO message: {e}")
def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]):
"""Start the countdown timer"""
......@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent):
self._send_timer_update()
def _on_timer_expired(self):
"""Handle timer expiration - start next match"""
"""Handle timer expiration - start the match that was prepared by START_INTRO"""
try:
logger.info("Match timer expired, starting next match...")
logger.info("Match timer expired, starting prepared match...")
# Find and start the next match
match_info = self._find_and_start_next_match()
with self._timer_lock:
pending_match_id = self.pending_match_id
fixture_id = self.current_fixture_id
if match_info:
logger.info(f"Started match {match_info['match_id']} in fixture {match_info['fixture_id']}")
if pending_match_id:
# Send MATCH_START message for the prepared match
match_start_message = MessageBuilder.match_start(
sender=self.name,
fixture_id=fixture_id,
match_id=pending_match_id
)
# Reset timer for next interval
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, match_info['fixture_id'])
self.message_bus.publish(match_start_message)
logger.info(f"Sent MATCH_START for prepared match {pending_match_id} in fixture {fixture_id}")
logger.info("Timer stopped - will restart after match completion (NEXT_MATCH)")
# Clear the pending match and stop the timer
with self._timer_lock:
self.pending_match_id = None
self._stop_timer()
else:
logger.info("No more matches to start, stopping timer")
logger.warning("No pending match prepared by START_INTRO, stopping timer")
self._stop_timer()
except Exception as e:
logger.error(f"Failed to handle timer expiration: {e}")
# Reset timer on error
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, self.current_fixture_id)
# Stop timer on error - don't restart automatically
self._stop_timer()
def _find_and_start_next_match(self) -> Optional[Dict[str, Any]]:
"""Find and start the next available match"""
......@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent):
target_fixture_id = target_match.fixture_id
if target_match:
# Send MATCH_START message
match_start_message = MessageBuilder.match_start(
# Send START_INTRO message
start_intro_message = MessageBuilder.start_intro(
sender=self.name,
fixture_id=target_fixture_id or target_match.fixture_id,
match_id=target_match.id
)
self.message_bus.publish(match_start_message)
self.message_bus.publish(start_intro_message)
return {
"fixture_id": target_fixture_id or target_match.fixture_id,
......
......@@ -70,6 +70,7 @@ class MessageType(Enum):
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
PLAY_VIDEO_RESULT = "PLAY_VIDEO_RESULT"
PLAY_VIDEO_RESULT_DONE = "PLAY_VIDEO_RESULT_DONE"
MATCH_DONE = "MATCH_DONE"
NEXT_MATCH = "NEXT_MATCH"
GAME_STATUS = "GAME_STATUS"
......@@ -661,11 +662,27 @@ class MessageBuilder:
)
@staticmethod
def play_video_result(sender: str, fixture_id: str, match_id: int, result: str) -> Message:
def play_video_result(sender: str, fixture_id: str, match_id: int, result: str, under_over_result: Optional[str] = None) -> Message:
"""Create PLAY_VIDEO_RESULT message"""
data = {
"fixture_id": fixture_id,
"match_id": match_id,
"result": result
}
if under_over_result is not None:
data["under_over_result"] = under_over_result
return Message(
type=MessageType.PLAY_VIDEO_RESULT,
sender=sender,
data=data
)
@staticmethod
def play_video_result_done(sender: str, fixture_id: str, match_id: int, result: str) -> Message:
"""Create PLAY_VIDEO_RESULT_DONE message"""
return Message(
type=MessageType.PLAY_VIDEO_RESULT_DONE,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id,
......@@ -674,15 +691,18 @@ class MessageBuilder:
)
@staticmethod
def match_done(sender: str, fixture_id: str, match_id: int) -> Message:
def match_done(sender: str, fixture_id: str, match_id: int, result: Optional[str] = None) -> Message:
"""Create MATCH_DONE message"""
data = {
"fixture_id": fixture_id,
"match_id": match_id
}
if result is not None:
data["result"] = result
return Message(
type=MessageType.MATCH_DONE,
sender=sender,
data={
"fixture_id": fixture_id,
"match_id": match_id
}
data=data
)
@staticmethod
......
......@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent):
self.broadcast_port = broadcast_port
self.broadcast_interval = 30.0 # 30 seconds
self.socket: Optional[socket.socket] = None
# Server information to broadcast
self.server_info: Dict[str, Any] = {
"service": "MBetterClient",
......@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent):
"url": "http://127.0.0.1:5001",
"timestamp": time.time()
}
# Shutdown event for responsive shutdown
self.shutdown_event = threading.Event()
# Register message queue
self.message_queue = self.message_bus.register_component(self.name)
logger.info(f"UDP Broadcast component initialized on port {broadcast_port}")
def initialize(self) -> bool:
......@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Set timeout to prevent blocking operations
self.socket.settimeout(1.0)
# Clear shutdown event
self.shutdown_event.clear()
# Subscribe to system status messages to get web server info
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
logger.info("UDP broadcast socket initialized successfully")
return True
except Exception as e:
logger.error(f"UDP broadcast initialization failed: {e}")
return False
......@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent):
last_broadcast_time = 0
while self.running:
while self.running and not self.shutdown_event.is_set():
try:
current_time = time.time()
# Process messages
message = self.message_bus.get_message(self.name, timeout=1.0)
# Process messages with shorter timeout for responsive shutdown
message = self.message_bus.get_message(self.name, timeout=0.5)
if message:
self._process_message(message)
# Broadcast every 30 seconds
if current_time - last_broadcast_time >= self.broadcast_interval:
self._broadcast_server_info()
last_broadcast_time = current_time
# Update heartbeat
self.heartbeat()
time.sleep(0.5)
# Check shutdown event more frequently
if self.shutdown_event.wait(0.5):
break
except Exception as e:
logger.error(f"UDP broadcast loop error: {e}")
time.sleep(2.0)
# Shorter sleep on error to be more responsive to shutdown
if not self.shutdown_event.wait(1.0):
continue
break
except Exception as e:
logger.error(f"UDP broadcast run failed: {e}")
......@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent):
"""Shutdown UDP broadcast component"""
try:
logger.info("Shutting down UDP broadcast...")
# Signal shutdown event to wake up the main loop
self.shutdown_event.set()
# Close socket to prevent further operations
if self.socket:
self.socket.close()
self.socket = None
try:
self.socket.close()
except Exception as e:
logger.debug(f"Error closing UDP socket: {e}")
finally:
self.socket = None
except Exception as e:
logger.error(f"UDP broadcast shutdown error: {e}")
......@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent):
for broadcast_addr in broadcast_addresses:
try:
self.socket.sendto(broadcast_data, (broadcast_addr, self.broadcast_port))
logger.debug(f"Broadcasted to {broadcast_addr}:{self.broadcast_port}")
if self.socket and not self.shutdown_event.is_set():
self.socket.sendto(broadcast_data, (broadcast_addr, self.broadcast_port))
logger.debug(f"Broadcasted to {broadcast_addr}:{self.broadcast_port}")
else:
break # Exit if shutting down or socket closed
except socket.timeout:
logger.debug(f"Timeout broadcasting to {broadcast_addr}")
except OSError as e:
if self.shutdown_event.is_set():
break # Exit if shutting down
logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}")
except Exception as e:
logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}")
......
This diff is collapsed.
......@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent):
def _create_server(self):
"""Create HTTP/HTTPS server with SocketIO support"""
try:
from werkzeug.serving import make_server
protocol = "HTTP"
if self.settings.enable_ssl:
protocol = "HTTPS"
logger.info("SSL enabled - SocketIO server will use HTTPS")
logger.info("SSL enabled - server will use HTTPS")
# Get SSL certificate paths
from ..config.settings import get_user_data_dir
cert_path, key_path = get_ssl_certificate_paths(get_user_data_dir())
if cert_path and key_path:
# Create SSL context for HTTPS
self.ssl_context = create_ssl_context(cert_path, key_path)
if not self.ssl_context:
logger.warning("SSL context creation failed, falling back to HTTP")
self.ssl_context = None
protocol = "HTTP"
else:
logger.warning("SSL certificate files not available, falling back to HTTP")
self.ssl_context = None
protocol = "HTTP"
# Create WSGI server that can be shutdown
if self.socketio:
# For SocketIO, try to use the SocketIO WSGI app
try:
wsgi_app = self.socketio.WSGIApp(self.app)
except AttributeError:
# Fallback for older SocketIO versions or different implementations
logger.warning("SocketIO WSGIApp not available, falling back to standard Flask app")
wsgi_app = self.app
self.socketio = None # Disable SocketIO since we can't use it
else:
wsgi_app = self.app
self.server = make_server(
host=self.settings.host,
port=self.settings.port,
app=wsgi_app,
threaded=True,
ssl_context=self.ssl_context
)
logger.info(f"{protocol} server with SocketIO created on {self.settings.host}:{self.settings.port}")
logger.info(f"{protocol} server created on {self.settings.host}:{self.settings.port}")
if self.settings.enable_ssl:
if self.settings.enable_ssl and self.ssl_context:
logger.info("⚠️ Using self-signed certificate - browsers will show security warning")
logger.info(" You can safely proceed by accepting the certificate")
......@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent):
socketio_status = "with SocketIO" if self.socketio else "without SocketIO"
logger.info(f"Starting {protocol} server {socketio_status} on {self.settings.host}:{self.settings.port}")
if self.socketio:
# Run SocketIO server
self.socketio.run(
self.app,
host=self.settings.host,
port=self.settings.port,
debug=False,
use_reloader=False,
log_output=False
)
if self.server:
# Use the shutdown-capable server
# serve_forever() will block until shutdown() is called
self.server.serve_forever()
logger.info("HTTP server stopped")
else:
# Run Flask server without SocketIO
self.app.run(
host=self.settings.host,
port=self.settings.port,
debug=False,
use_reloader=False
)
logger.error("Server not created, cannot start")
return
except Exception as e:
if self.running: # Only log if not shutting down
protocol = "HTTPS" if self.settings.enable_ssl else "HTTP"
logger.error(f"{protocol} server error: {e}")
else:
# Expected during shutdown
logger.debug(f"Server stopped during shutdown: {e}")
def _setup_ssl_error_suppression(self):
"""Setup logging filter to suppress expected SSL connection errors"""
......@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent):
"""Shutdown web dashboard"""
try:
logger.info("Shutting down WebDashboard...")
# Shutdown the HTTP server
if self.server:
logger.info("Shutting down HTTP server...")
self.server.shutdown()
logger.info("HTTP server shutdown initiated")
# Note: SocketIO connections will be closed when the server shuts down
# No explicit SocketIO shutdown needed as it's handled by the WSGI server
except Exception as e:
logger.error(f"WebDashboard shutdown error: {e}")
......
......@@ -410,17 +410,9 @@ def api_tokens():
def fixtures():
"""Fixtures management page"""
try:
# Restrict cashier users from accessing fixtures page
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/fixtures.html',
user=current_user,
page_title="Fixtures")
user=current_user,
page_title="Fixtures")
except Exception as e:
logger.error(f"Fixtures page error: {e}")
flash("Error loading fixtures", "error")
......@@ -432,18 +424,10 @@ def fixtures():
def fixture_details(fixture_id):
"""Fixture details page showing all matches in the fixture"""
try:
# Restrict cashier users from accessing fixture details page
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/fixture_details.html',
user=current_user,
fixture_id=fixture_id,
page_title=f"Fixture Details - Fixture #{fixture_id}")
user=current_user,
fixture_id=fixture_id,
page_title=f"Fixture Details - Fixture #{fixture_id}")
except Exception as e:
logger.error(f"Fixture details page error: {e}")
flash("Error loading fixture details", "error")
......@@ -455,19 +439,11 @@ def fixture_details(fixture_id):
def match_details(match_id, fixture_id):
"""Match details page showing match information and outcomes"""
try:
# Restrict cashier users from accessing match details page
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/match_details.html',
user=current_user,
match_id=match_id,
fixture_id=fixture_id,
page_title=f"Match Details - Match #{match_id}")
user=current_user,
match_id=match_id,
fixture_id=fixture_id,
page_title=f"Match Details - Match #{match_id}")
except Exception as e:
logger.error(f"Match details page error: {e}")
flash("Error loading match details", "error")
......@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id):
'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township,
'status': match.status
'status': match.status,
'result': match.result
}
else:
detail_data['match'] = None
......@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id):
'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township,
'status': match.status
'status': match.status,
'result': match.result
}
else:
detail_data['match'] = None
......
......@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr>
<td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br>
<small class="text-muted">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</small>
${detail.match && detail.match.result ? `<br><small class="text-info"><i class="fas fa-trophy me-1"></i>Result: ${detail.match.result}</small>` : ''}
</td>
<td><span class="badge bg-primary">${detail.outcome}</span></td>
<td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td>
......
......@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr>
<td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br>
<small class="text-muted">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</small>
${detail.match && detail.match.result ? `<br><small class="text-info"><i class="fas fa-trophy me-1"></i>Result: ${detail.match.result}</small>` : ''}
</td>
<td><span class="badge bg-primary">${detail.outcome}</span></td>
<td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td>
......
......@@ -579,6 +579,7 @@
<div class="col-8">
<h6 class="fw-bold mb-1">Match #${detail.match ? detail.match.match_number : 'Unknown'}</h6>
<p class="text-muted small mb-1">${detail.match ? detail.match.fighter1_township + ' vs ' + detail.match.fighter2_township : 'Match info unavailable'}</p>
${detail.match && detail.match.result ? `<p class="text-info small mb-1"><i class="fas fa-trophy me-1"></i>Result: ${detail.match.result}</p>` : ''}
<span class="badge bg-primary">${detail.outcome}</span>
</div>
<div class="col-4 text-end">
......
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