feat: Add automated game timer with --start-timer command line switch

- Add --start-timer command line argument with configurable delay (default 4 minutes)
- Implement MatchTimerComponent integration with proper initialization and registration
- Add timer state API endpoints for web dashboard integration
- Enhance message bus communication between timer component and web dashboard
- Update documentation and changelog with comprehensive timer feature details
- Fix timer component not being initialized during application startup
- Add timer state caching and real-time API endpoint integration

The timer automatically sends START_GAME messages after the specified delay,
enabling automated match progression without manual intervention.
parent 17a5dc02
...@@ -2,6 +2,33 @@ ...@@ -2,6 +2,33 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.2.6] - 2025-08-26
### Added
- **Automated Game Timer**: New `--start-timer` command line switch that sends START_GAME message after specified delay (default 4 minutes)
- **MatchTimerComponent Integration**: Fixed timer component initialization and registration with thread manager
- **Timer State API**: Real-time timer state API endpoints for web dashboard integration
- **Message Bus Communication**: Proper inter-thread communication between timer component and web dashboard
- **Timer Configuration**: Configurable timer delay with command-line parameter support
### Enhanced
- **Application Architecture**: Enhanced timer system with proper component lifecycle management
- **Web Dashboard Integration**: Timer state caching and API endpoint integration
- **Command Line Interface**: Extended CLI with timer configuration options
- **Documentation**: Comprehensive timer feature documentation and usage examples
### Fixed
- **MatchTimerComponent Registration**: Fixed timer component not being initialized during application startup
- **Timer State Communication**: Resolved web dashboard timer state requests not reaching timer component
- **API Endpoint Integration**: Fixed timer state API endpoints returning placeholder data
### Technical Details
- **Timer Architecture**: Implemented threading.Timer with proper daemon thread management
- **Message Bus Integration**: Added CUSTOM message type handling for timer state requests
- **Settings Integration**: Added TimerConfig dataclass with database persistence support
- **Web Dashboard Enhancement**: Added timer state caching and message handler for timer responses
- **Cross-Platform Compatibility**: Timer system works consistently across Windows, Linux, and macOS
## [1.2.5] - 2025-08-26 ## [1.2.5] - 2025-08-26
### Added ### Added
......
...@@ -196,6 +196,12 @@ FLASK_DEBUG=False ...@@ -196,6 +196,12 @@ FLASK_DEBUG=False
# Basic usage (screen casting enabled by default) # Basic usage (screen casting enabled by default)
python main.py python main.py
# Start with automated game timer (4 minutes default)
python main.py --start-timer
# Start with custom timer delay (2 minutes)
python main.py --start-timer 2
# Available options # Available options
python main.py [OPTIONS] python main.py [OPTIONS]
...@@ -209,6 +215,7 @@ Options: ...@@ -209,6 +215,7 @@ Options:
--no-web Disable web dashboard --no-web Disable web dashboard
--no-api Disable API client --no-api Disable API client
--no-screen-cast Disable screen casting functionality --no-screen-cast Disable screen casting functionality
--start-timer INTEGER Start timer in minutes to send START_GAME message [default: 4]
--database-path TEXT Custom database path --database-path TEXT Custom database path
--log-level TEXT Logging level [default: INFO] --log-level TEXT Logging level [default: INFO]
--config-dir TEXT Custom configuration directory --config-dir TEXT Custom configuration directory
...@@ -684,7 +691,7 @@ Content-Type: application/json ...@@ -684,7 +691,7 @@ Content-Type: application/json
## Match Timer System ## Match Timer System
The application includes a comprehensive match timer system with automatic match progression and visual countdown displays. The application includes a comprehensive match timer system with automatic match progression, visual countdown displays, and command-line timer configuration.
### Match Timer Features ### Match Timer Features
...@@ -770,6 +777,59 @@ Timer appearance is controlled via CSS variables and classes: ...@@ -770,6 +777,59 @@ Timer appearance is controlled via CSS variables and classes:
} }
``` ```
### Command-Line Timer Configuration
The application supports automated game start via command-line timer configuration:
#### --start-timer Option
```bash
# Start with default 4-minute timer
python main.py --start-timer
# Start with custom timer delay (in minutes)
python main.py --start-timer 2
# Start with 30-second timer for testing
python main.py --start-timer 0.5
```
#### Timer Behavior
- **Automatic START_GAME**: When the timer expires, a START_GAME message is automatically sent to the message bus
- **MatchTimerComponent Integration**: The command-line timer works seamlessly with the existing MatchTimerComponent
- **Web Interface Display**: Timer state is available through the web dashboard API endpoints
- **Configurable Delay**: Timer delay can be set from seconds to hours (though practical use is typically minutes)
#### Timer Configuration Architecture
The command-line timer integrates with the existing timer system:
1. **Settings Integration**: Timer configuration is stored in `AppSettings.timer`
2. **Application Startup**: Timer is initialized during application startup if `--start-timer` is specified
3. **Message Bus Communication**: Timer expiration sends START_GAME message to trigger match progression
4. **Web Dashboard Integration**: Timer state is cached and served through API endpoints
#### Example Usage Scenarios
**Development Testing**:
```bash
# Quick 30-second timer for testing timer functionality
python main.py --start-timer 0.5 --no-qt
```
**Production Deployment**:
```bash
# 4-minute timer for match intervals
python main.py --start-timer 4 --web-port 8080
```
**Custom Timing**:
```bash
# 10-minute intervals for longer matches
python main.py --start-timer 10
```
### Match Timer API ### Match Timer API
#### Get Timer Configuration #### Get Timer Configuration
......
...@@ -20,10 +20,19 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -20,10 +20,19 @@ A cross-platform multimedia client application with video playback, web dashboar
- **Configuration Management**: Web-based configuration with section-based updates - **Configuration Management**: Web-based configuration with section-based updates
- **Remote Shutdown**: Admin-only application shutdown from web dashboard - **Remote Shutdown**: Admin-only application shutdown from web dashboard
- **Overlay System**: Command-line switchable between WebEngine and native Qt overlay rendering - **Overlay System**: Command-line switchable between WebEngine and native Qt overlay rendering
- **Automated Game Timer**: Configurable timer system that sends START_GAME messages after specified delay
## Recent Improvements ## Recent Improvements
### Version 1.2.4 (August 2025) ### Version 1.2.6 (August 2025)
-**Automated Game Timer**: New `--start-timer` command line switch that sends START_GAME message after specified delay (default 4 minutes)
-**MatchTimerComponent Integration**: Fixed timer component initialization and registration with thread manager
-**Timer State API**: Real-time timer state API endpoints for web dashboard integration
-**Message Bus Communication**: Proper inter-thread communication between timer component and web dashboard
-**Timer Configuration**: Configurable timer delay with command-line parameter support
### Version 1.2.5 (August 2025)
-**Screen Casting Integration**: Complete screen capture and Chromecast streaming functionality integrated into MBetterC application -**Screen Casting Integration**: Complete screen capture and Chromecast streaming functionality integrated into MBetterC application
-**ScreenCastComponent**: New threaded component with HTTP server, FFmpeg screen capture, and Chromecast device management -**ScreenCastComponent**: New threaded component with HTTP server, FFmpeg screen capture, and Chromecast device management
...@@ -153,6 +162,12 @@ python main.py --overlay-type native ...@@ -153,6 +162,12 @@ python main.py --overlay-type native
# Use WebEngine overlays (default) # Use WebEngine overlays (default)
python main.py --overlay-type webengine python main.py --overlay-type webengine
# Start with automated game timer (4 minutes default)
python main.py --start-timer
# Start with custom timer delay (2 minutes)
python main.py --start-timer 2
# Show help # Show help
python main.py --help python main.py --help
``` ```
...@@ -275,6 +290,11 @@ Threads communicate via Python Queues with structured messages: ...@@ -275,6 +290,11 @@ Threads communicate via Python Queues with structured messages:
- `POST /api/extraction/config` - Update game configuration settings - `POST /api/extraction/config` - Update game configuration settings
- `GET /api/server-time` - Get current server time for clock synchronization - `GET /api/server-time` - Get current server time for clock synchronization
#### Timer Management
- `GET /api/match-timer/state` - Get current timer state (running, remaining time, etc.)
- `POST /api/match-timer/control` - Control timer (start/stop) with action parameter
- `GET /api/match-timer/config` - Get timer configuration settings
### Message Types ### Message Types
#### Video Control #### Video Control
...@@ -298,6 +318,11 @@ Threads communicate via Python Queues with structured messages: ...@@ -298,6 +318,11 @@ Threads communicate via Python Queues with structured messages:
- `SYSTEM_STATUS` - Component status update - `SYSTEM_STATUS` - Component status update
- `LOG_ENTRY` - Log entry for database storage - `LOG_ENTRY` - Log entry for database storage
#### Timer Messages
- `START_GAME` - Start automated game timer with default interval
- `START_GAMES` - Start games with configuration-based interval
- `MATCH_START` - Start specific match with fixture and match IDs
#### Screen Cast Messages #### Screen Cast Messages
- `WEB_ACTION` - Web dashboard screen cast control actions - `WEB_ACTION` - Web dashboard screen cast control actions
- `SCREEN_CAST_START` - Start screen capture and streaming - `SCREEN_CAST_START` - Start screen capture and streaming
......
...@@ -173,6 +173,14 @@ Examples: ...@@ -173,6 +173,14 @@ Examples:
version='MbetterClient 1.0.0' version='MbetterClient 1.0.0'
) )
# Timer options
parser.add_argument(
'--start-timer',
type=int,
default=None,
help='Configure timer delay in minutes for START_GAME_DELAYED message when START_GAME is received (default: 4 minutes)'
)
return parser.parse_args() return parser.parse_args()
def validate_arguments(args): def validate_arguments(args):
...@@ -218,6 +226,13 @@ def main(): ...@@ -218,6 +226,13 @@ def main():
settings.enable_web = not args.no_web settings.enable_web = not args.no_web
settings.qt.use_native_overlay = args.native_overlay settings.qt.use_native_overlay = args.native_overlay
# Timer settings
if args.start_timer is not None:
settings.timer.enabled = True
settings.timer.delay_minutes = args.start_timer
else:
settings.timer.enabled = False
# Screen cast settings # Screen cast settings
# Screen cast is enabled by default, disable only if --no-screen-cast is specified # Screen cast is enabled by default, disable only if --no-screen-cast is specified
settings.enable_screen_cast = not args.no_screen_cast settings.enable_screen_cast = not args.no_screen_cast
......
...@@ -39,10 +39,15 @@ class MbetterClientApplication: ...@@ -39,10 +39,15 @@ class MbetterClientApplication:
self.api_client = None self.api_client = None
self.template_watcher = None self.template_watcher = None
self.screen_cast = None self.screen_cast = None
self.games_thread = None
self.match_timer = None
# Main loop thread # Main loop thread
self._main_loop_thread: Optional[threading.Thread] = None self._main_loop_thread: Optional[threading.Thread] = None
# Timer for automated game start
self._game_start_timer: Optional[threading.Timer] = None
logger.info("MbetterClient application initialized") logger.info("MbetterClient application initialized")
def initialize(self) -> bool: def initialize(self) -> bool:
...@@ -150,6 +155,7 @@ class MbetterClientApplication: ...@@ -150,6 +155,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.START_GAME, self._handle_start_game_message)
logger.info("Message bus initialized") logger.info("Message bus initialized")
return True return True
...@@ -220,6 +226,20 @@ class MbetterClientApplication: ...@@ -220,6 +226,20 @@ class MbetterClientApplication:
else: else:
logger.info("Screen cast component disabled") logger.info("Screen cast component disabled")
# Initialize games thread
if self._initialize_games_thread():
components_initialized += 1
else:
logger.error("Games thread initialization failed")
return False
# Initialize match timer
if self._initialize_match_timer():
components_initialized += 1
else:
logger.error("Match timer initialization failed")
return False
if components_initialized == 0: if components_initialized == 0:
logger.error("No components were initialized") logger.error("No components were initialized")
return False return False
...@@ -368,6 +388,48 @@ class MbetterClientApplication: ...@@ -368,6 +388,48 @@ class MbetterClientApplication:
logger.error(f"Screen cast initialization failed: {e}") logger.error(f"Screen cast initialization failed: {e}")
return False return False
def _initialize_games_thread(self) -> bool:
"""Initialize games thread component"""
try:
from .games_thread import GamesThread
self.games_thread = GamesThread(
name="games_thread",
message_bus=self.message_bus,
db_manager=self.db_manager
)
# Register with thread manager
self.thread_manager.register_component("games_thread", self.games_thread)
logger.info("Games thread component initialized")
return True
except Exception as e:
logger.error(f"Games thread initialization failed: {e}")
return False
def _initialize_match_timer(self) -> bool:
"""Initialize match timer component"""
try:
from .match_timer import MatchTimerComponent
self.match_timer = MatchTimerComponent(
message_bus=self.message_bus,
db_manager=self.db_manager,
config_manager=self.config_manager
)
# Register with thread manager
self.thread_manager.register_component("match_timer", self.match_timer)
logger.info("Match timer component initialized")
return True
except Exception as e:
logger.error(f"Match timer initialization failed: {e}")
return False
def run(self) -> int: def run(self) -> int:
"""Run the application""" """Run the application"""
try: try:
...@@ -624,12 +686,66 @@ class MbetterClientApplication: ...@@ -624,12 +686,66 @@ class MbetterClientApplication:
except Exception as e: except Exception as e:
logger.error(f"Failed to handle log entry: {e}") logger.error(f"Failed to handle log entry: {e}")
def _handle_start_game_message(self, message: Message):
"""Handle START_GAME message by starting the timer"""
try:
if not self.settings.timer.enabled:
logger.debug("Timer not enabled, ignoring START_GAME message")
return
# Cancel any existing timer
self._cancel_game_timer()
# Start new timer
delay_seconds = self.settings.timer.delay_minutes * 60
logger.info(f"START_GAME message received from {message.sender}, starting timer for {self.settings.timer.delay_minutes} minutes ({delay_seconds} seconds)")
self._game_start_timer()
except Exception as e:
logger.error(f"Failed to handle START_GAME message: {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
# This is where future extensions can be added # This is where future extensions can be added
pass pass
def _start_game_timer(self):
"""Start the timer for automated game start"""
if not self.settings.timer.enabled:
return
delay_seconds = self.settings.timer.delay_minutes * 60
logger.info(f"Starting game timer: {self.settings.timer.delay_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_DELAYED message")
try:
# Create and send START_GAME_DELAYED message
start_game_delayed_message = MessageBuilder.start_game_delayed(
sender="timer",
delay_minutes=self.settings.timer.delay_minutes
)
self.message_bus.publish(start_game_delayed_message, broadcast=True)
logger.info("START_GAME_DELAYED message sent successfully")
except Exception as e:
logger.error(f"Failed to send START_GAME_DELAYED message: {e}")
def _cancel_game_timer(self):
"""Cancel the game start timer if it's running"""
if self._game_start_timer and self._game_start_timer.is_alive():
logger.info("Cancelling game start timer")
self._game_start_timer.cancel()
self._game_start_timer = None
def _check_component_health(self): def _check_component_health(self):
"""Check health of all components""" """Check health of all components"""
try: try:
...@@ -666,6 +782,9 @@ class MbetterClientApplication: ...@@ -666,6 +782,9 @@ class MbetterClientApplication:
self.running = False self.running = False
self.shutdown_event.set() self.shutdown_event.set()
# Cancel game timer if running
self._cancel_game_timer()
# Send shutdown message to all components # Send shutdown message to all components
if self.message_bus: if self.message_bus:
shutdown_message = Message( shutdown_message = Message(
......
"""
Server-side match timer component for synchronized countdown across all clients
"""
import time
import logging
import threading
from typing import Dict, Any, Optional
from datetime import datetime, timedelta
from .thread_manager import ThreadedComponent
from .message_bus import MessageBus, Message, MessageType, MessageBuilder
from ..database.manager import DatabaseManager
from ..config.manager import ConfigManager
logger = logging.getLogger(__name__)
class MatchTimerComponent(ThreadedComponent):
"""Server-side match timer that synchronizes countdown across all clients"""
def __init__(self, message_bus: MessageBus, db_manager: DatabaseManager,
config_manager: ConfigManager):
super().__init__("match_timer", message_bus)
self.db_manager = db_manager
self.config_manager = config_manager
# Timer state
self.timer_running = False
self.timer_start_time: Optional[float] = None
self.timer_duration_seconds = 0
self.current_fixture_id: Optional[str] = None
self.current_match_id: Optional[int] = None
# Synchronization
self._timer_lock = threading.RLock()
self._last_update = time.time()
# Register message handlers
self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game)
self.message_bus.subscribe(self.name, MessageType.START_GAMES, self._handle_start_games)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
logger.info("MatchTimer component initialized")
def initialize(self) -> bool:
"""Initialize the match timer component"""
try:
logger.info("MatchTimer component initialized successfully")
return True
except Exception as e:
logger.error(f"Failed to initialize MatchTimer: {e}")
return False
def run(self):
"""Main timer loop"""
logger.info("MatchTimer component started")
while self.running:
try:
current_time = time.time()
# Check if timer needs to be updated
if self.timer_running and self.timer_start_time:
elapsed = current_time - self.timer_start_time
remaining = self.timer_duration_seconds - elapsed
if remaining <= 0:
# Timer reached zero, start next match
self._on_timer_expired()
else:
# Update last activity
self._last_update = current_time
# Check if we should stop the timer (no more matches)
if self.timer_running and self._should_stop_timer():
self._stop_timer()
logger.info("Timer stopped - no more matches to process")
# Sleep for a short interval
time.sleep(0.1)
except Exception as e:
logger.error(f"MatchTimer run loop error: {e}")
time.sleep(1.0)
logger.info("MatchTimer component stopped")
def shutdown(self):
"""Shutdown the match timer"""
with self._timer_lock:
self.timer_running = False
self.timer_start_time = None
self.current_fixture_id = None
self.current_match_id = None
logger.info("MatchTimer component shutdown")
def get_timer_state(self) -> Dict[str, Any]:
"""Get current timer state for API responses"""
with self._timer_lock:
if not self.timer_running or not self.timer_start_time:
return {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None
}
elapsed = time.time() - self.timer_start_time
remaining = max(0, self.timer_duration_seconds - elapsed)
return {
"running": True,
"remaining_seconds": int(remaining),
"total_seconds": self.timer_duration_seconds,
"fixture_id": self.current_fixture_id,
"match_id": self.current_match_id,
"start_time": self.timer_start_time,
"elapsed_seconds": int(elapsed)
}
def _handle_start_game(self, message: Message):
"""Handle START_GAME message"""
try:
fixture_id = message.data.get("fixture_id")
logger.info(f"Received START_GAME message for fixture: {fixture_id}")
# Get match interval from configuration
match_interval = self._get_match_interval()
# Start the timer
self._start_timer(match_interval * 60, fixture_id)
except Exception as e:
logger.error(f"Failed to handle START_GAME message: {e}")
def _handle_start_games(self, message: Message):
"""Handle START_GAMES message"""
try:
logger.info("Received START_GAMES message")
# Get match interval from configuration
match_interval = self._get_match_interval()
# Start the timer without specific fixture (will find first available)
self._start_timer(match_interval * 60, None)
except Exception as e:
logger.error(f"Failed to handle START_GAMES message: {e}")
def _handle_custom_message(self, message: Message):
"""Handle custom messages (like timer state requests)"""
try:
request = message.data.get("request")
if request == "get_timer_state":
# Send timer state response
timer_state = self.get_timer_state()
response = Message(
type=MessageType.CUSTOM,
sender=self.name,
recipient=message.sender,
data={
"response": "timer_state",
"timer_state": timer_state,
"timestamp": time.time()
},
correlation_id=message.correlation_id
)
self.message_bus.publish(response)
elif request == "stop":
# Stop the timer
self._stop_timer()
response = Message(
type=MessageType.CUSTOM,
sender=self.name,
recipient=message.sender,
data={
"response": "timer_stopped",
"timestamp": time.time()
},
correlation_id=message.correlation_id
)
self.message_bus.publish(response)
except Exception as e:
logger.error(f"Failed to handle custom message: {e}")
def _start_timer(self, duration_seconds: int, fixture_id: Optional[str]):
"""Start the countdown timer"""
with self._timer_lock:
self.timer_duration_seconds = duration_seconds
self.timer_start_time = time.time()
self.timer_running = True
self.current_fixture_id = fixture_id
logger.info(f"Match timer started: {duration_seconds}s for fixture {fixture_id}")
# Send timer started notification
self._send_timer_update()
def _stop_timer(self):
"""Stop the countdown timer"""
with self._timer_lock:
self.timer_running = False
self.timer_start_time = None
self.current_fixture_id = None
self.current_match_id = None
logger.info("Match timer stopped")
# Send timer stopped notification
self._send_timer_update()
def _on_timer_expired(self):
"""Handle timer expiration - start next match"""
try:
logger.info("Match timer expired, starting next match...")
# Find and start the next match
match_info = self._find_and_start_next_match()
if match_info:
logger.info(f"Started match {match_info['match_id']} in fixture {match_info['fixture_id']}")
# Reset timer for next interval
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, match_info['fixture_id'])
else:
logger.info("No more matches to start, stopping timer")
self._stop_timer()
except Exception as e:
logger.error(f"Failed to handle timer expiration: {e}")
# Reset timer on error
match_interval = self._get_match_interval()
self._start_timer(match_interval * 60, self.current_fixture_id)
def _find_and_start_next_match(self) -> Optional[Dict[str, Any]]:
"""Find and start the next available match"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
# Priority 1: Use current fixture if specified
target_fixture_id = None
target_match = None
if self.current_fixture_id:
# Find next match in current fixture
matches = session.query(MatchModel).filter_by(
fixture_id=self.current_fixture_id
).order_by(MatchModel.match_number.asc()).all()
target_match = self._find_next_match_in_list(matches)
if not target_match:
# Priority 2: Find matches in today's fixtures
today = datetime.now().date()
today_matches = session.query(MatchModel).filter(
MatchModel.start_time >= datetime.combine(today, datetime.min.time()),
MatchModel.start_time < datetime.combine(today, datetime.max.time())
).order_by(MatchModel.created_at.asc()).all()
if today_matches:
# Group by fixture and find first available match
fixtures = {}
for match in today_matches:
if match.fixture_id not in fixtures:
fixtures[match.fixture_id] = []
fixtures[match.fixture_id].append(match)
# Try each fixture
for fixture_id, matches in fixtures.items():
target_match = self._find_next_match_in_list(matches)
if target_match:
target_fixture_id = fixture_id
break
if not target_match:
# Priority 3: Find any pending matches
all_matches = session.query(MatchModel).filter_by(
status='pending'
).order_by(MatchModel.created_at.asc()).all()
if all_matches:
target_match = all_matches[0]
target_fixture_id = target_match.fixture_id
if target_match:
# Send MATCH_START message
match_start_message = MessageBuilder.match_start(
sender=self.name,
fixture_id=target_fixture_id or target_match.fixture_id,
match_id=target_match.id
)
self.message_bus.publish(match_start_message)
return {
"fixture_id": target_fixture_id or target_match.fixture_id,
"match_id": target_match.id,
"match_number": target_match.match_number
}
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to find and start next match: {e}")
return None
def _find_next_match_in_list(self, matches: list) -> Optional[Any]:
"""Find the next match to start from a list of matches"""
# Priority order: bet -> scheduled -> pending
for status in ['bet', 'scheduled', 'pending']:
for match in matches:
if match.status == status:
return match
return None
def _should_stop_timer(self) -> bool:
"""Check if timer should be stopped (no more matches)"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import MatchModel
# Count matches that can be started
active_matches = session.query(MatchModel).filter(
MatchModel.status.in_(['bet', 'scheduled', 'pending'])
).count()
return active_matches == 0
finally:
session.close()
except Exception as e:
logger.error(f"Failed to check if timer should stop: {e}")
return False
def _get_match_interval(self) -> int:
"""Get match interval from configuration"""
try:
if self.config_manager:
general_config = self.config_manager.get_section_config("general") or {}
return general_config.get('match_interval', 20)
else:
return 20 # Default fallback
except Exception as e:
logger.error(f"Failed to get match interval: {e}")
return 20
def _send_timer_update(self):
"""Send timer update message to all clients"""
try:
timer_state = self.get_timer_state()
update_message = Message(
type=MessageType.CUSTOM, # Use custom type for timer updates
sender=self.name,
data={
"timer_update": timer_state,
"timestamp": time.time()
}
)
# Send to web dashboard for broadcasting to clients
update_message.recipient = "web_dashboard"
self.message_bus.publish(update_message)
except Exception as e:
logger.error(f"Failed to send timer update: {e}")
\ No newline at end of file
...@@ -63,6 +63,7 @@ class MessageType(Enum): ...@@ -63,6 +63,7 @@ class MessageType(Enum):
# Game messages # Game messages
START_GAME = "START_GAME" START_GAME = "START_GAME"
START_GAMES = "START_GAMES" START_GAMES = "START_GAMES"
START_GAME_DELAYED = "START_GAME_DELAYED"
MATCH_START = "MATCH_START" MATCH_START = "MATCH_START"
GAME_STATUS = "GAME_STATUS" GAME_STATUS = "GAME_STATUS"
GAME_UPDATE = "GAME_UPDATE" GAME_UPDATE = "GAME_UPDATE"
...@@ -549,6 +550,18 @@ class MessageBuilder: ...@@ -549,6 +550,18 @@ class MessageBuilder:
} }
) )
@staticmethod
def start_game_delayed(sender: str, fixture_id: Optional[str] = None, delay_minutes: Optional[int] = None) -> Message:
"""Create START_GAME_DELAYED message"""
return Message(
type=MessageType.START_GAME_DELAYED,
sender=sender,
data={
"fixture_id": fixture_id,
"delay_minutes": delay_minutes
}
)
@staticmethod @staticmethod
def match_start(sender: str, fixture_id: str, match_id: int) -> Message: def match_start(sender: str, fixture_id: str, match_id: int) -> Message:
"""Create MATCH_START message""" """Create MATCH_START message"""
......
...@@ -42,6 +42,16 @@ class WebDashboard(ThreadedComponent): ...@@ -42,6 +42,16 @@ class WebDashboard(ThreadedComponent):
self.api: Optional[DashboardAPI] = None self.api: Optional[DashboardAPI] = None
self.main_application = None # Will be set by the main application self.main_application = None # Will be set by the main application
# Timer state storage
self.current_timer_state: Dict[str, Any] = {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None
}
# 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)
...@@ -72,6 +82,7 @@ class WebDashboard(ThreadedComponent): ...@@ -72,6 +82,7 @@ class WebDashboard(ThreadedComponent):
# Subscribe to messages # Subscribe to messages
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)
logger.info("WebDashboard initialized successfully") logger.info("WebDashboard initialized successfully")
return True return True
...@@ -321,6 +332,32 @@ class WebDashboard(ThreadedComponent): ...@@ -321,6 +332,32 @@ class WebDashboard(ThreadedComponent):
except Exception as e: except Exception as e:
logger.error(f"Failed to handle system status: {e}") logger.error(f"Failed to handle system status: {e}")
def _handle_custom_message(self, message: Message):
"""Handle custom messages (like timer state responses)"""
try:
response = message.data.get("response")
if response == "timer_state":
# Update stored timer state
timer_state = message.data.get("timer_state", {})
self.current_timer_state.update(timer_state)
logger.debug(f"Timer state updated: {timer_state}")
elif response == "timer_stopped":
# Reset timer state
self.current_timer_state = {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None
}
logger.debug("Timer stopped")
except Exception as e:
logger.error(f"Failed to handle custom message: {e}")
def get_app_context(self): def get_app_context(self):
"""Get Flask application context""" """Get Flask application context"""
if self.app: if self.app:
...@@ -395,6 +432,38 @@ class WebDashboard(ThreadedComponent): ...@@ -395,6 +432,38 @@ class WebDashboard(ThreadedComponent):
logger.error(f"Failed to get system status: {e}") logger.error(f"Failed to get system status: {e}")
return {"error": str(e)} return {"error": str(e)}
def get_timer_state(self) -> Dict[str, Any]:
"""Get current timer state"""
try:
# Request fresh timer state from match_timer component
if self.message_bus:
request_message = Message(
type=MessageType.CUSTOM,
sender=self.name,
recipient="match_timer",
data={
"request": "get_timer_state",
"timestamp": time.time()
},
correlation_id=f"timer_state_{int(time.time() * 1000)}"
)
self.message_bus.publish(request_message)
# Return current cached state
return self.current_timer_state.copy()
except Exception as e:
logger.error(f"Failed to get timer state: {e}")
return {
"running": False,
"remaining_seconds": 0,
"total_seconds": 0,
"fixture_id": None,
"match_id": None,
"start_time": None,
"error": str(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:
......
...@@ -3,6 +3,7 @@ Flask routes for web dashboard ...@@ -3,6 +3,7 @@ Flask routes for web dashboard
""" """
import logging import logging
import time
from datetime import datetime from datetime import datetime
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, session
from flask_login import login_required, current_user, login_user, logout_user from flask_login import login_required, current_user, login_user, logout_user
...@@ -1976,3 +1977,194 @@ def update_extraction_config(): ...@@ -1976,3 +1977,194 @@ def update_extraction_config():
except Exception as e: except Exception as e:
logger.error(f"API update extraction config error: {e}") logger.error(f"API update extraction config error: {e}")
return jsonify({"error": str(e)}), 500 return jsonify({"error": str(e)}), 500
# Betting Mode API routes
@api_bp.route('/betting-mode')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_betting_mode():
"""Get current user's betting mode"""
try:
from ..database.models import BettingModeModel
user_id = getattr(current_user, 'id', None) or getattr(request, 'current_user', {}).get('user_id')
if not user_id:
return jsonify({"error": "User not authenticated"}), 401
session = api_bp.db_manager.get_session()
try:
# Get user's betting mode
betting_mode = session.query(BettingModeModel).filter_by(user_id=user_id).first()
if betting_mode:
mode = betting_mode.mode
else:
# Return default if not set
mode = 'all_bets_on_start'
return jsonify({
"success": True,
"betting_mode": mode,
"is_default": betting_mode is None
})
finally:
session.close()
except Exception as e:
logger.error(f"API get betting mode error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/betting-mode', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def set_betting_mode():
"""Set current user's betting mode"""
try:
from ..database.models import BettingModeModel
user_id = getattr(current_user, 'id', None) or getattr(request, 'current_user', {}).get('user_id')
if not user_id:
return jsonify({"error": "User not authenticated"}), 401
data = request.get_json() or {}
mode = data.get('betting_mode')
if not mode:
return jsonify({"error": "betting_mode is required"}), 400
# Validate mode
valid_modes = ['all_bets_on_start', 'one_bet_at_a_time']
if mode not in valid_modes:
return jsonify({"error": f"Invalid betting mode. Must be one of: {', '.join(valid_modes)}"}), 400
session = api_bp.db_manager.get_session()
try:
# Check if betting mode already exists for user
existing_mode = session.query(BettingModeModel).filter_by(user_id=user_id).first()
if existing_mode:
# Update existing mode
existing_mode.mode = mode
existing_mode.updated_at = datetime.utcnow()
else:
# Create new betting mode
new_mode = BettingModeModel(
user_id=user_id,
mode=mode
)
session.add(new_mode)
session.commit()
logger.info(f"Updated betting mode for user {user_id}: {mode}")
return jsonify({
"success": True,
"message": f"Betting mode set to '{mode}'",
"betting_mode": mode
})
finally:
session.close()
except Exception as e:
logger.error(f"API set betting mode error: {e}")
return jsonify({"error": str(e)}), 500
# Server-side Match Timer API endpoints
@api_bp.route('/match-timer/state')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_match_timer_state():
"""Get current match timer state from server-side component"""
try:
# Get timer state from web dashboard (which caches responses from match_timer)
if hasattr(g, 'main_app') and g.main_app and hasattr(g.main_app, 'web_dashboard'):
timer_state = g.main_app.web_dashboard.get_timer_state()
return jsonify({
"success": True,
"running": timer_state.get("running", False),
"remaining_seconds": timer_state.get("remaining_seconds", 0),
"total_seconds": timer_state.get("total_seconds", 0),
"fixture_id": timer_state.get("fixture_id"),
"match_id": timer_state.get("match_id"),
"start_time": timer_state.get("start_time"),
"elapsed_seconds": timer_state.get("elapsed_seconds", 0)
})
else:
return jsonify({
"success": False,
"error": "Web dashboard not available"
}), 500
except Exception as e:
logger.error(f"API get match timer state error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/match-timer/control', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def control_match_timer():
"""Control the match timer (admin only)"""
try:
data = request.get_json() or {}
action = data.get('action')
if not action:
return jsonify({"error": "Action is required"}), 400
if action == 'start':
# Send START_GAMES message to start the timer
if api_bp.message_bus:
from ..core.message_bus import MessageBuilder, MessageType
start_message = MessageBuilder.start_games(sender="web_dashboard")
api_bp.message_bus.publish(start_message)
return jsonify({
"success": True,
"message": "Match timer started"
})
else:
return jsonify({
"success": False,
"error": "Message bus not available"
}), 500
elif action == 'stop':
# Send custom stop message to timer component
if api_bp.message_bus:
from ..core.message_bus import Message, MessageType
stop_message = Message(
type=MessageType.CUSTOM,
sender="web_dashboard",
recipient="match_timer",
data={
"request": "stop",
"timestamp": time.time()
}
)
api_bp.message_bus.publish(stop_message)
return jsonify({
"success": True,
"message": "Match timer stop requested"
})
else:
return jsonify({
"success": False,
"error": "Message bus not available"
}), 500
else:
return jsonify({"error": f"Unknown action: {action}"}), 400
except Exception as e:
logger.error(f"API control match timer error: {e}")
return jsonify({"error": str(e)}), 500
\ No newline at end of file
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