Intro stage complete, fixture update management for failed downloads

completed
parent 816816c7
This diff is collapsed.
...@@ -52,6 +52,7 @@ class MbetterClientApplication: ...@@ -52,6 +52,7 @@ class MbetterClientApplication:
# Timer for automated game start # Timer for automated game start
self._game_start_timer: Optional[threading.Timer] = None 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") logger.info("MbetterClient application initialized")
...@@ -178,6 +179,7 @@ class MbetterClientApplication: ...@@ -178,6 +179,7 @@ class MbetterClientApplication:
self.message_bus.subscribe("core", MessageType.SYSTEM_SHUTDOWN, self._handle_shutdown_message) 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.CONFIG_UPDATE, self._handle_config_update)
self.message_bus.subscribe("core", MessageType.LOG_ENTRY, self._handle_log_entry) 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") logger.info("Message bus initialized")
return True return True
...@@ -611,6 +613,8 @@ class MbetterClientApplication: ...@@ -611,6 +613,8 @@ class MbetterClientApplication:
self._handle_config_request(message) self._handle_config_request(message)
elif message.type == MessageType.START_GAME: elif message.type == MessageType.START_GAME:
self._handle_start_game_message(message) self._handle_start_game_message(message)
elif message.type == MessageType.GAME_STATUS:
self._handle_game_status_response(message)
elif message.type == MessageType.SYSTEM_SHUTDOWN: elif message.type == MessageType.SYSTEM_SHUTDOWN:
self._handle_shutdown_message(message) self._handle_shutdown_message(message)
else: else:
...@@ -759,6 +763,41 @@ class MbetterClientApplication: ...@@ -759,6 +763,41 @@ class MbetterClientApplication:
except Exception as e: except Exception as e:
logger.error(f"Failed to handle START_GAME message: {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): def _run_additional_tasks(self):
"""Placeholder for additional periodic tasks""" """Placeholder for additional periodic tasks"""
# TODO: Implement additional tasks as requested by user # TODO: Implement additional tasks as requested by user
...@@ -770,6 +809,9 @@ class MbetterClientApplication: ...@@ -770,6 +809,9 @@ class MbetterClientApplication:
if self._start_timer_minutes is None: if self._start_timer_minutes is None:
return 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 # Special case: --start-timer 0 means 10 seconds delay for system initialization
if self._start_timer_minutes == 0: if self._start_timer_minutes == 0:
delay_seconds = 10 delay_seconds = 10
...@@ -782,6 +824,27 @@ class MbetterClientApplication: ...@@ -782,6 +824,27 @@ class MbetterClientApplication:
self._game_start_timer.daemon = True self._game_start_timer.daemon = True
self._game_start_timer.start() 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): def _on_game_timer_expired(self):
"""Called when the game start timer expires""" """Called when the game start timer expires"""
logger.info("Game start timer expired, sending START_GAME message") logger.info("Game start timer expired, sending START_GAME message")
...@@ -804,6 +867,7 @@ class MbetterClientApplication: ...@@ -804,6 +867,7 @@ class MbetterClientApplication:
logger.info("Cancelling game start timer") logger.info("Cancelling game start timer")
self._game_start_timer.cancel() self._game_start_timer.cancel()
self._game_start_timer = None self._game_start_timer = None
# Note: We keep _original_timer_interval for potential retries
def _check_component_health(self): def _check_component_health(self):
"""Check health of all components""" """Check health of all components"""
...@@ -841,8 +905,9 @@ class MbetterClientApplication: ...@@ -841,8 +905,9 @@ class MbetterClientApplication:
self.running = False self.running = False
self.shutdown_event.set() self.shutdown_event.set()
# Cancel game timer if running # Cancel game timer if running and clear timer state
self._cancel_game_timer() self._cancel_game_timer()
self._original_timer_interval = None
# Send shutdown message to all components # Send shutdown message to all components
if self.message_bus: if self.message_bus:
......
This diff is collapsed.
...@@ -5,7 +5,7 @@ Flask web dashboard application for MbetterClient ...@@ -5,7 +5,7 @@ Flask web dashboard application for MbetterClient
import time import time
import logging import logging
from pathlib import Path 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 import Flask, request, jsonify, render_template, redirect, url_for, session, g
from flask_login import LoginManager, login_required, current_user from flask_login import LoginManager, login_required, current_user
from flask_jwt_extended import JWTManager, create_access_token, jwt_required as flask_jwt_required from flask_jwt_extended import JWTManager, create_access_token, jwt_required as flask_jwt_required
...@@ -58,6 +58,12 @@ class WebDashboard(ThreadedComponent): ...@@ -58,6 +58,12 @@ class WebDashboard(ThreadedComponent):
"start_time": 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 # Register message queue
self.message_queue = self.message_bus.register_component(self.name) self.message_queue = self.message_bus.register_component(self.name)
...@@ -89,6 +95,10 @@ class WebDashboard(ThreadedComponent): ...@@ -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.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.SYSTEM_STATUS, self._handle_system_status)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message) 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") logger.info("WebDashboard initialized successfully")
return True return True
...@@ -537,6 +547,7 @@ class WebDashboard(ThreadedComponent): ...@@ -537,6 +547,7 @@ class WebDashboard(ThreadedComponent):
try: try:
response = message.data.get("response") response = message.data.get("response")
timer_update = message.data.get("timer_update") timer_update = message.data.get("timer_update")
fixture_status_update = message.data.get("fixture_status_update")
if response == "timer_state": if response == "timer_state":
# Update stored timer state # Update stored timer state
...@@ -561,23 +572,95 @@ class WebDashboard(ThreadedComponent): ...@@ -561,23 +572,95 @@ class WebDashboard(ThreadedComponent):
self.current_timer_state.update(timer_update) self.current_timer_state.update(timer_update)
logger.debug(f"Timer update received: {timer_update}") logger.debug(f"Timer update received: {timer_update}")
# Broadcast timer update to connected clients via global message bus # 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: try:
timer_update_message = Message( # Convert message to notification format
type=MessageType.CUSTOM, notification_data = {
sender=self.name, "type": message.type.value,
data={ "data": message.data,
"timer_update": timer_update, "timestamp": message.timestamp,
"timestamp": time.time() "sender": message.sender
} }
)
self.message_bus.publish(timer_update_message, broadcast=True) # Add to notification queue
logger.debug("Timer update broadcasted to clients") self._add_client_notification(message.type.value, message.data, message.timestamp, message.sender)
except Exception as broadcast_e:
logger.error(f"Failed to broadcast timer update: {broadcast_e}")
except Exception as e: except Exception as e:
logger.error(f"Failed to handle custom message: {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): def get_app_context(self):
"""Get Flask application context""" """Get Flask application context"""
......
...@@ -2600,89 +2600,60 @@ def notifications(): ...@@ -2600,89 +2600,60 @@ def notifications():
import threading import threading
import ssl import ssl
import socket import socket
from ..core.message_bus import MessageType
# Get timeout from query parameter (default 30 seconds) # Get timeout from query parameter (default 30 seconds)
timeout = int(request.args.get('timeout', 30)) timeout = int(request.args.get('timeout', 30))
if timeout > 60: # Max 60 seconds if timeout > 60: # Max 60 seconds
timeout = 60 timeout = 60
# Create a queue for this client # Create an event for timeout handling
notification_queue = []
notification_received = threading.Event() notification_received = threading.Event()
def message_handler(message): # Check for pending notifications from the web dashboard
"""Handle incoming messages for this client""" if hasattr(g, 'main_app') and g.main_app and hasattr(g.main_app, 'web_dashboard'):
if message.type in [MessageType.START_GAME, MessageType.GAME_STARTED, MessageType.MATCH_START, MessageType.GAME_STATUS]: # Register this event with the web dashboard so it gets notified when notifications arrive
notification_data = { g.main_app.web_dashboard.register_waiting_client(notification_received)
"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)
# Unsubscribe from messages safely
if api_bp.message_bus:
try: try:
# Use proper unsubscribe methods instead of direct removal # First check for any pending notifications
for msg_type in [MessageType.START_GAME, MessageType.MATCH_START, MessageType.GAME_STATUS, MessageType.CUSTOM]: pending_notifications = g.main_app.web_dashboard.get_pending_notifications()
try: if pending_notifications:
if hasattr(api_bp.message_bus, '_global_handlers') and msg_type in api_bp.message_bus._global_handlers: # Return pending notifications immediately
handlers = api_bp.message_bus._global_handlers[msg_type] logger.debug(f"Returning {len(pending_notifications)} pending notifications to client")
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 = { response_data = {
"success": True, "success": True,
"notification": notification "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: else:
# Timeout - return empty response # Timeout - return empty response
logger.debug("Notification wait timed out, returning empty response")
response_data = { response_data = {
"success": True, "success": True,
"notification": None "notifications": []
} }
finally:
# Always unregister the event when done
g.main_app.web_dashboard.unregister_waiting_client(notification_received)
else:
# Web dashboard not available, return empty response
response_data = {
"success": True,
"notifications": []
}
# Response data is already prepared above
# Handle SSL/connection errors gracefully when sending response # Handle SSL/connection errors gracefully when sending response
try: try:
......
...@@ -10,9 +10,15 @@ window.Dashboard = (function() { ...@@ -10,9 +10,15 @@ window.Dashboard = (function() {
let statusInterval = null; let statusInterval = null;
let isOnline = navigator.onLine; let isOnline = navigator.onLine;
let cache = {}; let cache = {};
let initialized = false; // Prevent multiple initializations
// Initialize dashboard // Initialize dashboard
function init(userConfig) { function init(userConfig) {
if (initialized) {
console.log('Dashboard already initialized, skipping...');
return;
}
config = Object.assign({ config = Object.assign({
statusUpdateInterval: 5000, statusUpdateInterval: 5000,
apiEndpoint: '/api', apiEndpoint: '/api',
...@@ -36,6 +42,7 @@ window.Dashboard = (function() { ...@@ -36,6 +42,7 @@ window.Dashboard = (function() {
// Initialize match timer // Initialize match timer
initMatchTimer(); initMatchTimer();
initialized = true;
console.log('Dashboard initialized successfully'); console.log('Dashboard initialized successfully');
} }
...@@ -463,9 +470,11 @@ window.Dashboard = (function() { ...@@ -463,9 +470,11 @@ window.Dashboard = (function() {
let lastServerSync = 0; let lastServerSync = 0;
let cachedMatchInterval = null; // Cache the match interval configuration let cachedMatchInterval = null; // Cache the match interval configuration
const SYNC_INTERVAL = 30000; // Sync with server every 30 seconds 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() { 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 // Load match interval config once at initialization
loadMatchIntervalConfig().then(function(intervalSeconds) { loadMatchIntervalConfig().then(function(intervalSeconds) {
...@@ -474,8 +483,8 @@ window.Dashboard = (function() { ...@@ -474,8 +483,8 @@ window.Dashboard = (function() {
// Initial sync with server // Initial sync with server
syncWithServerTimer(); syncWithServerTimer();
// REMOVED: No periodic sync - rely on notifications // Start real-time notification polling for timer updates
// REMOVED: No local countdown - rely on server updates only startNotificationPolling();
}).catch(function(error) { }).catch(function(error) {
console.error('Failed to load match timer config at initialization:', error); console.error('Failed to load match timer config at initialization:', error);
...@@ -484,6 +493,9 @@ window.Dashboard = (function() { ...@@ -484,6 +493,9 @@ window.Dashboard = (function() {
// Initial sync with server // Initial sync with server
syncWithServerTimer(); syncWithServerTimer();
// Start real-time notification polling for timer updates
startNotificationPolling();
}); });
} }
...@@ -568,6 +580,17 @@ window.Dashboard = (function() { ...@@ -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() { function updateMatchTimerDisplay() {
let displaySeconds = serverTimerState.remaining_seconds; let displaySeconds = serverTimerState.remaining_seconds;
...@@ -661,6 +684,130 @@ window.Dashboard = (function() { ...@@ -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() { function onStartGameMessage() {
console.log('Received START_GAME message, initializing timer...'); console.log('Received START_GAME message, initializing timer...');
...@@ -675,7 +822,6 @@ window.Dashboard = (function() { ...@@ -675,7 +822,6 @@ window.Dashboard = (function() {
// Update display // Update display
updateMatchTimerDisplay(); updateMatchTimerDisplay();
startLocalCountdown();
showNotification('Games started - match timer is now running', 'success'); showNotification('Games started - match timer is now running', 'success');
}); });
...@@ -731,7 +877,8 @@ window.Dashboard = (function() { ...@@ -731,7 +877,8 @@ window.Dashboard = (function() {
startMatchTimer: startMatchTimer, startMatchTimer: startMatchTimer,
stopMatchTimer: stopMatchTimer, stopMatchTimer: stopMatchTimer,
resetMatchTimer: resetMatchTimer, resetMatchTimer: resetMatchTimer,
startNextMatch: startNextMatch startNextMatch: startNextMatch,
stopNotificationPolling: stopNotificationPolling
}; };
})(); })();
......
...@@ -1082,8 +1082,6 @@ function showNotification(message, type = 'info') { ...@@ -1082,8 +1082,6 @@ function showNotification(message, type = 'info') {
}, 3000); }, 3000);
} }
</script> </script>
<!-- Include the main dashboard.js for timer functionality -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
<script> <script>
// Initialize dashboard with timer functionality // Initialize dashboard with timer functionality
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
......
...@@ -738,8 +738,6 @@ function loadAvailableTemplates() { ...@@ -738,8 +738,6 @@ function loadAvailableTemplates() {
}); });
} }
</script> </script>
<!-- Include the main dashboard.js for timer functionality -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
<script> <script>
// Initialize dashboard with timer functionality // Initialize dashboard with timer functionality
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
......
...@@ -927,8 +927,6 @@ function showNotification(message, type = 'info') { ...@@ -927,8 +927,6 @@ function showNotification(message, type = 'info') {
}, 3000); }, 3000);
} }
</script> </script>
<!-- Include the main dashboard.js for timer functionality -->
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
<script> <script>
// Initialize dashboard with timer functionality // Initialize dashboard with timer functionality
document.addEventListener('DOMContentLoaded', function() { 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