Match creations and cleanup of old matches now working correctly

parent 4e06f4e7
...@@ -28,14 +28,14 @@ class GamesThread(ThreadedComponent): ...@@ -28,14 +28,14 @@ class GamesThread(ThreadedComponent):
self.message_queue = None self.message_queue = None
def _cleanup_stale_ingame_matches(self): def _cleanup_stale_ingame_matches(self):
"""Clean up any stale 'ingame' matches from previous crashed sessions""" """Clean up any stale 'ingame' matches from previous crashed sessions and old 'bet' fixtures"""
try: try:
session = self.db_manager.get_session() session = self.db_manager.get_session()
try: try:
# Get today's date # Get today's date
today = datetime.now().date() today = datetime.now().date()
# Find all ingame matches from today that might be stale # PART 1: Clean up stale 'ingame' matches from today (existing logic)
stale_matches = session.query(MatchModel).filter( stale_matches = session.query(MatchModel).filter(
MatchModel.start_time.isnot(None), MatchModel.start_time.isnot(None),
MatchModel.start_time >= datetime.combine(today, datetime.min.time()), MatchModel.start_time >= datetime.combine(today, datetime.min.time()),
...@@ -44,26 +44,63 @@ class GamesThread(ThreadedComponent): ...@@ -44,26 +44,63 @@ class GamesThread(ThreadedComponent):
MatchModel.active_status == True MatchModel.active_status == True
).all() ).all()
if not stale_matches: if stale_matches:
logger.info(f"Found {len(stale_matches)} stale ingame matches - cleaning up")
for match in stale_matches:
logger.info(f"Cleaning up stale match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}")
match.status = 'pending'
match.active_status = False
session.commit()
logger.info(f"Cleaned up {len(stale_matches)} stale ingame matches")
else:
logger.info("No stale ingame matches found") logger.info("No stale ingame matches found")
return
logger.info(f"Found {len(stale_matches)} stale ingame matches - cleaning up") # PART 2: Clean up ALL old 'bet' fixtures (new logic)
old_bet_matches = session.query(MatchModel).filter(
MatchModel.status == 'bet',
MatchModel.active_status == True,
# Exclude today's matches to avoid interfering with active games
~MatchModel.start_time.between(
datetime.combine(today, datetime.min.time()),
datetime.combine(today, datetime.max.time())
)
).all()
if old_bet_matches:
logger.info(f"Found {len(old_bet_matches)} old 'bet' matches - cancelling them")
# Change status to pending and set active_status to False for match in old_bet_matches:
for match in stale_matches: logger.info(f"Cancelling old bet match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}")
logger.info(f"Cleaning up stale match {match.match_number}: {match.fighter1_township} vs {match.fighter2_township}") match.status = 'cancelled'
match.status = 'pending'
match.active_status = False
session.commit() # Cancel/refund associated bets
logger.info(f"Cleaned up {len(stale_matches)} stale ingame matches") self._cancel_match_bets(match.id, session)
session.commit()
logger.info(f"Cancelled {len(old_bet_matches)} old bet matches")
else:
logger.info("No old bet matches found to cancel")
finally: finally:
session.close() session.close()
except Exception as e: except Exception as e:
logger.error(f"Failed to cleanup stale ingame matches: {e}") logger.error(f"Failed to cleanup stale matches: {e}")
def _cancel_match_bets(self, match_id: int, session):
"""Cancel all pending bets for a match"""
try:
# Update all pending bets for this match to 'cancelled'
cancelled_count = session.query(BetDetailModel).filter(
BetDetailModel.match_id == match_id,
BetDetailModel.result == 'pending'
).update({'result': 'cancelled'})
if cancelled_count > 0:
logger.info(f"Cancelled {cancelled_count} pending bets for match {match_id}")
except Exception as e:
logger.error(f"Failed to cancel bets for match {match_id}: {e}")
def initialize(self) -> bool: def initialize(self) -> bool:
"""Initialize the games thread""" """Initialize the games thread"""
...@@ -836,6 +873,14 @@ class GamesThread(ThreadedComponent): ...@@ -836,6 +873,14 @@ class GamesThread(ThreadedComponent):
logger.info(f"🎬 Dispatching START_INTRO message for fixture {fixture_id}") logger.info(f"🎬 Dispatching START_INTRO message for fixture {fixture_id}")
self._dispatch_start_intro(fixture_id) self._dispatch_start_intro(fixture_id)
# Broadcast GAME_STARTED message to notify all components that game has started with this fixture
game_started_message = MessageBuilder.game_started(
sender=self.name,
fixture_id=fixture_id
)
self.message_bus.publish(game_started_message, broadcast=True)
logger.info(f"🎯 Broadcast GAME_STARTED message for fixture {fixture_id}")
# Refresh dashboard statuses # Refresh dashboard statuses
self._refresh_dashboard_statuses() self._refresh_dashboard_statuses()
...@@ -1881,11 +1926,11 @@ class GamesThread(ThreadedComponent): ...@@ -1881,11 +1926,11 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to send NEXT_MATCH: {e}") logger.error(f"Failed to send NEXT_MATCH: {e}")
def _select_random_completed_matches(self, count: int, session) -> List[MatchModel]: def _select_random_completed_matches(self, count: int, session) -> List[MatchModel]:
"""Select random completed matches from the database""" """Select random completed matches from the database (including cancelled and failed)"""
try: try:
# Get all completed matches (status = 'done') # Get all completed matches (status = 'done', 'cancelled', or 'failed')
completed_matches = session.query(MatchModel).filter( completed_matches = session.query(MatchModel).filter(
MatchModel.status == 'done', MatchModel.status.in_(['done', 'cancelled', 'failed']),
MatchModel.active_status == True MatchModel.active_status == True
).all() ).all()
...@@ -1907,7 +1952,13 @@ class GamesThread(ThreadedComponent): ...@@ -1907,7 +1952,13 @@ class GamesThread(ThreadedComponent):
"""Create new matches in the fixture by copying from old completed matches""" """Create new matches in the fixture by copying from old completed matches"""
try: try:
now = datetime.utcnow() now = datetime.utcnow()
match_number = 1
# Find the maximum match_number in the fixture and increment from there
max_match_number = session.query(MatchModel.match_number).filter(
MatchModel.fixture_id == fixture_id
).order_by(MatchModel.match_number.desc()).first()
match_number = (max_match_number[0] + 1) if max_match_number else 1
for old_match in old_matches: for old_match in old_matches:
# Create a new match based on the old one # Create a new match based on the old one
...@@ -1959,6 +2010,7 @@ class GamesThread(ThreadedComponent): ...@@ -1959,6 +2010,7 @@ class GamesThread(ThreadedComponent):
fixture_id = f"recycle_{uuid.uuid4().hex[:8]}" fixture_id = f"recycle_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow() now = datetime.utcnow()
# For a new fixture, start match_number from 1
match_number = 1 match_number = 1
for old_match in old_matches: for old_match in old_matches:
# Create a new match based on the old one # Create a new match based on the old one
......
...@@ -40,7 +40,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -40,7 +40,7 @@ class MatchTimerComponent(ThreadedComponent):
self.message_bus.register_component(self.name) self.message_bus.register_component(self.name)
# Register message handlers # Register message handlers
self.message_bus.subscribe(self.name, MessageType.START_GAME, self._handle_start_game) self.message_bus.subscribe(self.name, MessageType.GAME_STARTED, self._handle_game_started)
self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games) self.message_bus.subscribe(self.name, MessageType.SCHEDULE_GAMES, self._handle_schedule_games)
self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message) self.message_bus.subscribe(self.name, MessageType.CUSTOM, self._handle_custom_message)
self.message_bus.subscribe(self.name, MessageType.NEXT_MATCH, self._handle_next_match) self.message_bus.subscribe(self.name, MessageType.NEXT_MATCH, self._handle_next_match)
...@@ -106,8 +106,8 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -106,8 +106,8 @@ class MatchTimerComponent(ThreadedComponent):
logger.debug(f"MatchTimer processing message: {message}") logger.debug(f"MatchTimer processing message: {message}")
# Handle messages directly since some messages don't trigger subscription handlers # Handle messages directly since some messages don't trigger subscription handlers
if message.type == MessageType.START_GAME: if message.type == MessageType.GAME_STARTED:
self._handle_start_game(message) self._handle_game_started(message)
elif message.type == MessageType.SCHEDULE_GAMES: elif message.type == MessageType.SCHEDULE_GAMES:
self._handle_schedule_games(message) self._handle_schedule_games(message)
elif message.type == MessageType.CUSTOM: elif message.type == MessageType.CUSTOM:
...@@ -157,12 +157,12 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -157,12 +157,12 @@ class MatchTimerComponent(ThreadedComponent):
"elapsed_seconds": int(elapsed) "elapsed_seconds": int(elapsed)
} }
def _handle_start_game(self, message: Message): def _handle_game_started(self, message: Message):
"""Handle START_GAME message""" """Handle GAME_STARTED message"""
try: try:
fixture_id = message.data.get("fixture_id") fixture_id = message.data.get("fixture_id")
logger.info(f"Received START_GAME message for fixture: {fixture_id}") logger.info(f"Received GAME_STARTED message for fixture: {fixture_id}")
# Get match interval from configuration # Get match interval from configuration
match_interval = self._get_match_interval() match_interval = self._get_match_interval()
...@@ -171,7 +171,7 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -171,7 +171,7 @@ class MatchTimerComponent(ThreadedComponent):
self._start_timer(match_interval * 60, fixture_id) self._start_timer(match_interval * 60, fixture_id)
except Exception as e: except Exception as e:
logger.error(f"Failed to handle START_GAME message: {e}") logger.error(f"Failed to handle GAME_STARTED message: {e}")
def _handle_schedule_games(self, message: Message): def _handle_schedule_games(self, message: Message):
"""Handle SCHEDULE_GAMES message""" """Handle SCHEDULE_GAMES message"""
...@@ -435,9 +435,8 @@ class MatchTimerComponent(ThreadedComponent): ...@@ -435,9 +435,8 @@ class MatchTimerComponent(ThreadedComponent):
} }
) )
# Send to web dashboard for broadcasting to clients # Broadcast to all components including qt_player and web_dashboard
update_message.recipient = "web_dashboard" self.message_bus.publish(update_message, broadcast=True)
self.message_bus.publish(update_message)
except Exception as e: except Exception as e:
logger.error(f"Failed to send timer update: {e}") logger.error(f"Failed to send timer update: {e}")
\ No newline at end of file
...@@ -62,6 +62,7 @@ class MessageType(Enum): ...@@ -62,6 +62,7 @@ class MessageType(Enum):
# Game messages # Game messages
START_GAME = "START_GAME" START_GAME = "START_GAME"
GAME_STARTED = "GAME_STARTED"
SCHEDULE_GAMES = "SCHEDULE_GAMES" SCHEDULE_GAMES = "SCHEDULE_GAMES"
START_GAME_DELAYED = "START_GAME_DELAYED" START_GAME_DELAYED = "START_GAME_DELAYED"
START_INTRO = "START_INTRO" START_INTRO = "START_INTRO"
...@@ -572,6 +573,17 @@ class MessageBuilder: ...@@ -572,6 +573,17 @@ class MessageBuilder:
} }
) )
@staticmethod
def game_started(sender: str, fixture_id: str) -> Message:
"""Create GAME_STARTED message"""
return Message(
type=MessageType.GAME_STARTED,
sender=sender,
data={
"fixture_id": fixture_id
}
)
@staticmethod @staticmethod
def schedule_games(sender: str, fixture_id: Optional[str] = None) -> Message: def schedule_games(sender: str, fixture_id: Optional[str] = None) -> Message:
"""Create SCHEDULE_GAMES message""" """Create SCHEDULE_GAMES message"""
......
...@@ -2140,6 +2140,182 @@ class Migration_028_AddFixtureRefreshIntervalConfig(DatabaseMigration): ...@@ -2140,6 +2140,182 @@ class Migration_028_AddFixtureRefreshIntervalConfig(DatabaseMigration):
logger.error(f"Failed to remove fixture refresh interval configuration: {e}") logger.error(f"Failed to remove fixture refresh interval configuration: {e}")
return False return False
class Migration_029_ChangeMatchNumberToUniqueWithinFixture(DatabaseMigration):
"""Change match_number from globally unique to unique within fixture"""
def __init__(self):
super().__init__("029", "Change match_number from globally unique to unique within fixture")
def up(self, db_manager) -> bool:
"""Change match_number constraint from global uniqueness to unique within fixture"""
try:
with db_manager.engine.connect() as conn:
# SQLite doesn't support ALTER TABLE DROP CONSTRAINT directly
# We need to recreate the table with the new constraint
# Step 1: Create new table with correct constraint
conn.execute(text("""
CREATE TABLE IF NOT EXISTS matches_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_number INTEGER NOT NULL,
fighter1_township VARCHAR(255) NOT NULL,
fighter2_township VARCHAR(255) NOT NULL,
venue_kampala_township VARCHAR(255) NOT NULL,
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
fixture_active_time INTEGER NULL,
filename VARCHAR(1024) NOT NULL,
file_sha1sum VARCHAR(255) NOT NULL,
fixture_id VARCHAR(255) NOT NULL,
active_status BOOLEAN DEFAULT FALSE,
zip_filename VARCHAR(1024) NULL,
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(fixture_id, match_number)
)
"""))
# Step 2: Copy data from old table to new table
conn.execute(text("""
INSERT INTO matches_new
SELECT * FROM matches
"""))
# Step 3: Drop old table
conn.execute(text("DROP TABLE matches"))
# Step 4: Rename new table to original name
conn.execute(text("ALTER TABLE matches_new RENAME TO matches"))
# Step 5: Recreate indexes (without the old global unique constraint)
indexes = [
"CREATE INDEX IF NOT EXISTS ix_matches_match_number ON matches(match_number)",
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_id ON matches(fixture_id)",
"CREATE INDEX IF NOT EXISTS ix_matches_active_status ON matches(active_status)",
"CREATE INDEX IF NOT EXISTS ix_matches_file_sha1sum ON matches(file_sha1sum)",
"CREATE INDEX IF NOT EXISTS ix_matches_zip_sha1sum ON matches(zip_sha1sum)",
"CREATE INDEX IF NOT EXISTS ix_matches_zip_upload_status ON matches(zip_upload_status)",
"CREATE INDEX IF NOT EXISTS ix_matches_created_by ON matches(created_by)",
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_active_time ON matches(fixture_active_time)",
"CREATE INDEX IF NOT EXISTS ix_matches_done ON matches(done)",
"CREATE INDEX IF NOT EXISTS ix_matches_running ON matches(running)",
"CREATE INDEX IF NOT EXISTS ix_matches_status ON matches(status)",
"CREATE INDEX IF NOT EXISTS ix_matches_composite ON matches(active_status, zip_upload_status, created_at)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
conn.commit()
logger.info("Changed match_number constraint from globally unique to unique within fixture")
return True
except Exception as e:
logger.error(f"Failed to change match_number constraint: {e}")
return False
def down(self, db_manager) -> bool:
"""Revert match_number constraint back to globally unique"""
try:
with db_manager.engine.connect() as conn:
# Check if there are any duplicate match_numbers within the same fixture
# that would prevent adding back the global unique constraint
result = conn.execute(text("""
SELECT fixture_id, match_number, COUNT(*) as count
FROM matches
GROUP BY fixture_id, match_number
HAVING COUNT(*) > 1
"""))
duplicates = result.fetchall()
if duplicates:
logger.error(f"Cannot revert to global unique constraint - duplicate match_numbers within fixtures found: {[(row[0], row[1]) for row in duplicates]}")
return False
# Recreate table with global unique constraint on match_number
conn.execute(text("""
CREATE TABLE IF NOT EXISTS matches_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
match_number INTEGER NOT NULL UNIQUE,
fighter1_township VARCHAR(255) NOT NULL,
fighter2_township VARCHAR(255) NOT NULL,
venue_kampala_township VARCHAR(255) NOT NULL,
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
fixture_active_time INTEGER NULL,
filename VARCHAR(1024) NOT NULL,
file_sha1sum VARCHAR(255) NOT NULL,
fixture_id VARCHAR(255) NOT NULL,
active_status BOOLEAN DEFAULT FALSE,
zip_filename VARCHAR(1024) NULL,
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""))
# Copy data from old table to new table
conn.execute(text("""
INSERT INTO matches_new
SELECT * FROM matches
"""))
# Drop old table and rename new table
conn.execute(text("DROP TABLE matches"))
conn.execute(text("ALTER TABLE matches_new RENAME TO matches"))
# Recreate indexes
indexes = [
"CREATE INDEX IF NOT EXISTS ix_matches_match_number ON matches(match_number)",
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_id ON matches(fixture_id)",
"CREATE INDEX IF NOT EXISTS ix_matches_active_status ON matches(active_status)",
"CREATE INDEX IF NOT EXISTS ix_matches_file_sha1sum ON matches(file_sha1sum)",
"CREATE INDEX IF NOT EXISTS ix_matches_zip_sha1sum ON matches(zip_sha1sum)",
"CREATE INDEX IF NOT EXISTS ix_matches_zip_upload_status ON matches(zip_upload_status)",
"CREATE INDEX IF NOT EXISTS ix_matches_created_by ON matches(created_by)",
"CREATE INDEX IF NOT EXISTS ix_matches_fixture_active_time ON matches(fixture_active_time)",
"CREATE INDEX IF NOT EXISTS ix_matches_done ON matches(done)",
"CREATE INDEX IF NOT EXISTS ix_matches_running ON matches(running)",
"CREATE INDEX IF NOT EXISTS ix_matches_status ON matches(status)",
"CREATE INDEX IF NOT EXISTS ix_matches_composite ON matches(active_status, zip_upload_status, created_at)",
]
for index_sql in indexes:
conn.execute(text(index_sql))
conn.commit()
logger.info("Reverted match_number constraint back to globally unique")
return True
except Exception as e:
logger.error(f"Failed to revert match_number constraint: {e}")
return False
# Registry of all migrations in order # Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [ MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(), Migration_001_InitialSchema(),
...@@ -2170,6 +2346,7 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -2170,6 +2346,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_026_AddExtractionStatsTable(), Migration_026_AddExtractionStatsTable(),
Migration_027_AddDefaultIntroTemplatesConfig(), Migration_027_AddDefaultIntroTemplatesConfig(),
Migration_028_AddFixtureRefreshIntervalConfig(), Migration_028_AddFixtureRefreshIntervalConfig(),
Migration_029_ChangeMatchNumberToUniqueWithinFixture(),
] ]
......
...@@ -475,11 +475,11 @@ class MatchModel(BaseModel): ...@@ -475,11 +475,11 @@ class MatchModel(BaseModel):
Index('ix_matches_created_by', 'created_by'), Index('ix_matches_created_by', 'created_by'),
Index('ix_matches_fixture_active_time', 'fixture_active_time'), Index('ix_matches_fixture_active_time', 'fixture_active_time'),
Index('ix_matches_composite', 'active_status', 'zip_upload_status', 'created_at'), Index('ix_matches_composite', 'active_status', 'zip_upload_status', 'created_at'),
UniqueConstraint('match_number', name='uq_matches_match_number'), UniqueConstraint('fixture_id', 'match_number', name='uq_matches_fixture_match'),
) )
# Core match data from fixture file # Core match data from fixture file
match_number = Column(Integer, nullable=False, unique=True, comment='Match # from fixture file') match_number = Column(Integer, nullable=False, comment='Match # from fixture file')
fighter1_township = Column(String(255), nullable=False, comment='Fighter1 (Township)') fighter1_township = Column(String(255), nullable=False, comment='Fighter1 (Township)')
fighter2_township = Column(String(255), nullable=False, comment='Fighter2 (Township)') fighter2_township = Column(String(255), nullable=False, comment='Fighter2 (Township)')
venue_kampala_township = Column(String(255), nullable=False, comment='Venue (Kampala Township)') venue_kampala_township = Column(String(255), nullable=False, comment='Venue (Kampala Township)')
......
...@@ -55,11 +55,18 @@ class OverlayWebChannel(QObject): ...@@ -55,11 +55,18 @@ class OverlayWebChannel(QObject):
# Signal to receive console messages from JavaScript # Signal to receive console messages from JavaScript
consoleMessage = pyqtSignal(str, str, int, str) # level, message, line, source consoleMessage = pyqtSignal(str, str, int, str) # level, message, line, source
def __init__(self, db_manager=None): def __init__(self, db_manager=None, message_bus=None):
super().__init__() super().__init__()
self.mutex = QMutex() self.mutex = QMutex()
self.overlay_data = {} self.overlay_data = {}
self.db_manager = db_manager self.db_manager = db_manager
self.message_bus = message_bus
self.timer_state = {"running": False, "remaining_seconds": 0}
# Subscribe to timer updates if message bus is available
if self.message_bus:
self.message_bus.subscribe("qt_player", MessageType.CUSTOM, self._handle_timer_update)
logger.info("OverlayWebChannel initialized") logger.info("OverlayWebChannel initialized")
@pyqtSlot(str) @pyqtSlot(str)
...@@ -203,6 +210,33 @@ class OverlayWebChannel(QObject): ...@@ -203,6 +210,33 @@ class OverlayWebChannel(QObject):
logger.error(f"Failed to get fixture data: {e}") logger.error(f"Failed to get fixture data: {e}")
return json.dumps([]) return json.dumps([])
def _handle_timer_update(self, message: Message):
"""Handle timer update messages"""
try:
logger.debug(f"OverlayWebChannel received message: {message.type} from {message.sender}")
logger.debug(f"Message data: {message.data}")
if message.data.get("timer_update"):
timer_update = message.data["timer_update"]
with QMutexLocker(self.mutex):
self.timer_state = timer_update
logger.debug(f"Timer state updated: {timer_update}")
else:
logger.debug("Message does not contain timer_update")
except Exception as e:
logger.error(f"Failed to handle timer update: {e}")
@pyqtSlot(result=str)
def getTimerState(self) -> str:
"""Provide current cached timer state to JavaScript via WebChannel"""
try:
with QMutexLocker(self.mutex):
timer_state = self.timer_state.copy()
logger.debug(f"Providing cached timer state to JavaScript: {timer_state}")
return json.dumps(timer_state)
except Exception as e:
logger.error(f"Failed to get timer state: {e}")
return json.dumps({"running": False, "remaining_seconds": 0})
def _get_fixture_data_from_games_thread(self) -> Optional[List[Dict[str, Any]]]: def _get_fixture_data_from_games_thread(self) -> Optional[List[Dict[str, Any]]]:
"""Get fixture data from the games thread""" """Get fixture data from the games thread"""
try: try:
...@@ -302,6 +336,7 @@ class OverlayWebChannel(QObject): ...@@ -302,6 +336,7 @@ class OverlayWebChannel(QObject):
class VideoProcessingWorker(QRunnable): class VideoProcessingWorker(QRunnable):
"""Background worker for video processing tasks""" """Background worker for video processing tasks"""
...@@ -427,7 +462,11 @@ class OverlayWebView(QWebEngineView): ...@@ -427,7 +462,11 @@ class OverlayWebView(QWebEngineView):
# Setup WebChannel # Setup WebChannel
self.web_channel = QWebChannel() self.web_channel = QWebChannel()
self.overlay_channel = OverlayWebChannel(db_manager=self.db_manager) # Get message bus from parent window
message_bus = None
if hasattr(self.parent(), '_message_bus'):
message_bus = self.parent()._message_bus
self.overlay_channel = OverlayWebChannel(db_manager=self.db_manager, message_bus=message_bus)
self.web_channel.registerObject("overlay", self.overlay_channel) self.web_channel.registerObject("overlay", self.overlay_channel)
page.setWebChannel(self.web_channel) page.setWebChannel(self.web_channel)
...@@ -659,15 +698,16 @@ class OverlayWebView(QWebEngineView): ...@@ -659,15 +698,16 @@ class OverlayWebView(QWebEngineView):
from PyQt6.QtCore import QTimer from PyQt6.QtCore import QTimer
QTimer.singleShot(100, lambda: self._ensure_overlay_visibility_post_load(was_visible)) QTimer.singleShot(100, lambda: self._ensure_overlay_visibility_post_load(was_visible))
# If fixtures template was loaded, the template handles its own data fetching via JavaScript # If fixtures or match template was loaded, the template handles its own data fetching via WebChannel
if template_name == "fixtures.html" or template_name == "fixtures": if template_name == "fixtures.html" or template_name == "fixtures" or template_name == "match.html" or template_name == "match":
logger.info("Fixtures template loaded - template handles its own data fetching via JavaScript API calls") template_type = "fixtures" if ("fixtures" in template_name) else "match"
# Send webServerBaseUrl to the fixtures template for API calls logger.info(f"{template_type.title()} template loaded - template handles its own data fetching via WebChannel")
logger.info(f"Sending webServerBaseUrl to fixtures template: {self.web_server_url}") # Send webServerBaseUrl to the template for WebChannel setup
logger.info(f"Sending webServerBaseUrl to {template_type} template: {self.web_server_url}")
data_to_send = {'webServerBaseUrl': self.web_server_url} data_to_send = {'webServerBaseUrl': self.web_server_url}
if self.debug_overlay: if self.debug_overlay:
data_to_send['debugMode'] = True data_to_send['debugMode'] = True
logger.info("Debug mode enabled for fixtures template") logger.info(f"Debug mode enabled for {template_type} template")
self.update_overlay_data(data_to_send) self.update_overlay_data(data_to_send)
# Ensure console override is active after template load # Ensure console override is active after template load
...@@ -2918,6 +2958,10 @@ class QtVideoPlayer(QObject): ...@@ -2918,6 +2958,10 @@ class QtVideoPlayer(QObject):
if self.debug_player: if self.debug_player:
logger.info("Calling _handle_play_video_result handler") logger.info("Calling _handle_play_video_result handler")
self._handle_play_video_result(message) self._handle_play_video_result(message)
elif message.type == MessageType.CUSTOM:
if self.debug_player:
logger.info("Calling _handle_custom_message handler")
self._handle_custom_message(message)
else: else:
if self.debug_player: if self.debug_player:
logger.warning(f"No handler for message type: {message.type.value}") logger.warning(f"No handler for message type: {message.type.value}")
...@@ -3945,6 +3989,27 @@ class QtVideoPlayer(QObject): ...@@ -3945,6 +3989,27 @@ class QtVideoPlayer(QObject):
logger.info("QtPlayer: System status handling failed, trying to play intro directly") logger.info("QtPlayer: System status handling failed, trying to play intro directly")
self._check_and_play_intro() self._check_and_play_intro()
def _handle_custom_message(self, message: Message):
"""Handle custom messages, including timer updates for WebChannel"""
try:
# Forward timer update messages to the OverlayWebChannel
if message.data.get("timer_update"):
logger.debug(f"QtPlayer: Forwarding timer update to OverlayWebChannel")
if hasattr(self, 'window') and self.window and hasattr(self.window, 'window_overlay'):
overlay_view = self.window.window_overlay
if isinstance(overlay_view, OverlayWebView) and hasattr(overlay_view, 'overlay_channel'):
overlay_view.overlay_channel._handle_timer_update(message)
logger.debug("QtPlayer: Timer update forwarded to OverlayWebChannel")
else:
logger.debug("QtPlayer: No OverlayWebView or overlay_channel available for timer update")
else:
logger.debug("QtPlayer: No window or window_overlay available for timer update")
else:
logger.debug(f"QtPlayer: Received custom message without timer_update: {message.data}")
except Exception as e:
logger.error(f"QtPlayer: Failed to handle custom message: {e}")
def _handle_web_dashboard_ready(self, message: Message): def _handle_web_dashboard_ready(self, message: Message):
"""Handle web dashboard ready messages to update server URL""" """Handle web dashboard ready messages to update server URL"""
try: try:
......
...@@ -105,13 +105,14 @@ ...@@ -105,13 +105,14 @@
border-radius: 20px; border-radius: 20px;
padding: 30px; padding: 30px;
max-width: 90%; max-width: 90%;
max-height: 80%; max-height: 85%; /* Increased from 80% to allow more space */
overflow-y: auto; overflow: visible; /* Changed from overflow-y: auto to visible to prevent scrollbar */
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px); backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.1);
opacity: 0; opacity: 0;
animation: fadeInScale 1s ease-out forwards; animation: fadeInScale 1s ease-out forwards;
padding-bottom: 50px; /* Add extra bottom padding to ensure content doesn't touch border */
} }
.fixtures-title { .fixtures-title {
...@@ -315,24 +316,6 @@ ...@@ -315,24 +316,6 @@
} }
} }
/* Scrollbar styling */
.fixtures-panel::-webkit-scrollbar {
width: 8px;
}
.fixtures-panel::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.1);
border-radius: 4px;
}
.fixtures-panel::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 4px;
}
.fixtures-panel::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
</style> </style>
</head> </head>
<body> <body>
...@@ -770,46 +753,47 @@ ...@@ -770,46 +753,47 @@
]; ];
} }
// Find next match and start countdown // Get timer state and start countdown
function findNextMatchAndStartCountdown() { async function getTimerStateAndStartCountdown() {
if (!fixturesData || fixturesData.length === 0) { console.log('🔍 DEBUG: getTimerStateAndStartCountdown called');
return;
}
// Clear any existing countdown try {
if (countdownInterval) { // Get timer state from WebChannel
clearInterval(countdownInterval); const timerStateJson = await window.overlay.getTimerState();
countdownInterval = null; console.log('🔍 DEBUG: Raw timer state JSON:', timerStateJson);
} const timerState = JSON.parse(timerStateJson);
const now = new Date(); console.log('🔍 DEBUG: Parsed timer state:', timerState);
let nextMatch = null;
let earliestTime = null; // Clear any existing countdown
if (countdownInterval) {
// Find the match with the earliest start time that hasn't started yet clearInterval(countdownInterval);
for (const match of fixturesData) { countdownInterval = null;
if (match.start_time) {
const startTime = new Date(match.start_time);
if (startTime > now && (!earliestTime || startTime < earliestTime)) {
earliestTime = startTime;
nextMatch = match;
}
} }
}
if (nextMatch && earliestTime) { if (timerState.running && timerState.remaining_seconds > 0) {
nextMatchStartTime = earliestTime; // Timer is running, show countdown
nextMatchStartTime = new Date(Date.now() + (timerState.remaining_seconds * 1000));
// Show next match info // Show next match info (generic message since we don't know which match)
const nextMatchInfo = document.getElementById('nextMatchInfo'); const nextMatchInfo = document.getElementById('nextMatchInfo');
nextMatchInfo.textContent = `Next: ${nextMatch.fighter1_township || nextMatch.fighter1} vs ${nextMatch.fighter2_township || nextMatch.fighter2}`; nextMatchInfo.textContent = `Next match starting in:`;
nextMatchInfo.style.display = 'block'; nextMatchInfo.style.display = 'block';
console.log('🔍 DEBUG: Timer countdown displayed');
// Start countdown // Start countdown
updateCountdown(); updateCountdown();
countdownInterval = setInterval(updateCountdown, 1000); countdownInterval = setInterval(updateCountdown, 1000);
} else { console.log('🔍 DEBUG: Countdown started with timer state');
// No upcoming matches, hide countdown } else {
// No active timer, hide countdown
document.getElementById('nextMatchInfo').style.display = 'none';
document.getElementById('countdownTimer').style.display = 'none';
console.log('🔍 DEBUG: No active timer, countdown hidden');
}
} catch (error) {
console.log('🔍 DEBUG: Failed to get timer state:', error);
// Fallback: hide countdown
document.getElementById('nextMatchInfo').style.display = 'none'; document.getElementById('nextMatchInfo').style.display = 'none';
document.getElementById('countdownTimer').style.display = 'none'; document.getElementById('countdownTimer').style.display = 'none';
} }
...@@ -982,8 +966,8 @@ ...@@ -982,8 +966,8 @@
fixturesContent.style.display = 'block'; fixturesContent.style.display = 'block';
debugTime('Fixtures table rendered and displayed'); debugTime('Fixtures table rendered and displayed');
// Find next match and start countdown // Get timer state and start countdown
findNextMatchAndStartCountdown(); getTimerStateAndStartCountdown();
debugTime('Countdown initialization completed'); debugTime('Countdown initialization completed');
} }
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>Match Overlay</title> <title>Match Overlay</title>
<script src="qrc:///qtwebchannel/qwebchannel.js"></script>
<style> <style>
* { * {
margin: 0; margin: 0;
...@@ -19,7 +20,7 @@ ...@@ -19,7 +20,7 @@
height: 100vh; height: 100vh;
position: relative; position: relative;
} }
/* Debug indicator to verify CSS is loaded */ /* Debug indicator to verify CSS is loaded */
body::before { body::before {
content: 'Match Overlay v1.0 loaded'; content: 'Match Overlay v1.0 loaded';
...@@ -30,6 +31,61 @@ ...@@ -30,6 +31,61 @@
font-size: 10px; font-size: 10px;
z-index: 9999; z-index: 9999;
} }
/* Debug console overlay */
#debugConsole {
position: absolute;
top: 10px;
right: 10px;
width: 400px;
height: 300px;
background: rgba(0, 0, 0, 0.9);
border: 2px solid #00ff00;
border-radius: 8px;
color: #00ff00;
font-family: 'Courier New', monospace;
font-size: 11px;
padding: 8px;
overflow-y: auto;
z-index: 10000;
display: none; /* Hidden by default, shown when --debug-player */
}
#debugConsole.show {
display: block;
}
#debugConsole .console-header {
font-weight: bold;
border-bottom: 1px solid #00ff00;
padding-bottom: 4px;
margin-bottom: 4px;
}
#debugConsole .console-message {
margin: 2px 0;
word-wrap: break-word;
}
#debugConsole .console-timestamp {
color: #ffff00;
}
#debugConsole .console-level-log {
color: #ffffff;
}
#debugConsole .console-level-error {
color: #ff4444;
}
#debugConsole .console-level-warn {
color: #ffaa00;
}
#debugConsole .console-level-info {
color: #00aaff;
}
.overlay-container { .overlay-container {
position: absolute; position: absolute;
...@@ -45,10 +101,10 @@ ...@@ -45,10 +101,10 @@
} }
.fixtures-panel { .fixtures-panel {
background: rgba(0, 123, 255, 0.40); background: rgba(0, 123, 255, 0.85);
border-radius: 20px; border-radius: 20px;
padding: 30px; padding: 30px;
max-width: 90%; max-width: 95%;
max-height: 80%; max-height: 80%;
overflow-y: auto; overflow-y: auto;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
...@@ -57,44 +113,152 @@ ...@@ -57,44 +113,152 @@
opacity: 0; opacity: 0;
animation: fadeInScale 1s ease-out forwards; animation: fadeInScale 1s ease-out forwards;
} }
.fixtures-title { .match-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 30px;
width: 100%;
}
.fighters-section {
flex: 1;
text-align: left;
display: flex;
align-items: center;
}
.fighter-names {
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.fighter1-name, .fighter2-name {
font-size: 72px;
font-weight: bold;
color: white; color: white;
font-size: 28px; text-shadow: 3px 3px 6px rgba(0, 0, 0, 0.7);
line-height: 1.1;
}
.vs-text {
font-size: 48px;
font-weight: bold;
color: #ffeb3b;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.6);
padding: 12px 24px;
border-radius: 16px;
border: 3px solid #ffeb3b;
margin: 16px 0;
}
.venue-timer-section {
flex: 1;
text-align: right;
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 24px;
}
.venue-label {
font-size: 36px;
font-weight: bold;
color: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
margin-bottom: -6px;
}
.venue-display {
font-size: 144px;
font-weight: bold;
color: rgba(255, 255, 255, 0.95);
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
line-height: 1.1;
}
.next-match-info {
text-align: center;
color: rgba(255, 255, 255, 0.9);
font-size: 32px;
margin-bottom: 10px;
font-weight: bold; font-weight: bold;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.countdown-timer {
text-align: center; text-align: center;
margin-bottom: 25px; color: #ffeb3b;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5); font-size: 64px;
font-weight: bold;
margin-bottom: 24px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.8);
background: rgba(0, 0, 0, 0.3);
padding: 12px 24px;
border-radius: 16px;
border: 3px solid rgba(255, 235, 59, 0.3);
}
.countdown-timer.warning {
color: #ff9800;
border-color: rgba(255, 152, 0, 0.5);
}
.countdown-timer.urgent {
color: #f44336;
border-color: rgba(244, 67, 54, 0.5);
animation: pulse 1s infinite;
}
@keyframes pulse {
0% { transform: scale(1); }
50% { transform: scale(1.05); }
100% { transform: scale(1); }
} }
.fixtures-table { .outcomes-grid {
display: grid;
gap: 10px;
width: 100%; width: 100%;
border-collapse: collapse; max-width: 1200px;
color: white; margin-top: 20px;
font-size: 16px; justify-content: center;
background: transparent;
} }
.fixtures-table th { .outcome-cell {
padding: 15px 10px; display: flex;
text-align: center; flex-direction: column;
background: rgba(255, 255, 255, 0.1); align-items: center;
justify-content: center;
min-height: 80px;
}
.outcome-name {
font-size: 28px;
font-weight: bold; font-weight: bold;
font-size: 14px; color: rgba(255, 255, 255, 0.9);
text-transform: uppercase; margin-bottom: 8px;
letter-spacing: 1px;
border-radius: 8px;
margin: 2px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5); text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
} }
.fixtures-table td { .outcome-value {
padding: 12px 10px; font-size: 36px;
text-align: center; font-weight: bold;
background: rgba(255, 255, 255, 0.05); color: #ffffff;
border-radius: 6px; text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
margin: 1px; }
transition: background-color 0.3s ease;
.under-over-cell {
background: rgba(40, 167, 69, 0.2);
border: 1px solid rgba(40, 167, 69, 0.3);
}
.under-over-cell .outcome-value {
color: #28a745;
} }
.fixtures-table tbody tr:hover td { .fixtures-table tbody tr:hover td {
...@@ -251,22 +415,37 @@ ...@@ -251,22 +415,37 @@
</style> </style>
</head> </head>
<body> <body>
<!-- Debug Console Overlay -->
<div id="debugConsole">
<div class="console-header">🔍 JavaScript Debug Console</div>
<div id="consoleOutput"></div>
</div>
<div class="overlay-container"> <div class="overlay-container">
<div class="fixtures-panel" id="fixturesPanel"> <div class="fixtures-panel" id="fixturesPanel">
<div class="fixtures-title" id="matchTitle">Next Match</div> <!-- Top section with fighters and venue/timer -->
<div class="venue-display" id="matchVenue">Venue</div> <div class="match-header">
<div class="fighters-section">
<div class="fighter-names" id="fighterNames">
<div class="fighter1-name">Fighter 1</div>
<div class="vs-text">VS</div>
<div class="fighter2-name">Fighter 2</div>
</div>
</div>
<div class="venue-timer-section">
<div class="venue-label">VENUE:</div>
<div class="venue-display" id="matchVenue">Venue</div>
<div class="next-match-info" id="nextMatchInfo" style="display: none;"></div>
<div class="countdown-timer" id="countdownTimer" style="display: none;"></div>
</div>
</div>
<!-- Bottom section with odds grid -->
<div class="loading-message" id="loadingMessage" style="display: none;">Loading match data...</div> <div class="loading-message" id="loadingMessage" style="display: none;">Loading match data...</div>
<div id="matchContent" style="display: none;"> <div id="matchContent" style="display: none;">
<table class="fixtures-table" id="outcomesTable"> <div class="outcomes-grid" id="outcomesGrid">
<thead> <!-- Grid items will be populated by JavaScript -->
<tr id="outcomesHeader"> </div>
<!-- Headers will be populated by JavaScript -->
</tr>
</thead>
<tbody id="outcomesBody">
<!-- Content will be populated by JavaScript -->
</tbody>
</table>
</div> </div>
<div class="no-matches" id="noMatches" style="display: none;"> <div class="no-matches" id="noMatches" style="display: none;">
No matches available for betting No matches available for betting
...@@ -279,104 +458,327 @@ ...@@ -279,104 +458,327 @@
let overlayData = {}; let overlayData = {};
let fixturesData = null; let fixturesData = null;
let outcomesData = null; let outcomesData = null;
let countdownInterval = null;
let nextMatchStartTime = null;
let startTime = null;
// Apply console.log override immediately with buffering
(function() {
var originalConsoleLog = console.log;
var messageBuffer = [];
console.log = function(...args) {
var message = args.map(String).join(' ');
if (window.overlay && window.overlay.log) {
window.overlay.log('[LOG] ' + message);
} else {
messageBuffer.push('[LOG] ' + message);
}
originalConsoleLog.apply(console, args);
};
// Function to flush buffer when overlay becomes available
window.flushConsoleBuffer = function() {
if (window.overlay && window.overlay.log) {
messageBuffer.forEach(function(msg) {
window.overlay.log(msg);
});
messageBuffer = [];
}
};
// Check periodically for overlay availability
var checkInterval = setInterval(function() {
if (window.overlay && window.overlay.log) {
window.flushConsoleBuffer();
clearInterval(checkInterval);
}
}, 50);
// Clear interval after 5 seconds to avoid infinite polling
setTimeout(function() {
clearInterval(checkInterval);
}, 5000);
})();
// Test console override
console.log('TEST: Console override applied and buffering');
// 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}`);
}
// 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
let webServerUrlReceived = false;
// Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) { // Debug logging function that sends messages to Qt application logs
console.log('Received overlay data:', data); function debugLog(message, level = 'info') {
overlayData = data || {}; try {
// Try to send to Qt WebChannel if available
// Update web server base URL if provided if (typeof qt !== 'undefined' && qt.webChannelTransport) {
if (data && data.webServerBaseUrl) { // Send debug message to Qt application
webServerBaseUrl = data.webServerBaseUrl; if (window.sendDebugMessage) {
console.log('Updated web server base URL:', webServerBaseUrl); window.sendDebugMessage(`[MATCH] ${message}`);
}
}
} catch (e) {
// Fallback to console if WebChannel not available - use original console to avoid recursion
originalConsoleLog(`[MATCH FALLBACK] ${message}`);
} }
// Check if we have fixtures data // Always log to console as well for browser debugging - use original to avoid recursion
if (data && data.fixtures) { originalConsoleLog(`🔍 DEBUG: ${message}`);
fixturesData = data.fixtures; }
renderMatch();
// Store original console.log before overriding
const originalConsoleLog = console.log;
// Debug console functionality
let debugConsoleEnabled = false;
const maxConsoleMessages = 50;
let consoleMessageCount = 0;
function addToDebugConsole(level, message) {
if (!debugConsoleEnabled) return;
const consoleOutput = document.getElementById('consoleOutput');
if (!consoleOutput) return;
const messageDiv = document.createElement('div');
messageDiv.className = `console-message console-level-${level}`;
const timestamp = new Date().toLocaleTimeString();
messageDiv.innerHTML = `<span class="console-timestamp">[${timestamp}]</span> ${message}`;
consoleOutput.appendChild(messageDiv);
consoleMessageCount++;
// Remove old messages if too many
if (consoleMessageCount > maxConsoleMessages) {
const firstMessage = consoleOutput.firstElementChild;
if (firstMessage) {
consoleOutput.removeChild(firstMessage);
consoleMessageCount--;
}
}
// Auto-scroll to bottom
consoleOutput.scrollTop = consoleOutput.scrollHeight;
}
// Check if debug console should be enabled (via WebChannel or timeout)
function checkDebugConsole() {
const debugConsole = document.getElementById('debugConsole');
if (debugConsole && debugConsole.classList.contains('show')) {
debugConsoleEnabled = true;
console.log('🔍 Debug console detected as enabled');
}
}
// Check for debug console enablement once after a delay
setTimeout(checkDebugConsole, 1000);
// Setup WebChannel communication
function setupWebChannel() {
// Check if WebChannel is already set up by overlay.js
if (window.overlay) {
console.log('🔍 DEBUG: WebChannel already set up by overlay.js');
// Test WebChannel
if (window.overlay && window.overlay.log) {
window.overlay.log('TEST: WebChannel connection successful');
}
// Listen for data updates from Python
if (window.overlay.dataUpdated) {
window.overlay.dataUpdated.connect(function(data) {
console.log('🔍 DEBUG: Received data update from Python:', data);
if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl;
webServerUrlReceived = true;
console.log('🔍 DEBUG: Web server base URL updated to:', webServerBaseUrl);
// If we were waiting for the URL, start fetching data now
if (document.readyState === 'complete' || document.readyState === 'interactive') {
console.log('🔍 DEBUG: Document ready, starting data fetch after URL update');
fetchFixtureData();
}
}
if (data && data.debugMode) {
console.log('🔍 DEBUG: Debug mode enabled, showing debug console');
const debugConsole = document.getElementById('debugConsole');
if (debugConsole) {
debugConsole.classList.add('show');
debugConsoleEnabled = true;
}
}
if (data && data.timer_update) {
console.log('🔍 DEBUG: Timer update received (fallback):', data.timer_update);
// Handle timer updates from match_timer
const timerData = data.timer_update;
if (timerData.running && timerData.remaining_seconds !== undefined) {
// Format remaining time
const minutes = Math.floor(timerData.remaining_seconds / 60);
const seconds = timerData.remaining_seconds % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
const countdownTimer = document.getElementById('countdownTimer');
if (countdownTimer) {
countdownTimer.textContent = timeString;
countdownTimer.className = 'countdown-timer';
countdownTimer.style.display = 'block';
// Add warning/urgent classes based on time remaining
if (timerData.remaining_seconds <= 60) { // 1 minute
countdownTimer.className = 'countdown-timer urgent';
} else if (timerData.remaining_seconds <= 300) { // 5 minutes
countdownTimer.className = 'countdown-timer warning';
} else {
countdownTimer.className = 'countdown-timer';
}
}
}
}
if (data && data.timer_update) {
console.log('🔍 DEBUG: Timer update received:', data.timer_update);
// Handle timer updates from match_timer
const timerData = data.timer_update;
if (timerData.running && timerData.remaining_seconds !== undefined) {
// Format remaining time
const minutes = Math.floor(timerData.remaining_seconds / 60);
const seconds = timerData.remaining_seconds % 60;
const timeString = `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
const countdownTimer = document.getElementById('countdownTimer');
if (countdownTimer) {
countdownTimer.textContent = timeString;
countdownTimer.className = 'countdown-timer';
countdownTimer.style.display = 'block';
// Add warning/urgent classes based on time remaining
if (timerData.remaining_seconds <= 60) { // 1 minute
countdownTimer.className = 'countdown-timer urgent';
} else if (timerData.remaining_seconds <= 300) { // 5 minutes
countdownTimer.className = 'countdown-timer warning';
} else {
countdownTimer.className = 'countdown-timer';
}
}
}
}
});
}
return;
}
// Fallback: setup WebChannel if overlay.js didn't do it
if (typeof qt !== 'undefined' && qt.webChannelTransport) {
try {
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('🔍 DEBUG: WebChannel connected successfully (fallback)');
// Connect to overlay object
window.overlay = channel.objects.overlay;
// Listen for data updates from Python
if (window.overlay && window.overlay.dataUpdated) {
window.overlay.dataUpdated.connect(function(data) {
console.log('🔍 DEBUG: Received data update from Python:', data);
if (data && data.webServerBaseUrl) {
webServerBaseUrl = data.webServerBaseUrl;
webServerUrlReceived = true;
console.log('🔍 DEBUG: Web server base URL updated to:', webServerBaseUrl);
// If we were waiting for the URL, start fetching data now
if (document.readyState === 'complete' || document.readyState === 'interactive') {
console.log('🔍 DEBUG: Document ready, starting data fetch after URL update');
fetchFixtureData();
}
}
if (data && data.debugMode) {
console.log('🔍 DEBUG: Debug mode enabled, showing debug console');
const debugConsole = document.getElementById('debugConsole');
if (debugConsole) {
debugConsole.classList.add('show');
debugConsoleEnabled = true;
}
}
});
} else {
console.log('🔍 DEBUG: Overlay object not available in WebChannel');
}
});
} catch (e) {
console.log('🔍 DEBUG: Failed to setup WebChannel:', e);
}
} else { } else {
// Fetch fixtures data from API console.log('🔍 DEBUG: WebChannel not available, using default webServerBaseUrl');
fetchFixturesData().then(() => {
renderMatch();
});
} }
} }
// Console override is now applied at the beginning of the script with buffering
// Fetch fixtures data from the API // Fetch fixture data using WebChannel instead of API fetch
async function fetchFixturesData() { async function fetchFixtureData() {
try { try {
console.log('Fetching fixtures data from API...'); debugTime('Fetching fixture data using WebChannel');
console.log('DEBUG: [MATCH] Attempting to get fixture data from WebChannel');
// Try multiple API endpoints with different authentication levels console.log('DEBUG: [MATCH] window.overlay exists:', !!window.overlay);
const apiEndpoints = [ console.log('DEBUG: [MATCH] window.overlay.getFixtureData exists:', !!(window.overlay && window.overlay.getFixtureData));
`${webServerBaseUrl}/api/cashier/pending-matches`,
`${webServerBaseUrl}/api/fixtures`, if (window.overlay && window.overlay.getFixtureData) {
`${webServerBaseUrl}/api/status` // Fallback to basic status endpoint // Get fixture data from Qt WebChannel (returns a Promise)
]; const fixtureJson = await window.overlay.getFixtureData();
console.log('DEBUG: [MATCH] Received fixture data from WebChannel:', fixtureJson);
let apiData = null;
let usedEndpoint = null; if (fixtureJson) {
try {
for (const endpoint of apiEndpoints) { fixturesData = JSON.parse(fixtureJson);
try { console.log('DEBUG: [MATCH] Parsed fixture data:', fixturesData);
console.log(`Trying API endpoint: ${endpoint}`); console.log('DEBUG: [MATCH] fixturesData.length:', fixturesData ? fixturesData.length : 'null');
const response = await fetch(endpoint, { debugTime('Fixture data received from WebChannel');
method: 'GET',
headers: { if (fixturesData && fixturesData.length > 0) {
'Content-Type': 'application/json' console.log('DEBUG: [MATCH] WebChannel returned fixture data, calling renderMatch()');
}, renderMatch();
credentials: 'include' // Include cookies for authentication } else {
}); console.log('DEBUG: [MATCH] WebChannel returned empty fixture data');
debugTime('No fixture data in WebChannel response');
if (response.ok) { showNoMatches('No matches available');
const data = await response.json();
console.log(`API Response from ${endpoint}:`, data);
if (data.success) {
apiData = data;
usedEndpoint = endpoint;
break;
} }
} else { } catch (parseError) {
console.warn(`API endpoint ${endpoint} returned status ${response.status}`); console.log('DEBUG: [MATCH] Failed to parse fixture data from WebChannel:', parseError);
debugTime(`Failed to parse fixture data: ${parseError.message}`);
showNoMatches('Unable to load matches - data parsing failed');
} }
} catch (endpointError) { } else {
console.warn(`Failed to fetch from ${endpoint}:`, endpointError); console.log('DEBUG: [MATCH] WebChannel returned null/empty fixture data');
continue; debugTime('No fixture data from WebChannel');
showNoMatches('No matches available');
} }
} } else {
console.log('DEBUG: [MATCH] WebChannel overlay.getFixtureData not available');
if (apiData && apiData.matches && apiData.matches.length > 0) { console.log('DEBUG: [MATCH] window object keys:', Object.keys(window));
console.log(`Found ${apiData.matches.length} matches from ${usedEndpoint}`); if (window.overlay) {
fixturesData = apiData.matches; console.log('DEBUG: [MATCH] window.overlay keys:', Object.keys(window.overlay));
renderFixtures();
return Promise.resolve();
} else if (apiData && apiData.fixtures && apiData.fixtures.length > 0) {
// Handle fixtures endpoint format
console.log(`Found ${apiData.fixtures.length} fixtures from ${usedEndpoint}`);
// Convert fixtures to matches format
fixturesData = [];
apiData.fixtures.forEach(fixture => {
if (fixture.matches) {
fixturesData.push(...fixture.matches);
}
});
if (fixturesData.length > 0) {
renderFixtures();
return Promise.resolve();
} }
debugTime('WebChannel not available for fixture data');
showNoMatches('Unable to load matches - WebChannel not available');
} }
// If we reach here, no valid data was found
console.log('No fixture data available from any API endpoint, will show fallback');
return Promise.reject('No API data available');
} catch (error) { } catch (error) {
console.error('Error fetching fixtures data:', error); console.log('DEBUG: [MATCH] Exception caught in fetchFixtureData');
return Promise.reject(error); console.log('DEBUG: [MATCH] Error message =', error.message);
console.log('DEBUG: [MATCH] Error stack =', error.stack);
debugTime(`Failed to fetch fixture data: ${error.message}`);
showNoMatches('Unable to load matches - WebChannel error');
} }
} }
...@@ -474,8 +876,99 @@ ...@@ -474,8 +876,99 @@
]; ];
} }
// Render the focused match view (first match in bet status) // Find next match and start countdown (same as fixtures.html)
function findNextMatchAndStartCountdown() {
if (!fixturesData || fixturesData.length === 0) {
return;
}
// Clear any existing countdown
if (countdownInterval) {
clearInterval(countdownInterval);
countdownInterval = null;
}
const now = new Date();
let nextMatch = null;
let earliestTime = null;
// Find the match with the earliest start time that hasn't started yet
for (const match of fixturesData) {
if (match.start_time) {
const startTime = new Date(match.start_time);
if (startTime > now && (!earliestTime || startTime < earliestTime)) {
earliestTime = startTime;
nextMatch = match;
}
}
}
if (nextMatch && earliestTime) {
nextMatchStartTime = earliestTime;
// Show next match info
const nextMatchInfo = document.getElementById('nextMatchInfo');
const fighter1 = nextMatch.fighter1_township || nextMatch.fighter1 || 'Fighter 1';
const fighter2 = nextMatch.fighter2_township || nextMatch.fighter2 || 'Fighter 2';
nextMatchInfo.textContent = `Next: ${fighter1} vs ${fighter2}`;
nextMatchInfo.style.display = 'block';
// Start countdown
updateCountdown();
countdownInterval = setInterval(updateCountdown, 1000);
} else {
// No upcoming matches, hide countdown
document.getElementById('nextMatchInfo').style.display = 'none';
document.getElementById('countdownTimer').style.display = 'none';
}
}
// Update countdown display
function updateCountdown() {
if (!nextMatchStartTime) {
return;
}
const now = new Date();
const timeDiff = nextMatchStartTime - now;
if (timeDiff <= 0) {
// Match has started
document.getElementById('countdownTimer').textContent = 'LIVE';
document.getElementById('countdownTimer').className = 'countdown-timer';
return;
}
const hours = Math.floor(timeDiff / (1000 * 60 * 60));
const minutes = Math.floor((timeDiff % (1000 * 60 * 60)) / (1000 * 60));
const seconds = Math.floor((timeDiff % (1000 * 60)) / 1000);
let timeString = '';
if (hours > 0) {
timeString = `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
} else {
timeString = `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
const countdownTimer = document.getElementById('countdownTimer');
countdownTimer.textContent = timeString;
// Add warning/urgent classes based on time remaining
if (timeDiff <= 60000) { // 1 minute
countdownTimer.className = 'countdown-timer urgent';
} else if (timeDiff <= 300000) { // 5 minutes
countdownTimer.className = 'countdown-timer warning';
} else {
countdownTimer.className = 'countdown-timer';
}
countdownTimer.style.display = 'block';
}
// Render the focused match view (first match in bet status) - TV titling style
function renderMatch() { function renderMatch() {
debugTime('Starting renderMatch function');
const loadingMessage = document.getElementById('loadingMessage'); const loadingMessage = document.getElementById('loadingMessage');
const matchContent = document.getElementById('matchContent'); const matchContent = document.getElementById('matchContent');
const noMatches = document.getElementById('noMatches'); const noMatches = document.getElementById('noMatches');
...@@ -486,45 +979,75 @@ ...@@ -486,45 +979,75 @@
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) {
showNoMatches('No matches available for betting'); debugTime('No fixtures data available');
showNoMatches('No matches available');
return; return;
} }
// Find the first match with status 'bet' debugTime(`Processing ${fixturesData.length} fixtures`);
const betMatch = fixturesData.find(match => match.status === 'bet'); console.log('DEBUG: fixturesData =', fixturesData);
if (!betMatch) { // Find the first match (next upcoming match)
showNoMatches('No matches currently available for betting'); const nextMatch = fixturesData[0]; // Get first match from the 5 retrieved
if (!nextMatch) {
showNoMatches('No matches available');
return; return;
} }
console.log('Rendering focused match:', betMatch); console.log('Rendering next match for TV titling:', nextMatch);
console.log('DEBUG: nextMatch properties:', Object.keys(nextMatch));
console.log('DEBUG: nextMatch.fighter1_township =', nextMatch.fighter1_township);
console.log('DEBUG: nextMatch.fighter2_township =', nextMatch.fighter2_township);
console.log('DEBUG: nextMatch.venue_kampala_township =', nextMatch.venue_kampala_township);
// Update fighter names in top left (multi-line layout)
const fighter1 = nextMatch.fighter1_township || nextMatch.fighter1 || 'Fighter 1';
const fighter2 = nextMatch.fighter2_township || nextMatch.fighter2 || 'Fighter 2';
// Update title and venue const fighter1Name = document.querySelector('.fighter1-name');
const fighter1 = betMatch.fighter1_township || betMatch.fighter1 || 'Fighter 1'; const fighter2Name = document.querySelector('.fighter2-name');
const fighter2 = betMatch.fighter2_township || betMatch.fighter2 || 'Fighter 2';
matchTitle.textContent = `${fighter1} vs ${fighter2}`;
const venue = betMatch.venue_kampala_township || betMatch.venue || 'TBD'; if (fighter1Name) fighter1Name.textContent = fighter1;
if (fighter2Name) fighter2Name.textContent = fighter2;
// Update venue in top right
const venue = nextMatch.venue_kampala_township || nextMatch.venue || 'TBD';
matchVenue.textContent = venue; matchVenue.textContent = venue;
// Get outcomes for this match // Get outcomes for this match (comes as object/dict from database)
const outcomes = betMatch.outcomes || []; let outcomes = nextMatch.outcomes || {};
if (outcomes.length === 0) { console.log('DEBUG: Raw outcomes object:', outcomes);
console.log('DEBUG: Outcomes type:', typeof outcomes);
console.log('DEBUG: Outcomes keys:', Object.keys(outcomes));
// Convert outcomes object to array format for processing
let outcomesArray = [];
if (typeof outcomes === 'object' && outcomes !== null) {
// Handle dict format from database
Object.entries(outcomes).forEach(([key, value]) => {
outcomesArray.push({
outcome_name: key,
outcome_value: value
});
});
}
if (outcomesArray.length === 0) {
console.log('No outcomes found for match, using defaults'); console.log('No outcomes found for match, using defaults');
// Use default outcomes if none available // Use default outcomes if none available
outcomes.push(...getDefaultOutcomes()); outcomesArray = getDefaultOutcomes();
} }
console.log(`Found ${outcomes.length} outcomes for match ${betMatch.id || betMatch.match_number}`); console.log(`Found ${outcomesArray.length} outcomes for match ${nextMatch.id || nextMatch.match_number}`);
// Sort outcomes: common ones first, then alphabetically // Sort outcomes: common ones first, then alphabetically
const sortedOutcomes = outcomes.sort((a, b) => { const sortedOutcomes = outcomesArray.sort((a, b) => {
// Handle both API formats const aName = a.outcome_name || '';
const aName = a.outcome_name || a.column_name || ''; const bName = b.outcome_name || '';
const bName = b.outcome_name || b.column_name || '';
// Priority order for common outcomes // Priority order for common outcomes
const priority = { const priority = {
...@@ -543,27 +1066,37 @@ ...@@ -543,27 +1066,37 @@
return aName.localeCompare(bName); return aName.localeCompare(bName);
}); });
// Create table header // Create a balanced grid where each cell contains both outcome name and value
outcomesHeader.innerHTML = sortedOutcomes.map(outcome => { // Use square grid sizing (round up to next square)
const outcomeName = outcome.outcome_name || outcome.column_name; const totalOutcomes = sortedOutcomes.length;
return `<th>${outcomeName}</th>`; const gridSize = Math.ceil(Math.sqrt(totalOutcomes));
}).join('');
// Set CSS grid columns
// Create table body with odds const outcomesGrid = document.getElementById('outcomesGrid');
outcomesBody.innerHTML = ` outcomesGrid.style.gridTemplateColumns = `repeat(${gridSize}, 1fr)`;
<tr>
${sortedOutcomes.map(outcome => { let gridHTML = '';
const outcomeName = outcome.outcome_name || outcome.column_name;
const outcomeValue = outcome.outcome_value || outcome.float_value; // Create grid items for each outcome
const isUnderOver = outcomeName === 'UNDER' || outcomeName === 'OVER'; sortedOutcomes.forEach(outcome => {
const oddsClass = isUnderOver ? 'odds-value under-over' : 'odds-value'; const outcomeValue = outcome.outcome_value;
const displayValue = outcomeValue !== undefined && outcomeValue !== null ? parseFloat(outcomeValue).toFixed(2) : '-'; const displayValue = outcomeValue !== undefined && outcomeValue !== null ? parseFloat(outcomeValue).toFixed(2) : '-';
return `<td><span class="${oddsClass}">${displayValue}</span></td>`; const isUnderOver = outcome.outcome_name === 'UNDER' || outcome.outcome_name === 'OVER';
}).join('')} const cellClass = isUnderOver ? 'outcome-cell under-over-cell' : 'outcome-cell';
</tr> gridHTML += `<div class="${cellClass}">
`; <div class="outcome-name">${outcome.outcome_name}</div>
<div class="outcome-value">${displayValue}</div>
</div>`;
});
outcomesGrid.innerHTML = gridHTML;
matchContent.style.display = 'block'; matchContent.style.display = 'block';
debugTime('Match rendered and displayed');
// Find next match and start countdown
findNextMatchAndStartCountdown();
debugTime('Countdown initialization completed');
} }
// Show no matches message // Show no matches message
...@@ -577,60 +1110,48 @@ ...@@ -577,60 +1110,48 @@
// Initialize when DOM is loaded // Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
console.log('Match overlay initialized - attempting to fetch next match data'); startTime = Date.now();
debugTime('DOM Content Loaded - Starting match overlay initialization');
// Setup WebChannel first
setupWebChannel();
// Show loading message initially // Show loading message initially
document.getElementById('fixturesContent').style.display = 'none'; document.getElementById('matchContent').style.display = 'none';
document.getElementById('noMatches').style.display = 'none'; document.getElementById('noMatches').style.display = 'none';
document.getElementById('loadingMessage').style.display = 'block'; document.getElementById('loadingMessage').style.display = 'block';
document.getElementById('loadingMessage').textContent = 'Loading next match data...'; document.getElementById('loadingMessage').textContent = 'Loading next match data...';
// Start fetching real data immediately debugTime('UI initialized - Loading message displayed');
fetchFixturesData().then(() => {
renderMatch(); // Wait briefly for WebChannel to connect and potentially receive webServerBaseUrl
// If API fails completely, show fallback data after a short delay setTimeout(() => {
if (!webServerUrlReceived) {
console.log('🔍 DEBUG: WebServerBaseUrl not received via WebChannel, proceeding with WebChannel data fetch');
}
// Fetch fixture data directly from WebChannel
debugTime('Fetching fixture data from WebChannel');
fetchFixtureData();
// Set up periodic refresh every 30 seconds
setTimeout(function refreshData() {
debugTime('Periodic refresh: fetching updated fixture data');
fetchFixtureData();
setTimeout(refreshData, 30000);
}, 30000);
// Show no matches if no data after 5 seconds total
setTimeout(() => { setTimeout(() => {
if (!fixturesData || fixturesData.length === 0) { if (!fixturesData || fixturesData.length === 0) {
console.log('No data loaded after API attempts, forcing fallback display'); debugTime('No data received after 5 seconds - showing waiting message');
showFallbackMatches(); showNoMatches('Waiting for match data...');
renderMatch(); } else {
debugTime('Data was received before 5 second timeout');
} }
}, 2000); }, 5000);
}).catch(() => { }, 50); // Wait 50ms for WebChannel setup
console.log('API fetch failed, showing fallback data');
showFallbackMatches();
renderMatch();
});
// Refresh data every 30 seconds
setInterval(function() {
console.log('Refreshing match data...');
fetchFixturesData().then(() => {
renderMatch();
});
}, 30000);
}); });
// Qt WebChannel initialization (when available)
if (typeof QWebChannel !== 'undefined') {
new QWebChannel(qt.webChannelTransport, function(channel) {
console.log('WebChannel initialized for match overlay');
// Connect to overlay object if available
if (channel.objects.overlay) {
channel.objects.overlay.dataChanged.connect(function(data) {
updateOverlayData(data);
});
// Get initial data
if (channel.objects.overlay.getCurrentData) {
channel.objects.overlay.getCurrentData(function(data) {
updateOverlayData(data);
});
}
}
});
}
</script> </script>
<!-- <!--
......
...@@ -2613,7 +2613,7 @@ def notifications(): ...@@ -2613,7 +2613,7 @@ def notifications():
def message_handler(message): def message_handler(message):
"""Handle incoming messages for this client""" """Handle incoming messages for this client"""
if message.type in [MessageType.START_GAME, MessageType.MATCH_START, MessageType.GAME_STATUS]: if message.type in [MessageType.START_GAME, MessageType.GAME_STARTED, MessageType.MATCH_START, MessageType.GAME_STATUS]:
notification_data = { notification_data = {
"type": message.type.value, "type": message.type.value,
"data": message.data, "data": message.data,
......
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