Intro stage complete, fixture update management for failed downloads

completed
parent 816816c7
This diff is collapsed.
......@@ -52,6 +52,7 @@ class MbetterClientApplication:
# Timer for automated game start
self._game_start_timer: Optional[threading.Timer] = None
self._original_timer_interval: Optional[int] = None # Store original interval for retries
logger.info("MbetterClient application initialized")
......@@ -178,6 +179,7 @@ class MbetterClientApplication:
self.message_bus.subscribe("core", MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message)
self.message_bus.subscribe("core", MessageType.CONFIG_UPDATE, self._handle_config_update)
self.message_bus.subscribe("core", MessageType.LOG_ENTRY, self._handle_log_entry)
self.message_bus.subscribe("core", MessageType.GAME_STATUS, self._handle_game_status_response)
logger.info("Message bus initialized")
return True
......@@ -611,6 +613,8 @@ class MbetterClientApplication:
self._handle_config_request(message)
elif message.type == MessageType.START_GAME:
self._handle_start_game_message(message)
elif message.type == MessageType.GAME_STATUS:
self._handle_game_status_response(message)
elif message.type == MessageType.SYSTEM_SHUTDOWN:
self._handle_shutdown_message(message)
else:
......@@ -758,6 +762,41 @@ class MbetterClientApplication:
except Exception as e:
logger.error(f"Failed to handle START_GAME message: {e}")
def _handle_game_status_response(self, message: Message):
"""Handle GAME_STATUS responses, particularly for timer-initiated START_GAME failures"""
try:
status = message.data.get("status", "unknown")
sender = message.sender
# Only process responses that might be related to our timer-initiated START_GAME
# We check if we have an active timer that might need restarting
if self._game_start_timer is None and self._original_timer_interval is None:
# No active timer management needed
return
# Check if this is a failure response that should trigger timer restart
failure_statuses = ["waiting_for_downloads", "discarded", "error", "no_matches"]
if status in failure_statuses:
logger.info(f"START_GAME failed with status '{status}' from {sender} - restarting timer")
# Cancel any existing timer
self._cancel_game_timer()
# Restart timer with original interval
if self._original_timer_interval is not None:
logger.info(f"Restarting game start timer with original interval: {self._original_timer_interval} minutes")
self._start_game_timer_with_interval(self._original_timer_interval)
else:
logger.warning("No original timer interval available for restart")
elif status == "started":
logger.info(f"START_GAME succeeded with status '{status}' from {sender} - timer job completed")
# Game started successfully, clear timer state
self._original_timer_interval = None
except Exception as e:
logger.error(f"Failed to handle GAME_STATUS response: {e}")
def _run_additional_tasks(self):
"""Placeholder for additional periodic tasks"""
......@@ -770,6 +809,9 @@ class MbetterClientApplication:
if self._start_timer_minutes is None:
return
# Store the original interval for potential retries
self._original_timer_interval = self._start_timer_minutes
# Special case: --start-timer 0 means 10 seconds delay for system initialization
if self._start_timer_minutes == 0:
delay_seconds = 10
......@@ -782,6 +824,27 @@ class MbetterClientApplication:
self._game_start_timer.daemon = True
self._game_start_timer.start()
def _start_game_timer_with_interval(self, minutes: int):
"""Start the game timer with a specific interval (used for retries)"""
if minutes < 0:
logger.error(f"Invalid timer interval: {minutes} minutes")
return
# Update stored interval
self._original_timer_interval = minutes
# Special case: timer 0 means 10 seconds delay for system initialization
if minutes == 0:
delay_seconds = 10
logger.info(f"Restarting command line game timer: 0 minutes = 10 seconds delay for system initialization")
else:
delay_seconds = minutes * 60
logger.info(f"Restarting command line game timer: {minutes} minutes ({delay_seconds} seconds)")
self._game_start_timer = threading.Timer(delay_seconds, self._on_game_timer_expired)
self._game_start_timer.daemon = True
self._game_start_timer.start()
def _on_game_timer_expired(self):
"""Called when the game start timer expires"""
logger.info("Game start timer expired, sending START_GAME message")
......@@ -804,6 +867,7 @@ class MbetterClientApplication:
logger.info("Cancelling game start timer")
self._game_start_timer.cancel()
self._game_start_timer = None
# Note: We keep _original_timer_interval for potential retries
def _check_component_health(self):
"""Check health of all components"""
......@@ -841,8 +905,9 @@ class MbetterClientApplication:
self.running = False
self.shutdown_event.set()
# Cancel game timer if running
# Cancel game timer if running and clear timer state
self._cancel_game_timer()
self._original_timer_interval = None
# Send shutdown message to all components
if self.message_bus:
......
This diff is collapsed.
......@@ -5,7 +5,7 @@ Flask web dashboard application for MbetterClient
import time
import logging
from pathlib import Path
from typing import Optional, Dict, Any
from typing import Optional, Dict, Any, List
from flask import Flask, request, jsonify, render_template, redirect, url_for, session, g
from flask_login import LoginManager, login_required, current_user
from flask_jwt_extended import JWTManager, create_access_token, jwt_required as flask_jwt_required
......@@ -57,6 +57,12 @@ class WebDashboard(ThreadedComponent):
"match_id": None,
"start_time": None
}
# Client notification queue for long-polling clients
self.notification_queue: List[Dict[str, Any]] = []
self.notification_lock = threading.Lock()
self.waiting_clients: List[threading.Event] = [] # Events for waiting long-poll clients
self.waiting_clients_lock = threading.Lock()
# Register message queue
self.message_queue = self.message_bus.register_component(self.name)
......@@ -89,6 +95,10 @@ class WebDashboard(ThreadedComponent):
self.message_bus.subscribe(self.name, MessageType.CONFIG_UPDATE, self._handle_config_update)
self.message_bus.subscribe(self.name, MessageType.SYSTEM_STATUS, self._handle_system_status)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
# Subscribe to messages for client notifications
self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_client_notification)
self.message_bus.subscribe(self.name, MessageType.MATCH_START, self._handle_client_notification)
self.message_bus.subscribe(self.name, MessageType.GAME_STATUS, self._handle_client_notification)
logger.info("WebDashboard initialized successfully")
return True
......@@ -537,6 +547,7 @@ class WebDashboard(ThreadedComponent):
try:
response = message.data.get("response")
timer_update = message.data.get("timer_update")
fixture_status_update = message.data.get("fixture_status_update")
if response == "timer_state":
# Update stored timer state
......@@ -560,24 +571,96 @@ class WebDashboard(ThreadedComponent):
# Handle periodic timer updates from match_timer component
self.current_timer_state.update(timer_update)
logger.debug(f"Timer update received: {timer_update}")
# Broadcast timer update to connected clients via global message bus
try:
timer_update_message = Message(
type=MessageType.CUSTOM,
sender=self.name,
data={
"timer_update": timer_update,
"timestamp": time.time()
}
)
self.message_bus.publish(timer_update_message, broadcast=True)
logger.debug("Timer update broadcasted to clients")
except Exception as broadcast_e:
logger.error(f"Failed to broadcast timer update: {broadcast_e}")
# Add timer update to notification queue for long-polling clients
self._add_client_notification("TIMER_UPDATE", timer_update, message.timestamp)
elif fixture_status_update:
# Handle fixture status updates from games_thread
logger.debug(f"Fixture status update received: {fixture_status_update}")
# Add fixture status update to notification queue for long-polling clients
self._add_client_notification("FIXTURE_STATUS_UPDATE", fixture_status_update, message.timestamp)
except Exception as e:
logger.error(f"Failed to handle custom message: {e}")
def _handle_client_notification(self, message: Message):
"""Handle messages that should be sent to long-polling clients"""
try:
# Convert message to notification format
notification_data = {
"type": message.type.value,
"data": message.data,
"timestamp": message.timestamp,
"sender": message.sender
}
# Add to notification queue
self._add_client_notification(message.type.value, message.data, message.timestamp, message.sender)
except Exception as e:
logger.error(f"Failed to handle client notification: {e}")
def _add_client_notification(self, notification_type: str, data: Dict[str, Any],
timestamp: float, sender: str = None):
"""Add notification to the client queue"""
try:
with self.notification_lock:
notification = {
"type": notification_type,
"data": data,
"timestamp": timestamp
}
if sender:
notification["sender"] = sender
self.notification_queue.append(notification)
# Keep queue size reasonable (limit to last 100 notifications)
if len(self.notification_queue) > 100:
self.notification_queue = self.notification_queue[-50:]
logger.debug(f"Added client notification: {notification_type}")
# Wake up all waiting clients
with self.waiting_clients_lock:
for event in self.waiting_clients:
event.set()
self.waiting_clients.clear()
except Exception as e:
logger.error(f"Failed to add client notification: {e}")
def get_pending_notifications(self) -> List[Dict[str, Any]]:
"""Get the first pending notification for a client (one at a time)"""
try:
with self.notification_lock:
if self.notification_queue:
# Return only the first notification and remove it from queue
notification = self.notification_queue.pop(0)
return [notification]
return []
except Exception as e:
logger.error(f"Failed to get pending notifications: {e}")
return []
def register_waiting_client(self, event: threading.Event):
"""Register a waiting client Event to be notified when notifications arrive"""
try:
with self.waiting_clients_lock:
self.waiting_clients.append(event)
except Exception as e:
logger.error(f"Failed to register waiting client: {e}")
def unregister_waiting_client(self, event: threading.Event):
"""Unregister a waiting client Event"""
try:
with self.waiting_clients_lock:
if event in self.waiting_clients:
self.waiting_clients.remove(event)
except Exception as e:
logger.error(f"Failed to unregister waiting client: {e}")
def get_app_context(self):
"""Get Flask application context"""
......
......@@ -2600,90 +2600,61 @@ def notifications():
import threading
import ssl
import socket
from ..core.message_bus import MessageType
# Get timeout from query parameter (default 30 seconds)
timeout = int(request.args.get('timeout', 30))
if timeout > 60: # Max 60 seconds
timeout = 60
# Create a queue for this client
notification_queue = []
# Create an event for timeout handling
notification_received = threading.Event()
def message_handler(message):
"""Handle incoming messages for this client"""
if message.type in [MessageType.START_GAME, MessageType.GAME_STARTED, MessageType.MATCH_START, MessageType.GAME_STATUS]:
notification_data = {
"type": message.type.value,
"data": message.data,
"timestamp": message.timestamp,
"sender": message.sender
}
notification_queue.append(notification_data)
notification_received.set()
elif message.type == MessageType.CUSTOM and "timer_update" in message.data:
# Handle timer updates from match_timer component
notification_data = {
"type": "TIMER_UPDATE",
"data": message.data["timer_update"],
"timestamp": message.timestamp,
"sender": message.sender
}
notification_queue.append(notification_data)
notification_received.set()
elif message.type == MessageType.CUSTOM and "fixture_status_update" in message.data:
# Handle fixture status updates from games_thread
notification_data = {
"type": "FIXTURE_STATUS_UPDATE",
"data": message.data["fixture_status_update"],
"timestamp": message.timestamp,
"sender": message.sender
}
notification_queue.append(notification_data)
notification_received.set()
# Subscribe to relevant message types
if api_bp.message_bus:
api_bp.message_bus.subscribe_global(MessageType.START_GAME, message_handler)
api_bp.message_bus.subscribe_global(MessageType.MATCH_START, message_handler)
api_bp.message_bus.subscribe_global(MessageType.GAME_STATUS, message_handler)
api_bp.message_bus.subscribe_global(MessageType.CUSTOM, message_handler)
# Wait for notification or timeout
notification_received.wait(timeout=timeout)
# Check for pending notifications from the web dashboard
if hasattr(g, 'main_app') and g.main_app and hasattr(g.main_app, 'web_dashboard'):
# Register this event with the web dashboard so it gets notified when notifications arrive
g.main_app.web_dashboard.register_waiting_client(notification_received)
# Unsubscribe from messages safely
if api_bp.message_bus:
try:
# Use proper unsubscribe methods instead of direct removal
for msg_type in [MessageType.START_GAME, MessageType.MATCH_START, MessageType.GAME_STATUS, MessageType.CUSTOM]:
try:
if hasattr(api_bp.message_bus, '_global_handlers') and msg_type in api_bp.message_bus._global_handlers:
handlers = api_bp.message_bus._global_handlers[msg_type]
if message_handler in handlers:
handlers.remove(message_handler)
except (AttributeError, KeyError, ValueError) as e:
logger.debug(f"Handler cleanup warning for {msg_type}: {e}")
except Exception as e:
logger.warning(f"Error during notification cleanup: {e}")
# Prepare response data
if notification_queue:
# Return the first notification received
notification = notification_queue[0]
logger.debug(f"Notification sent to client: {notification['type']}")
response_data = {
"success": True,
"notification": notification
}
# First check for any pending notifications
pending_notifications = g.main_app.web_dashboard.get_pending_notifications()
if pending_notifications:
# Return pending notifications immediately
logger.debug(f"Returning {len(pending_notifications)} pending notifications to client")
response_data = {
"success": True,
"notifications": pending_notifications
}
else:
# No pending notifications, wait for new ones or timeout
logger.debug(f"Waiting for notifications with {timeout}s timeout")
notification_received.wait(timeout=timeout)
# After waiting, check again for any notifications that arrived
pending_notifications = g.main_app.web_dashboard.get_pending_notifications()
if pending_notifications:
logger.debug(f"Returning {len(pending_notifications)} notifications after wait")
response_data = {
"success": True,
"notifications": pending_notifications
}
else:
# Timeout - return empty response
logger.debug("Notification wait timed out, returning empty response")
response_data = {
"success": True,
"notifications": []
}
finally:
# Always unregister the event when done
g.main_app.web_dashboard.unregister_waiting_client(notification_received)
else:
# Timeout - return empty response
# Web dashboard not available, return empty response
response_data = {
"success": True,
"notification": None
"notifications": []
}
# Response data is already prepared above
# Handle SSL/connection errors gracefully when sending response
try:
return jsonify(response_data)
......
......@@ -5,14 +5,20 @@
// Dashboard namespace
window.Dashboard = (function() {
'use strict';
let config = {};
let statusInterval = null;
let isOnline = navigator.onLine;
let cache = {};
let initialized = false; // Prevent multiple initializations
// Initialize dashboard
function init(userConfig) {
if (initialized) {
console.log('Dashboard already initialized, skipping...');
return;
}
config = Object.assign({
statusUpdateInterval: 5000,
apiEndpoint: '/api',
......@@ -36,6 +42,7 @@ window.Dashboard = (function() {
// Initialize match timer
initMatchTimer();
initialized = true;
console.log('Dashboard initialized successfully');
}
......@@ -463,9 +470,11 @@ window.Dashboard = (function() {
let lastServerSync = 0;
let cachedMatchInterval = null; // Cache the match interval configuration
const SYNC_INTERVAL = 30000; // Sync with server every 30 seconds
let longPollingActive = false; // Flag to control long polling
let longPollingController = null; // AbortController for cancelling requests
function initMatchTimer() {
console.log('Initializing server-only match timer (no local countdown)...');
console.log('Initializing server-only match timer with real-time notifications...');
// Load match interval config once at initialization
loadMatchIntervalConfig().then(function(intervalSeconds) {
......@@ -474,9 +483,9 @@ window.Dashboard = (function() {
// Initial sync with server
syncWithServerTimer();
// REMOVED: No periodic sync - rely on notifications
// REMOVED: No local countdown - rely on server updates only
// Start real-time notification polling for timer updates
startNotificationPolling();
}).catch(function(error) {
console.error('Failed to load match timer config at initialization:', error);
// Use default and continue
......@@ -484,6 +493,9 @@ window.Dashboard = (function() {
// Initial sync with server
syncWithServerTimer();
// Start real-time notification polling for timer updates
startNotificationPolling();
});
}
......@@ -568,6 +580,17 @@ window.Dashboard = (function() {
}
}
function stopNotificationPolling() {
console.log('Stopping long polling for notifications...');
longPollingActive = false;
// Cancel any ongoing request
if (longPollingController) {
longPollingController.abort();
longPollingController = null;
}
}
function updateMatchTimerDisplay() {
let displaySeconds = serverTimerState.remaining_seconds;
......@@ -661,6 +684,130 @@ window.Dashboard = (function() {
});
}
function startNotificationPolling() {
console.log('Starting long polling for real-time notifications...');
if (longPollingActive) {
stopNotificationPolling();
}
longPollingActive = true;
performLongPoll();
}
function performLongPoll() {
if (!longPollingActive || !isOnline) {
return;
}
// Create AbortController for this request
longPollingController = new AbortController();
const signal = longPollingController.signal;
// Make long polling request (server handles 30-second timeout)
const url = config.apiEndpoint + '/notifications?_=' + Date.now() + Math.random();
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: signal
};
fetch(url, options)
.then(function(response) {
if (!response.ok) {
throw new Error('HTTP ' + response.status);
}
return response.json();
})
.then(function(data) {
if (data.success && data.notifications && data.notifications.length > 0) {
// Process each notification
data.notifications.forEach(function(notification) {
handleNotification(notification);
});
}
// Immediately start the next long poll after processing notifications
if (longPollingActive) {
// Small delay to prevent overwhelming the server
setTimeout(performLongPoll, 100);
}
})
.catch(function(error) {
if (error.name === 'AbortError') {
// Request was cancelled, don't restart
console.debug('Long poll cancelled');
return;
}
console.debug('Long poll failed:', error.message);
// Retry after a delay if still active
if (longPollingActive) {
setTimeout(performLongPoll, 2000); // Retry after 2 seconds on error
}
});
}
function handleNotification(notification) {
console.log('Received notification:', notification.type, notification);
if (notification.type === 'TIMER_UPDATE' && notification.data) {
// Update timer state from server notification
serverTimerState = {
running: notification.data.running || false,
remaining_seconds: notification.data.remaining_seconds || 0,
total_seconds: notification.data.total_seconds || 0,
fixture_id: notification.data.fixture_id || null,
match_id: notification.data.match_id || null,
start_time: notification.data.start_time || null
};
// Update display immediately
updateMatchTimerDisplay();
console.log('Timer updated from notification:', serverTimerState);
}
else if (notification.type === 'START_GAME') {
console.log('Start game notification received');
onStartGameMessage();
showNotification('Games started - match timer is now running', 'success');
}
else if (notification.type === 'MATCH_START') {
console.log('Match started:', notification.data);
showNotification('New match has started!', 'info');
}
else if (notification.type === 'GAME_STATUS') {
console.log('Game status update:', notification.data);
// Update system status if provided
if (notification.data.status) {
const systemStatus = document.getElementById('system-status');
if (systemStatus) {
systemStatus.textContent = notification.data.status.charAt(0).toUpperCase() + notification.data.status.slice(1);
systemStatus.className = 'badge ' + (notification.data.status === 'running' ? 'bg-success' : 'bg-secondary');
}
}
}
else if (notification.type === 'FIXTURE_STATUS_UPDATE') {
console.log('Fixture status update:', notification.data);
// Trigger page refresh if on fixtures page
if (window.location.pathname.includes('/fixtures')) {
// Refresh fixtures data if function exists
if (typeof loadFixtures === 'function') {
loadFixtures();
}
if (typeof loadFixtureDetails === 'function') {
loadFixtureDetails();
}
}
}
else {
console.log('Unknown notification type:', notification.type);
}
}
function onStartGameMessage() {
console.log('Received START_GAME message, initializing timer...');
......@@ -675,7 +822,6 @@ window.Dashboard = (function() {
// Update display
updateMatchTimerDisplay();
startLocalCountdown();
showNotification('Games started - match timer is now running', 'success');
});
......@@ -731,7 +877,8 @@ window.Dashboard = (function() {
startMatchTimer: startMatchTimer,
stopMatchTimer: stopMatchTimer,
resetMatchTimer: resetMatchTimer,
startNextMatch: startNextMatch
startNextMatch: startNextMatch,
stopNotificationPolling: stopNotificationPolling
};
})();
......
......@@ -1082,8 +1082,6 @@ function showNotification(message, type = 'info') {
}, 3000);
}
</script>
<!-- Include the main dashboard.js for timer functionality -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
<script>
// Initialize dashboard with timer functionality
document.addEventListener('DOMContentLoaded', function() {
......
......@@ -738,8 +738,6 @@ function loadAvailableTemplates() {
});
}
</script>
<!-- Include the main dashboard.js for timer functionality -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
<script>
// Initialize dashboard with timer functionality
document.addEventListener('DOMContentLoaded', function() {
......
......@@ -927,8 +927,6 @@ function showNotification(message, type = 'info') {
}, 3000);
}
</script>
<!-- Include the main dashboard.js for timer functionality -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
<script>
// Initialize dashboard with timer functionality
document.addEventListener('DOMContentLoaded', function() {
......
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