Almost there!

parent e8a91c81
This diff is collapsed.
...@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -31,6 +31,7 @@ class MatchTimerComponent(ThreadedComponent):
self.timer_duration_seconds = 0 self.timer_duration_seconds = 0
self.current_fixture_id: Optional[str] = None self.current_fixture_id: Optional[str] = None
self.current_match_id: Optional[int] = None self.current_match_id: Optional[int] = None
self.pending_match_id: Optional[int] = None # Match prepared by START_INTRO
# Synchronization # Synchronization
self._timer_lock = threading.RLock() self._timer_lock = threading.RLock()
...@@ -44,6 +45,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -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.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.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.NEXT_MATCH, self._handle_next_match)
self.message_bus.subscribe(self.name, MessageType.START_INTRO, self._handle_start_intro)
logger.info("MatchTimer component initialized") logger.info("MatchTimer component initialized")
...@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -114,6 +116,8 @@ class MatchTimerComponent(ThreadedComponent):
self._handle_custom_message(message) self._handle_custom_message(message)
elif message.type == MessageType.NEXT_MATCH: elif message.type == MessageType.NEXT_MATCH:
self._handle_next_match(message) self._handle_next_match(message)
elif message.type == MessageType.START_INTRO:
self._handle_start_intro(message)
except Exception as e: except Exception as e:
logger.error(f"Failed to process message: {e}") logger.error(f"Failed to process message: {e}")
...@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -125,6 +129,7 @@ class MatchTimerComponent(ThreadedComponent):
self.timer_start_time = None self.timer_start_time = None
self.current_fixture_id = None self.current_fixture_id = None
self.current_match_id = None self.current_match_id = None
self.pending_match_id = None
# Unregister from message bus # Unregister from message bus
self.message_bus.unregister_component(self.name) self.message_bus.unregister_component(self.name)
...@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -229,12 +234,13 @@ class MatchTimerComponent(ThreadedComponent):
logger.error(f"Failed to handle custom message: {e}") logger.error(f"Failed to handle custom message: {e}")
def _handle_next_match(self, message: Message): 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: try:
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
match_id = message.data.get("match_id") match_id = message.data.get("match_id")
logger.info(f"Received NEXT_MATCH message for fixture {fixture_id}, match {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 # Find and start the next match
match_info = self._find_and_start_next_match() match_info = self._find_and_start_next_match()
...@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -245,12 +251,37 @@ class MatchTimerComponent(ThreadedComponent):
# Reset timer for next interval # Reset timer for next interval
match_interval = self._get_match_interval() match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, match_info['fixture_id']) self._start_timer(match_interval * 60, match_info['fixture_id'])
logger.info(f"Timer restarted for {match_interval} minute interval")
else: else:
logger.info("No more matches to start, stopping timer") logger.info("No more matches to start, stopping timer")
self._stop_timer() self._stop_timer()
except Exception as e: except Exception as e:
logger.error(f"Failed to handle NEXT_MATCH message: {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]): def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]):
"""Start the countdown timer""" """Start the countdown timer"""
...@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -279,28 +310,38 @@ class MatchTimerComponent(ThreadedComponent):
self._send_timer_update() self._send_timer_update()
def _on_timer_expired(self): def _on_timer_expired(self):
"""Handle timer expiration - start next match""" """Handle timer expiration - start the match that was prepared by START_INTRO"""
try: try:
logger.info("Match timer expired, starting next match...") logger.info("Match timer expired, starting prepared match...")
# Find and start the next match with self._timer_lock:
match_info = self._find_and_start_next_match() pending_match_id = self.pending_match_id
fixture_id = self.current_fixture_id
if match_info: if pending_match_id:
logger.info(f"Started match {match_info['match_id']} in fixture {match_info['fixture_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 self.message_bus.publish(match_start_message)
match_interval = self._get_match_interval() logger.info(f"Sent MATCH_START for prepared match {pending_match_id} in fixture {fixture_id}")
self._start_timer(match_interval * 60, match_info['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: else:
logger.info("No more matches to start, stopping timer") logger.warning("No pending match prepared by START_INTRO, stopping timer")
self._stop_timer() self._stop_timer()
except Exception as e: except Exception as e:
logger.error(f"Failed to handle timer expiration: {e}") logger.error(f"Failed to handle timer expiration: {e}")
# Reset timer on error # Stop timer on error - don't restart automatically
match_interval = self._get_match_interval() self._stop_timer()
self._start_timer(match_interval * 60, self.current_fixture_id)
def _find_and_start_next_match(self) -> Optional[Dict[str, Any]]: def _find_and_start_next_match(self) -> Optional[Dict[str, Any]]:
"""Find and start the next available match""" """Find and start the next available match"""
...@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -355,14 +396,14 @@ class MatchTimerComponent(ThreadedComponent):
target_fixture_id = target_match.fixture_id target_fixture_id = target_match.fixture_id
if target_match: if target_match:
# Send MATCH_START message # Send START_INTRO message
match_start_message = MessageBuilder.match_start( start_intro_message = MessageBuilder.start_intro(
sender=self.name, sender=self.name,
fixture_id=target_fixture_id or target_match.fixture_id, fixture_id=target_fixture_id or target_match.fixture_id,
match_id=target_match.id match_id=target_match.id
) )
self.message_bus.publish(match_start_message) self.message_bus.publish(start_intro_message)
return { return {
"fixture_id": target_fixture_id or target_match.fixture_id, "fixture_id": target_fixture_id or target_match.fixture_id,
......
...@@ -70,6 +70,7 @@ class MessageType(Enum): ...@@ -70,6 +70,7 @@ class MessageType(Enum):
PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH" PLAY_VIDEO_MATCH = "PLAY_VIDEO_MATCH"
PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE" PLAY_VIDEO_MATCH_DONE = "PLAY_VIDEO_MATCH_DONE"
PLAY_VIDEO_RESULT = "PLAY_VIDEO_RESULT" PLAY_VIDEO_RESULT = "PLAY_VIDEO_RESULT"
PLAY_VIDEO_RESULT_DONE = "PLAY_VIDEO_RESULT_DONE"
MATCH_DONE = "MATCH_DONE" MATCH_DONE = "MATCH_DONE"
NEXT_MATCH = "NEXT_MATCH" NEXT_MATCH = "NEXT_MATCH"
GAME_STATUS = "GAME_STATUS" GAME_STATUS = "GAME_STATUS"
...@@ -661,11 +662,27 @@ class MessageBuilder: ...@@ -661,11 +662,27 @@ class MessageBuilder:
) )
@staticmethod @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""" """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( return Message(
type=MessageType.PLAY_VIDEO_RESULT, type=MessageType.PLAY_VIDEO_RESULT,
sender=sender, 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={ data={
"fixture_id": fixture_id, "fixture_id": fixture_id,
"match_id": match_id, "match_id": match_id,
...@@ -674,15 +691,18 @@ class MessageBuilder: ...@@ -674,15 +691,18 @@ class MessageBuilder:
) )
@staticmethod @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""" """Create MATCH_DONE message"""
data = {
"fixture_id": fixture_id,
"match_id": match_id
}
if result is not None:
data["result"] = result
return Message( return Message(
type=MessageType.MATCH_DONE, type=MessageType.MATCH_DONE,
sender=sender, sender=sender,
data={ data=data
"fixture_id": fixture_id,
"match_id": match_id
}
) )
@staticmethod @staticmethod
......
...@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -24,7 +24,7 @@ class UDPBroadcastComponent(ThreadedComponent):
self.broadcast_port = broadcast_port self.broadcast_port = broadcast_port
self.broadcast_interval = 30.0 # 30 seconds self.broadcast_interval = 30.0 # 30 seconds
self.socket: Optional[socket.socket] = None self.socket: Optional[socket.socket] = None
# Server information to broadcast # Server information to broadcast
self.server_info: Dict[str, Any] = { self.server_info: Dict[str, Any] = {
"service": "MBetterClient", "service": "MBetterClient",
...@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -34,10 +34,13 @@ class UDPBroadcastComponent(ThreadedComponent):
"url": "http://127.0.0.1:5001", "url": "http://127.0.0.1:5001",
"timestamp": time.time() "timestamp": time.time()
} }
# Shutdown event for responsive shutdown
self.shutdown_event = threading.Event()
# Register message queue # Register message queue
self.message_queue = self.message_bus.register_component(self.name) self.message_queue = self.message_bus.register_component(self.name)
logger.info(f"UDP Broadcast component initialized on port {broadcast_port}") logger.info(f"UDP Broadcast component initialized on port {broadcast_port}")
def initialize(self) -> bool: def initialize(self) -> bool:
...@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -47,13 +50,18 @@ class UDPBroadcastComponent(ThreadedComponent):
self.socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 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_BROADCAST, 1)
self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 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 # Subscribe to system status messages to get web server info
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status) self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
logger.info("UDP broadcast socket initialized successfully") logger.info("UDP broadcast socket initialized successfully")
return True return True
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast initialization failed: {e}") logger.error(f"UDP broadcast initialization failed: {e}")
return False return False
...@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -68,28 +76,33 @@ class UDPBroadcastComponent(ThreadedComponent):
last_broadcast_time = 0 last_broadcast_time = 0
while self.running: while self.running and not self.shutdown_event.is_set():
try: try:
current_time = time.time() current_time = time.time()
# Process messages # Process messages with shorter timeout for responsive shutdown
message = self.message_bus.get_message(self.name, timeout=1.0) message = self.message_bus.get_message(self.name, timeout=0.5)
if message: if message:
self._process_message(message) self._process_message(message)
# Broadcast every 30 seconds # Broadcast every 30 seconds
if current_time - last_broadcast_time >= self.broadcast_interval: if current_time - last_broadcast_time >= self.broadcast_interval:
self._broadcast_server_info() self._broadcast_server_info()
last_broadcast_time = current_time last_broadcast_time = current_time
# Update heartbeat # Update heartbeat
self.heartbeat() self.heartbeat()
time.sleep(0.5) # Check shutdown event more frequently
if self.shutdown_event.wait(0.5):
break
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast loop error: {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: except Exception as e:
logger.error(f"UDP broadcast run failed: {e}") logger.error(f"UDP broadcast run failed: {e}")
...@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -100,11 +113,19 @@ class UDPBroadcastComponent(ThreadedComponent):
"""Shutdown UDP broadcast component""" """Shutdown UDP broadcast component"""
try: try:
logger.info("Shutting down UDP broadcast...") 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: if self.socket:
self.socket.close() try:
self.socket = None self.socket.close()
except Exception as e:
logger.debug(f"Error closing UDP socket: {e}")
finally:
self.socket = None
except Exception as e: except Exception as e:
logger.error(f"UDP broadcast shutdown error: {e}") logger.error(f"UDP broadcast shutdown error: {e}")
...@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent): ...@@ -170,8 +191,17 @@ class UDPBroadcastComponent(ThreadedComponent):
for broadcast_addr in broadcast_addresses: for broadcast_addr in broadcast_addresses:
try: try:
self.socket.sendto(broadcast_data, (broadcast_addr, self.broadcast_port)) if self.socket and not self.shutdown_event.is_set():
logger.debug(f"Broadcasted to {broadcast_addr}:{self.broadcast_port}") 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: except Exception as e:
logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}") logger.debug(f"Failed to broadcast to {broadcast_addr}: {e}")
......
This diff is collapsed.
...@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent): ...@@ -351,15 +351,52 @@ class WebDashboard(ThreadedComponent):
def _create_server(self): def _create_server(self):
"""Create HTTP/HTTPS server with SocketIO support""" """Create HTTP/HTTPS server with SocketIO support"""
try: try:
from werkzeug.serving import make_server
protocol = "HTTP" protocol = "HTTP"
if self.settings.enable_ssl: if self.settings.enable_ssl:
protocol = "HTTPS" 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("⚠️ Using self-signed certificate - browsers will show security warning")
logger.info(" You can safely proceed by accepting the certificate") logger.info(" You can safely proceed by accepting the certificate")
...@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent): ...@@ -427,29 +464,22 @@ class WebDashboard(ThreadedComponent):
socketio_status = "with SocketIO" if self.socketio else "without SocketIO" 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}") logger.info(f"Starting {protocol} server {socketio_status} on {self.settings.host}:{self.settings.port}")
if self.socketio: if self.server:
# Run SocketIO server # Use the shutdown-capable server
self.socketio.run( # serve_forever() will block until shutdown() is called
self.app, self.server.serve_forever()
host=self.settings.host, logger.info("HTTP server stopped")
port=self.settings.port,
debug=False,
use_reloader=False,
log_output=False
)
else: else:
# Run Flask server without SocketIO logger.error("Server not created, cannot start")
self.app.run( return
host=self.settings.host,
port=self.settings.port,
debug=False,
use_reloader=False
)
except Exception as e: except Exception as e:
if self.running: # Only log if not shutting down if self.running: # Only log if not shutting down
protocol = "HTTPS" if self.settings.enable_ssl else "HTTP" protocol = "HTTPS" if self.settings.enable_ssl else "HTTP"
logger.error(f"{protocol} server error: {e}") 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): def _setup_ssl_error_suppression(self):
"""Setup logging filter to suppress expected SSL connection errors""" """Setup logging filter to suppress expected SSL connection errors"""
...@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent): ...@@ -492,10 +522,16 @@ class WebDashboard(ThreadedComponent):
"""Shutdown web dashboard""" """Shutdown web dashboard"""
try: try:
logger.info("Shutting down WebDashboard...") logger.info("Shutting down WebDashboard...")
# Shutdown the HTTP server
if self.server: if self.server:
logger.info("Shutting down HTTP server...")
self.server.shutdown() 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: except Exception as e:
logger.error(f"WebDashboard shutdown error: {e}") logger.error(f"WebDashboard shutdown error: {e}")
......
...@@ -410,17 +410,9 @@ def api_tokens(): ...@@ -410,17 +410,9 @@ def api_tokens():
def fixtures(): def fixtures():
"""Fixtures management page""" """Fixtures management page"""
try: 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', return render_template('dashboard/fixtures.html',
user=current_user, user=current_user,
page_title="Fixtures") page_title="Fixtures")
except Exception as e: except Exception as e:
logger.error(f"Fixtures page error: {e}") logger.error(f"Fixtures page error: {e}")
flash("Error loading fixtures", "error") flash("Error loading fixtures", "error")
...@@ -432,18 +424,10 @@ def fixtures(): ...@@ -432,18 +424,10 @@ def fixtures():
def fixture_details(fixture_id): def fixture_details(fixture_id):
"""Fixture details page showing all matches in the fixture""" """Fixture details page showing all matches in the fixture"""
try: 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', return render_template('dashboard/fixture_details.html',
user=current_user, user=current_user,
fixture_id=fixture_id, fixture_id=fixture_id,
page_title=f"Fixture Details - Fixture #{fixture_id}") page_title=f"Fixture Details - Fixture #{fixture_id}")
except Exception as e: except Exception as e:
logger.error(f"Fixture details page error: {e}") logger.error(f"Fixture details page error: {e}")
flash("Error loading fixture details", "error") flash("Error loading fixture details", "error")
...@@ -455,19 +439,11 @@ def fixture_details(fixture_id): ...@@ -455,19 +439,11 @@ def fixture_details(fixture_id):
def match_details(match_id, fixture_id): def match_details(match_id, fixture_id):
"""Match details page showing match information and outcomes""" """Match details page showing match information and outcomes"""
try: 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', return render_template('dashboard/match_details.html',
user=current_user, user=current_user,
match_id=match_id, match_id=match_id,
fixture_id=fixture_id, fixture_id=fixture_id,
page_title=f"Match Details - Match #{match_id}") page_title=f"Match Details - Match #{match_id}")
except Exception as e: except Exception as e:
logger.error(f"Match details page error: {e}") logger.error(f"Match details page error: {e}")
flash("Error loading match details", "error") flash("Error loading match details", "error")
...@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id): ...@@ -4222,7 +4198,8 @@ def get_cashier_bet_details(bet_id):
'fighter1_township': match.fighter1_township, 'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township, 'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township, 'venue_kampala_township': match.venue_kampala_township,
'status': match.status 'status': match.status,
'result': match.result
} }
else: else:
detail_data['match'] = None detail_data['match'] = None
...@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id): ...@@ -4508,7 +4485,8 @@ def verify_bet_details(bet_id):
'fighter1_township': match.fighter1_township, 'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township, 'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township, 'venue_kampala_township': match.venue_kampala_township,
'status': match.status 'status': match.status,
'result': match.result
} }
else: else:
detail_data['match'] = None detail_data['match'] = None
......
...@@ -590,6 +590,7 @@ function displayBetDetails(bet) { ...@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr> <tr>
<td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br> <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> <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>
<td><span class="badge bg-primary">${detail.outcome}</span></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> <td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td>
......
...@@ -590,6 +590,7 @@ function displayBetDetails(bet) { ...@@ -590,6 +590,7 @@ function displayBetDetails(bet) {
<tr> <tr>
<td><strong>Match #${detail.match ? detail.match.match_number : 'Unknown'}</strong><br> <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> <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>
<td><span class="badge bg-primary">${detail.outcome}</span></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> <td><strong class="currency-amount" data-amount="${detail.amount}">${formatCurrency(detail.amount)}</strong></td>
......
...@@ -579,6 +579,7 @@ ...@@ -579,6 +579,7 @@
<div class="col-8"> <div class="col-8">
<h6 class="fw-bold mb-1">Match #${detail.match ? detail.match.match_number : 'Unknown'}</h6> <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> <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> <span class="badge bg-primary">${detail.outcome}</span>
</div> </div>
<div class="col-4 text-end"> <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