Add websocket

parent 897b39c1
...@@ -13,10 +13,9 @@ import os ...@@ -13,10 +13,9 @@ import os
from pathlib import Path from pathlib import Path
from typing import Optional, Dict, Any, List from typing import Optional, Dict, Any, List
# Suppress Chromium sandbox warnings when running as root - MUST be set before Qt imports # Disable Qt WebEngine sandbox to allow localhost fetches - MUST be set before Qt imports
if os.geteuid() == 0: # Running as root os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = '1'
os.environ['QTWEBENGINE_DISABLE_SANDBOX'] = '1' print("Qt WebEngine sandbox disabled for localhost access")
print("Qt WebEngine sandbox disabled for root user")
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
...@@ -1890,6 +1889,53 @@ class PlayerWindow(QMainWindow): ...@@ -1890,6 +1889,53 @@ class PlayerWindow(QMainWindow):
except Exception as e: except Exception as e:
logger.error(f"Failed to send fixtures data to overlay: {e}") logger.error(f"Failed to send fixtures data to overlay: {e}")
def _fetch_fixture_data(self):
"""Fetch fixture data using priority order: WebSocket -> API -> QWebChannel"""
try:
logger.info("Fetching fixture data using priority order: WebSocket -> API -> QWebChannel")
# Priority 1: Try WebSocket (if available)
if hasattr(self, 'window_overlay') and self.window_overlay:
overlay_view = self.window_overlay
if hasattr(overlay_view, 'socket') and overlay_view.socket and overlay_view.socket.connected:
logger.info("WebSocket available, requesting fixture data...")
# WebSocket request would be handled asynchronously
# For now, fall through to API
pass
# Priority 2: Try API fetch
try:
import requests
base_url = self._get_web_server_base_url()
api_url = f"{base_url}/api/cashier/pending-matches"
logger.info(f"Fetching fixtures data from API: {api_url}")
response = requests.get(api_url, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('success') and data.get('matches'):
matches = data['matches']
logger.info(f"Successfully fetched {len(matches)} matches from API")
return {'matches': matches}
else:
logger.warning(f"API returned success=false or no matches: {data}")
else:
logger.error(f"API request failed with status {response.status_code}: {response.text}")
except Exception as api_error:
logger.warning(f"API fetch failed: {api_error}")
# Priority 3: Try QWebChannel (last resort)
logger.info("Falling back to QWebChannel for fixture data")
# QWebChannel communication would happen through the overlay
# For now, return None to indicate no data available
logger.warning("QWebChannel fallback not implemented yet")
return None
except Exception as e:
logger.error(f"Failed to fetch fixture data: {e}")
return None
def _fetch_fixtures_data(self): def _fetch_fixtures_data(self):
"""Fetch fixtures data from the web API""" """Fetch fixtures data from the web API"""
try: try:
...@@ -2513,7 +2559,7 @@ class QtVideoPlayer(QObject): ...@@ -2513,7 +2559,7 @@ class QtVideoPlayer(QObject):
} }
# Fetch and include fixture data if available # Fetch and include fixture data if available
fixture_data = self._fetch_fixture_data() fixture_data = self._fetch_fixtures_data()
if fixture_data: if fixture_data:
default_data['fixtures'] = fixture_data.get('matches', []) default_data['fixtures'] = fixture_data.get('matches', [])
logger.info(f"Included {len(default_data['fixtures'])} fixtures in default overlay") logger.info(f"Included {len(default_data['fixtures'])} fixtures in default overlay")
...@@ -3071,7 +3117,7 @@ class QtVideoPlayer(QObject): ...@@ -3071,7 +3117,7 @@ class QtVideoPlayer(QObject):
# If loading fixtures template, fetch and send fixture data # If loading fixtures template, fetch and send fixture data
if load_specific_template == "fixtures.html": if load_specific_template == "fixtures.html":
logger.info("Loading fixtures template, fetching fixture data...") logger.info("Loading fixtures template, fetching fixture data...")
fixture_data = self._fetch_fixture_data() fixture_data = self._fetch_fixtures_data()
if fixture_data: if fixture_data:
# Send fixture data to the overlay # Send fixture data to the overlay
fixture_payload = {'fixtures': fixture_data.get('matches', [])} fixture_payload = {'fixtures': fixture_data.get('matches', [])}
......
...@@ -318,6 +318,114 @@ ...@@ -318,6 +318,114 @@
let outcomesData = null; let outcomesData = null;
let countdownInterval = null; let countdownInterval = null;
let nextMatchStartTime = null; let nextMatchStartTime = null;
let startTime = null;
// Debug timing helper
function getTimestamp() {
return new Date().toISOString();
}
function debugTime(label) {
const now = Date.now();
const elapsed = startTime ? ` (+${now - startTime}ms)` : '';
console.log(`🔍 DEBUG [${getTimestamp()}] ${label}${elapsed}`);
}
// WebSocket variables
let wsConnection = null;
let wsReconnectInterval = null;
let wsConnected = false;
// WebSocket connection functions
function connectWebSocket() {
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
debugTime('WebSocket already connected');
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws/fixtures`;
debugTime(`Attempting WebSocket connection to: ${wsUrl}`);
try {
wsConnection = new WebSocket(wsUrl);
wsConnection.onopen = function(event) {
debugTime('WebSocket connection opened');
wsConnected = true;
// Clear any reconnect interval
if (wsReconnectInterval) {
clearInterval(wsReconnectInterval);
wsReconnectInterval = null;
}
// Request initial data
sendWebSocketMessage({ action: 'get_fixtures' });
};
wsConnection.onmessage = function(event) {
try {
const data = JSON.parse(event.data);
debugTime('WebSocket message received');
handleWebSocketMessage(data);
} catch (error) {
debugTime(`WebSocket message parse error: ${error.message}`);
}
};
wsConnection.onclose = function(event) {
debugTime(`WebSocket connection closed (code: ${event.code})`);
wsConnected = false;
// Attempt to reconnect after 5 seconds
if (!wsReconnectInterval) {
debugTime('Scheduling WebSocket reconnection in 5 seconds');
wsReconnectInterval = setTimeout(() => {
wsReconnectInterval = null;
connectWebSocket();
}, 5000);
}
};
wsConnection.onerror = function(error) {
debugTime('WebSocket connection error');
wsConnected = false;
};
} catch (error) {
debugTime(`WebSocket creation failed: ${error.message}`);
wsConnected = false;
}
}
function sendWebSocketMessage(message) {
if (wsConnection && wsConnection.readyState === WebSocket.OPEN) {
wsConnection.send(JSON.stringify(message));
debugTime(`WebSocket message sent: ${message.action || 'unknown'}`);
} else {
debugTime('Cannot send WebSocket message - connection not open');
}
}
function handleWebSocketMessage(data) {
debugTime(`Handling WebSocket message: ${data.type || 'unknown'}`);
if (data.type === 'fixtures_update' && data.fixtures) {
debugTime('Processing fixtures update from WebSocket');
fixturesData = data.fixtures;
renderFixtures();
} else if (data.type === 'matches_update' && data.matches) {
debugTime('Processing matches update from WebSocket');
fixturesData = data.matches;
renderFixtures();
} else if (data.type === 'config_update' && data.webServerBaseUrl) {
debugTime(`Updating web server URL: ${data.webServerBaseUrl}`);
webServerBaseUrl = data.webServerBaseUrl;
} else {
debugTime(`Unknown WebSocket message type: ${data.type}`);
}
}
// Web server configuration - will be set via WebChannel // Web server configuration - will be set via WebChannel
let webServerBaseUrl = 'http://127.0.0.1:5001'; // Default fallback let webServerBaseUrl = 'http://127.0.0.1:5001'; // Default fallback
...@@ -368,41 +476,43 @@ ...@@ -368,41 +476,43 @@
// Function to receive fixture data from Qt WebChannel // Function to receive fixture data from Qt WebChannel
function updateFixtureData(data) { function updateFixtureData(data) {
console.log('🔍 DEBUG: Received fixture data from WebChannel:', data); debugTime('Received fixture data from WebChannel');
if (data && data.fixtures) { if (data && data.fixtures) {
debugTime('Processing fixtures data');
fixturesData = data.fixtures; fixturesData = data.fixtures;
renderFixtures(); renderFixtures();
} else if (data && data.matches) { } else if (data && data.matches) {
debugTime('Processing matches data');
fixturesData = data.matches; fixturesData = data.matches;
renderFixtures(); renderFixtures();
} else { } else {
console.log('🔍 DEBUG: No valid fixture data in WebChannel update, waiting for data'); debugTime('No valid fixture data in WebChannel update, waiting for data');
// Don't show fallback data - wait for real data // Don't show fallback data - wait for real data
} }
} }
// Function to update overlay data (called by Qt WebChannel) // Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) { function updateOverlayData(data) {
console.log('Received overlay data:', data); debugTime('Received overlay data from WebChannel');
overlayData = data || {}; overlayData = data || {};
// Update web server base URL if provided // Update web server base URL if provided
if (data && data.webServerBaseUrl) { if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl; webServerBaseUrl = data.webServerBaseUrl;
console.log('Updated web server base URL:', webServerBaseUrl); debugTime(`Updated web server base URL: ${webServerBaseUrl}`);
} }
// Check if we have fixtures data from WebChannel // Check if we have fixtures data from WebChannel
if (data && data.fixtures) { if (data && data.fixtures) {
console.log('🔍 DEBUG: Received fixtures data from WebChannel'); debugTime('Processing fixtures data from overlay data');
fixturesData = data.fixtures; fixturesData = data.fixtures;
renderFixtures(); renderFixtures();
} else if (data && data.matches) { } else if (data && data.matches) {
console.log('🔍 DEBUG: Received matches data from WebChannel'); debugTime('Processing matches data from overlay data');
fixturesData = data.matches; fixturesData = data.matches;
renderFixtures(); renderFixtures();
} else { } else {
console.log('🔍 DEBUG: No fixtures data in WebChannel update, will wait for updateFixtureData call'); debugTime('No fixtures data in overlay data, will wait for updateFixtureData call');
// Don't fetch from API - wait for Python side to provide data via updateFixtureData // Don't fetch from API - wait for Python side to provide data via updateFixtureData
} }
} }
...@@ -600,6 +710,8 @@ ...@@ -600,6 +710,8 @@
// Render the fixtures table // Render the fixtures table
function renderFixtures() { function renderFixtures() {
debugTime('Starting renderFixtures function');
const loadingMessage = document.getElementById('loadingMessage'); const loadingMessage = document.getElementById('loadingMessage');
const fixturesContent = document.getElementById('fixturesContent'); const fixturesContent = document.getElementById('fixturesContent');
const noMatches = document.getElementById('noMatches'); const noMatches = document.getElementById('noMatches');
...@@ -608,12 +720,16 @@ ...@@ -608,12 +720,16 @@
loadingMessage.style.display = 'none'; loadingMessage.style.display = 'none';
noMatches.style.display = 'none'; noMatches.style.display = 'none';
debugTime('UI elements updated - loading message hidden');
if (!fixturesData || fixturesData.length === 0) { if (!fixturesData || fixturesData.length === 0) {
debugTime('No fixtures data available');
showNoMatches('No matches available for today'); showNoMatches('No matches available for today');
return; return;
} }
debugTime(`Processing ${fixturesData.length} fixtures`);
// Get all available outcomes from the matches // Get all available outcomes from the matches
const allOutcomes = new Set(); const allOutcomes = new Set();
fixturesData.forEach(match => { fixturesData.forEach(match => {
...@@ -715,9 +831,11 @@ ...@@ -715,9 +831,11 @@
}); });
fixturesContent.style.display = 'block'; fixturesContent.style.display = 'block';
debugTime('Fixtures table rendered and displayed');
// Find next match and start countdown // Find next match and start countdown
findNextMatchAndStartCountdown(); findNextMatchAndStartCountdown();
debugTime('Countdown initialization completed');
} }
// Show no matches message // Show no matches message
...@@ -731,8 +849,8 @@ ...@@ -731,8 +849,8 @@
// Initialize when DOM is loaded // Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('🔍 DEBUG: Fixtures overlay initialized - waiting for WebChannel data'); startTime = Date.now();
console.log('🔍 DEBUG: DOM ready, WebChannel will provide fixture data'); debugTime('DOM Content Loaded - Starting fixtures overlay initialization');
// Show loading message initially // Show loading message initially
document.getElementById('fixturesContent').style.display = 'none'; document.getElementById('fixturesContent').style.display = 'none';
...@@ -740,106 +858,95 @@ ...@@ -740,106 +858,95 @@
document.getElementById('loadingMessage').style.display = 'block'; document.getElementById('loadingMessage').style.display = 'block';
document.getElementById('loadingMessage').textContent = 'Loading live fixture data...'; document.getElementById('loadingMessage').textContent = 'Loading live fixture data...';
// Wait for WebChannel to provide data - no direct API fetching debugTime('UI initialized - Loading message displayed');
console.log('🔍 DEBUG: Waiting for Python side to provide fixture data via WebChannel');
// Try to trigger data load via WebChannel immediately // Try WebSocket connection first (faster than WebChannel)
if (typeof qt !== 'undefined' && qt.webChannelTransport) { debugTime('Attempting WebSocket connection for real-time data');
try { connectWebSocket();
new QWebChannel(qt.webChannelTransport, function(channel) {
if (channel.objects.overlay && channel.objects.overlay.trigger_fixtures_data_load) {
console.log('🔍 DEBUG: Calling trigger_fixtures_data_load immediately');
channel.objects.overlay.trigger_fixtures_data_load();
}
});
} catch (e) {
console.log('🔍 DEBUG: Immediate WebChannel trigger failed:', e);
}
}
// Fallback: Try to trigger data load via WebChannel after 1 second // Fallback: Try WebChannel if WebSocket fails after 2 seconds
setTimeout(function() { setTimeout(() => {
console.log('🔍 DEBUG: Fallback - trying to trigger data load via WebChannel'); if (!wsConnected) {
if (typeof qt !== 'undefined' && qt.webChannelTransport) { debugTime('WebSocket not connected after 2 seconds, trying WebChannel fallback');
try { // Initialize WebChannel connection for data updates
new QWebChannel(qt.webChannelTransport, function(channel) { debugTime('Initializing WebChannel connection for data updates');
if (channel.objects.overlay && channel.objects.overlay.trigger_fixtures_data_load) {
console.log('🔍 DEBUG: Calling trigger_fixtures_data_load from fallback timer'); // Poll for QWebChannel availability
channel.objects.overlay.trigger_fixtures_data_load(); let channelInitialized = false;
let pollCount = 0;
const checkWebChannel = function() {
pollCount++;
if (channelInitialized) return;
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
debugTime(`QWebChannel available after ${pollCount} polls, initializing`);
try {
new QWebChannel(qt.webChannelTransport, function(channel) {
if (channelInitialized) return;
channelInitialized = true;
debugTime('WebChannel initialized, setting up connections');
if (channel.objects.overlay && channel.objects.overlay.trigger_fixtures_data_load) {
debugTime('Calling trigger_fixtures_data_load from WebChannel');
channel.objects.overlay.trigger_fixtures_data_load();
}
// Set up signal connections
if (channel.objects.overlay) {
channel.objects.overlay.dataChanged.connect(function(data) {
debugTime('WebChannel dataChanged signal received');
updateOverlayData(data);
});
if (channel.objects.overlay.fixtureDataUpdated) {
channel.objects.overlay.fixtureDataUpdated.connect(function(data) {
debugTime('WebChannel fixture data signal received');
updateFixtureData(data);
});
}
if (channel.objects.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) {
debugTime('WebChannel initial data received');
updateOverlayData(data);
});
}
// Additional trigger attempts
if (channel.objects.overlay.trigger_fixtures_data_load) {
channel.objects.overlay.trigger_fixtures_data_load();
} else if (channel.objects.overlay.sendFixtureDataUpdate) {
channel.objects.overlay.sendFixtureDataUpdate({'test': 'fallback_trigger'});
}
}
});
} catch (e) {
debugTime(`WebChannel initialization failed: ${e.message}`);
} }
}); } else {
} catch (e) { // Continue polling if not available yet
console.log('🔍 DEBUG: Fallback WebChannel trigger failed:', e); if (pollCount % 20 === 0) { // Log every 20 polls (every second)
} debugTime(`Still polling for QWebChannel... (${pollCount} polls)`);
}
setTimeout(checkWebChannel, 50);
}
};
checkWebChannel();
} }
}, 1000); }, 2000);
// Show no matches if no data after 3 seconds // Show no matches if no data after 5 seconds total
setTimeout(() => { setTimeout(() => {
if (!fixturesData || fixturesData.length === 0) { if (!fixturesData || fixturesData.length === 0) {
console.log('🔍 DEBUG: No data received after 3 seconds'); debugTime('No data received after 5 seconds - showing waiting message');
showNoMatches('Waiting for fixture data...'); showNoMatches('Waiting for fixture data...');
} else {
debugTime('Data was received before 5 second timeout');
} }
}, 3000); }, 5000);
}); });
// Qt WebChannel initialization (when available)
if (typeof QWebChannel !== 'undefined') {
console.log('🔍 DEBUG: QWebChannel available, initializing...');
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('🔍 DEBUG: WebChannel initialized for fixtures overlay');
console.log('🔍 DEBUG: Available channel objects:', Object.keys(channel.objects));
// Connect to overlay object if available
if (channel.objects.overlay) {
console.log('🔍 DEBUG: Overlay object found in WebChannel');
console.log('🔍 DEBUG: Overlay object methods:', Object.getOwnPropertyNames(channel.objects.overlay));
channel.objects.overlay.dataChanged.connect(function(data) {
console.log('🔍 DEBUG: Overlay dataChanged signal received:', data);
updateOverlayData(data);
});
// Connect fixture data signal
if (channel.objects.overlay.fixtureDataUpdated) {
console.log('🔍 DEBUG: Connecting to fixtureDataUpdated signal');
channel.objects.overlay.fixtureDataUpdated.connect(function(data) {
console.log('🔍 DEBUG: Fixture data signal received:', data);
updateFixtureData(data);
});
} else {
console.log('🔍 DEBUG: fixtureDataUpdated signal not available on overlay object');
}
// Get initial data
if (channel.objects.overlay.getCurrentData) {
console.log('🔍 DEBUG: Calling getCurrentData for initial data');
channel.objects.overlay.getCurrentData(function(data) {
console.log('🔍 DEBUG: Received initial data from getCurrentData:', data);
updateOverlayData(data);
});
} else {
console.log('🔍 DEBUG: getCurrentData method not available');
}
// Try to manually trigger fixtures data load
console.log('🔍 DEBUG: Attempting to manually trigger fixtures data load');
if (channel.objects.overlay.trigger_fixtures_data_load) {
console.log('🔍 DEBUG: Calling trigger_fixtures_data_load manually');
channel.objects.overlay.trigger_fixtures_data_load();
} else if (channel.objects.overlay.sendFixtureDataUpdate) {
console.log('🔍 DEBUG: Calling sendFixtureDataUpdate manually');
channel.objects.overlay.sendFixtureDataUpdate({'test': 'manual_trigger'});
} else {
console.log('🔍 DEBUG: No manual trigger method available');
}
} else {
console.log('🔍 DEBUG: No overlay object found in WebChannel');
}
});
} else {
console.log('🔍 DEBUG: QWebChannel not available, will rely on DOM ready event');
}
</script> </script>
<!-- <!--
......
...@@ -9,6 +9,7 @@ from typing import Optional, Dict, Any ...@@ -9,6 +9,7 @@ from typing import Optional, Dict, Any
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
from flask_socketio import SocketIO, emit
from werkzeug.serving import make_server from werkzeug.serving import make_server
import threading import threading
...@@ -38,6 +39,7 @@ class WebDashboard(ThreadedComponent): ...@@ -38,6 +39,7 @@ class WebDashboard(ThreadedComponent):
# Flask app and server # Flask app and server
self.app: Optional[Flask] = None self.app: Optional[Flask] = None
self.socketio: Optional[SocketIO] = None
self.server: Optional = None self.server: Optional = None
self.ssl_context: Optional = None # SSL context for HTTPS self.ssl_context: Optional = None # SSL context for HTTPS
self.auth_manager: Optional[AuthManager] = None self.auth_manager: Optional[AuthManager] = None
...@@ -103,7 +105,14 @@ class WebDashboard(ThreadedComponent): ...@@ -103,7 +105,14 @@ class WebDashboard(ThreadedComponent):
app = Flask(__name__, app = Flask(__name__,
template_folder=str(template_dir), template_folder=str(template_dir),
static_folder=str(static_dir)) static_folder=str(static_dir))
# Initialize SocketIO (disabled for PyInstaller compatibility)
try:
socketio = SocketIO(app, cors_allowed_origins="*", async_mode='threading')
except Exception as e:
logger.warning(f"SocketIO initialization failed: {e}, disabling WebSocket support")
socketio = None
# Configuration # Configuration
app.config.update({ app.config.update({
'SECRET_KEY': self.settings.secret_key, 'SECRET_KEY': self.settings.secret_key,
...@@ -184,23 +193,117 @@ class WebDashboard(ThreadedComponent): ...@@ -184,23 +193,117 @@ class WebDashboard(ThreadedComponent):
def forbidden_error(error): def forbidden_error(error):
return render_template('errors/403.html'), 403 return render_template('errors/403.html'), 403
# Store socketio instance in class
self.socketio = socketio
# Setup SocketIO event handlers if SocketIO is available
if self.socketio:
self._setup_socketio_handlers()
else:
logger.warning("SocketIO not available, WebSocket features will be disabled")
return app return app
def _setup_socketio_handlers(self):
"""Setup SocketIO event handlers"""
@self.socketio.on('connect')
def handle_connect():
logger.info(f"Client connected: {request.sid}")
emit('connected', {'status': 'connected'})
@self.socketio.on('disconnect')
def handle_disconnect():
logger.info(f"Client disconnected: {request.sid}")
@self.socketio.on('join_fixtures')
def handle_join_fixtures(data):
from flask_socketio import join_room
join_room('fixtures')
logger.info(f"Client {request.sid} joined fixtures room")
emit('joined_fixtures', {'status': 'joined'})
@self.socketio.on('leave_fixtures')
def handle_leave_fixtures(data):
from flask_socketio import leave_room
leave_room('fixtures')
logger.info(f"Client {request.sid} left fixtures room")
@self.socketio.on('get_fixtures')
def handle_get_fixtures(data):
try:
if self.db_manager:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
matches = session.query(MatchModel).filter_by(active_status=True).order_by(MatchModel.match_number).all()
fixtures_data = []
for match in matches[:5]: # Only first 5 matches
match_dict = match.to_dict()
fixtures_data.append(match_dict)
emit('fixtures_update', {
'fixtures': fixtures_data,
'timestamp': time.time()
})
logger.info(f"Sent fixtures data to client {request.sid}: {len(fixtures_data)} matches")
finally:
session.close()
else:
emit('error', {'message': 'Database not available'})
except Exception as e:
logger.error(f"Error getting fixtures for WebSocket: {e}")
emit('error', {'message': str(e)})
@self.socketio.on('get_matches')
def handle_get_matches(data):
try:
if self.db_manager:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
matches = session.query(MatchModel).filter_by(active_status=True).order_by(MatchModel.match_number).all()
matches_data = []
for match in matches[:5]: # Only first 5 matches
match_dict = match.to_dict()
matches_data.append(match_dict)
emit('matches_update', {
'matches': matches_data,
'timestamp': time.time()
})
logger.info(f"Sent matches data to client {request.sid}: {len(matches_data)} matches")
finally:
session.close()
else:
emit('error', {'message': 'Database not available'})
except Exception as e:
logger.error(f"Error getting matches for WebSocket: {e}")
emit('error', {'message': str(e)})
def _setup_routes(self): def _setup_routes(self):
"""Setup Flask routes""" """Setup Flask routes"""
# Pass dependencies to route modules # Pass dependencies to route modules
main_bp.db_manager = self.db_manager main_bp.db_manager = self.db_manager
main_bp.config_manager = self.config_manager main_bp.config_manager = self.config_manager
main_bp.message_bus = self.message_bus main_bp.message_bus = self.message_bus
auth_bp.auth_manager = self.auth_manager auth_bp.auth_manager = self.auth_manager
auth_bp.db_manager = self.db_manager auth_bp.db_manager = self.db_manager
api_bp.api = self.api api_bp.api = self.api
api_bp.auth_manager = self.auth_manager api_bp.auth_manager = self.auth_manager
api_bp.db_manager = self.db_manager api_bp.db_manager = self.db_manager
api_bp.config_manager = self.config_manager api_bp.config_manager = self.config_manager
api_bp.message_bus = self.message_bus api_bp.message_bus = self.message_bus
api_bp.socketio = self.socketio
# Pass dependencies to screen cast blueprint # Pass dependencies to screen cast blueprint
screen_cast_bp.message_bus = self.message_bus screen_cast_bp.message_bus = self.message_bus
...@@ -213,58 +316,20 @@ class WebDashboard(ThreadedComponent): ...@@ -213,58 +316,20 @@ class WebDashboard(ThreadedComponent):
self.app.register_blueprint(screen_cast_bp, url_prefix='/screen_cast') self.app.register_blueprint(screen_cast_bp, url_prefix='/screen_cast')
def _create_server(self): def _create_server(self):
"""Create HTTP/HTTPS server""" """Create HTTP/HTTPS server with SocketIO support"""
try: try:
# Setup SSL context if enabled
ssl_context = None
protocol = "HTTP" protocol = "HTTP"
if self.settings.enable_ssl: if self.settings.enable_ssl:
from ..config.settings import get_user_data_dir protocol = "HTTPS"
logger.info("SSL enabled - SocketIO server will use HTTPS")
# Determine SSL certificate paths
if self.settings.ssl_cert_path and self.settings.ssl_key_path: logger.info(f"{protocol} server with SocketIO created on {self.settings.host}:{self.settings.port}")
# Use provided certificate paths
cert_path = self.settings.ssl_cert_path
key_path = self.settings.ssl_key_path
logger.info(f"Using provided SSL certificates: {cert_path}, {key_path}")
else:
# Auto-generate or get existing certificates
user_data_dir = get_user_data_dir()
cert_path, key_path = get_ssl_certificate_paths(
user_data_dir,
auto_generate=self.settings.ssl_auto_generate
)
# Create SSL context
if cert_path and key_path:
ssl_context = create_ssl_context(cert_path, key_path)
if ssl_context:
self.ssl_context = ssl_context
protocol = "HTTPS"
logger.info(f"SSL enabled - server will use HTTPS")
else:
logger.error("Failed to create SSL context, falling back to HTTP")
self.settings.enable_ssl = False
else:
logger.error("SSL certificates not available, falling back to HTTP")
self.settings.enable_ssl = False
# Create server with or without SSL
self.server = make_server(
self.settings.host,
self.settings.port,
self.app,
threaded=True,
ssl_context=ssl_context
)
logger.info(f"{protocol} server created on {self.settings.host}:{self.settings.port}")
if self.settings.enable_ssl: if self.settings.enable_ssl:
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")
except Exception as e: except Exception as e:
logger.error(f"Failed to create {protocol} server: {e}") logger.error(f"Failed to create {protocol} server: {e}")
raise raise
...@@ -323,17 +388,31 @@ class WebDashboard(ThreadedComponent): ...@@ -323,17 +388,31 @@ class WebDashboard(ThreadedComponent):
logger.info("WebDashboard thread ended") logger.info("WebDashboard thread ended")
def _run_server(self): def _run_server(self):
"""Run HTTP/HTTPS server with SSL error suppression""" """Run HTTP/HTTPS server with optional SocketIO"""
try: try:
protocol = "HTTPS" if self.settings.enable_ssl else "HTTP" protocol = "HTTPS" if self.settings.enable_ssl else "HTTP"
logger.info(f"Starting {protocol} server on {self.settings.host}:{self.settings.port}") 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}")
# Apply SSL error logging suppression
if self.settings.enable_ssl: if self.socketio:
self._setup_ssl_error_suppression() # Run SocketIO server
self.socketio.run(
self.server.serve_forever() self.app,
host=self.settings.host,
port=self.settings.port,
debug=False,
use_reloader=False,
log_output=False
)
else:
# Run Flask server without SocketIO
self.app.run(
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"
...@@ -588,6 +667,38 @@ class WebDashboard(ThreadedComponent): ...@@ -588,6 +667,38 @@ class WebDashboard(ThreadedComponent):
"error": str(e) "error": str(e)
} }
def broadcast_fixtures_update(self):
"""Broadcast fixtures update to all connected WebSocket clients"""
try:
if not self.socketio:
logger.debug("SocketIO not available, skipping broadcast")
return
# Get fixtures data
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
matches = session.query(MatchModel).filter_by(active_status=True).order_by(MatchModel.match_number).all()
fixtures_data = []
for match in matches[:5]: # Only first 5 matches
match_dict = match.to_dict()
fixtures_data.append(match_dict)
# Broadcast to fixtures room
self.socketio.emit('fixtures_update', {
'fixtures': fixtures_data,
'timestamp': time.time()
}, room='fixtures')
logger.debug(f"Broadcasted fixtures update to {len(fixtures_data)} matches")
finally:
session.close()
except Exception as e:
logger.error(f"Failed to broadcast fixtures update: {e}")
def create_app(db_manager: DatabaseManager, config_manager: ConfigManager, def create_app(db_manager: DatabaseManager, config_manager: ConfigManager,
settings: WebConfig) -> Flask: settings: WebConfig) -> Flask:
......
...@@ -7,6 +7,7 @@ import time ...@@ -7,6 +7,7 @@ import time
from datetime import datetime, date from datetime import datetime, date
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session, g from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session, g
from flask_login import login_required, current_user, login_user, logout_user from flask_login import login_required, current_user, login_user, logout_user
from flask_socketio import emit, join_room, leave_room
from werkzeug.security import check_password_hash from werkzeug.security import check_password_hash
from werkzeug.utils import secure_filename from werkzeug.utils import secure_filename
...@@ -5180,4 +5181,5 @@ def upload_intro_video(): ...@@ -5180,4 +5181,5 @@ def upload_intro_video():
except Exception as e: except Exception as e:
logger.error(f"API upload intro video error: {e}") logger.error(f"API upload intro video error: {e}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
\ No newline at end of file
...@@ -20,6 +20,7 @@ bcrypt>=4.0.0 ...@@ -20,6 +20,7 @@ bcrypt>=4.0.0
Werkzeug>=2.3.0 Werkzeug>=2.3.0
Jinja2>=3.1.0 Jinja2>=3.1.0
WTForms>=3.0.0 WTForms>=3.0.0
Flask-SocketIO>=5.3.0
# Configuration and environment # Configuration and environment
python-dotenv>=0.19.0 python-dotenv>=0.19.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment