Release 12

parent c00f9cdb
#!/usr/bin/env python3
"""
Script to check the current state of matches in the database
"""
import sys
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from mbetterclient.database.manager import DatabaseManager
from mbetterclient.database.models import MatchModel
from mbetterclient.config.settings import get_user_data_dir
def main():
print("🔍 Checking database matches...")
# Initialize database manager
db_path = get_user_data_dir() / "mbetterclient.db"
db_manager = DatabaseManager(str(db_path))
if not db_manager.initialize():
print("❌ Failed to initialize database")
return
session = db_manager.get_session()
try:
# Get all matches
matches = session.query(MatchModel).filter(MatchModel.active_status == True).all()
print(f"📊 Found {len(matches)} active matches in database")
if not matches:
print("❌ No active matches found")
return
# Group by fixture
fixtures = {}
for match in matches:
if match.fixture_id not in fixtures:
fixtures[match.fixture_id] = []
fixtures[match.fixture_id].append(match)
for fixture_id, fixture_matches in fixtures.items():
print(f"\n🏟️ Fixture: {fixture_id}")
print(f" Total matches: {len(fixture_matches)}")
# Count by status
status_counts = {}
for match in fixture_matches:
status = match.status
if status not in status_counts:
status_counts[status] = 0
status_counts[status] += 1
print(f" Status counts: {status_counts}")
# Show details for first few matches
print(" First 5 matches:")
for i, match in enumerate(fixture_matches[:5]):
print(f" #{match.match_number}: {match.fighter1_township} vs {match.fighter2_township} - Status: {match.status} - Start: {match.start_time}")
if len(fixture_matches) > 5:
print(f" ... and {len(fixture_matches) - 5} more matches")
# Check if fixture is from yesterday
if fixture_matches and fixture_matches[0].start_time:
from datetime import datetime, timedelta
yesterday = datetime.utcnow() - timedelta(days=1)
match_date = fixture_matches[0].start_time.date()
yesterday_date = yesterday.date()
is_yesterday = match_date == yesterday_date
print(f" Date: {match_date} - Is yesterday: {is_yesterday}")
except Exception as e:
print(f"❌ Error checking database: {e}")
import traceback
traceback.print_exc()
finally:
session.close()
if __name__ == "__main__":
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
Script to create test data for cross-day fixture testing.
Removes all existing fixtures and creates 10 matches for a "yesterday fixture":
- 5 completed matches (status='done')
- 5 matches in bet status (status='bet')
"""
import sys
import os
from datetime import datetime, timedelta
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from mbetterclient.database.manager import DatabaseManager
from mbetterclient.database.models import MatchModel, MatchOutcomeModel, MatchTemplateModel, MatchOutcomeTemplateModel, BetModel, BetDetailModel, ExtractionStatsModel
from mbetterclient.config.settings import get_user_data_dir
from sqlalchemy.orm import joinedload
def create_basic_templates(session):
"""Create basic match templates for testing"""
templates_data = [
("John Doe", "Mike Smith", "Sports Arena"),
("Alex Johnson", "Chris Brown", "Championship Hall"),
("David Wilson", "Tom Davis", "Boxing Center"),
("Robert Lee", "James Miller", "Fight Club"),
("Kevin White", "Brian Taylor", "Ring Stadium"),
("Steve Harris", "Paul Walker", "Combat Zone"),
("Mark Thompson", "Jason Clark", "Battle Ground"),
("Andrew Lewis", "Scott Hall", "Warrior Arena"),
("Peter Parker", "Bruce Wayne", "Gladiator Hall"),
("Tony Stark", "Clark Kent", "Champions Ring")
]
for i, (fighter1, fighter2, venue) in enumerate(templates_data):
# Create template
template = MatchTemplateModel(
match_number=i + 1,
fighter1_township=fighter1,
fighter2_township=fighter2,
venue_kampala_township=venue,
filename=f"template_{i+1}.txt",
file_sha1sum=f"template_sha1_{i}",
fixture_id=f"template_fixture_{i+1}", # Templates need a fixture_id
active_status=True,
zip_filename=f"template_{i+1}.zip",
zip_sha1sum=f"template_zip_sha1_{i}",
zip_upload_status='completed',
zip_validation_status='valid'
)
session.add(template)
session.flush() # Get the template ID
# Create template outcomes
outcomes = [
('WIN1', 2.10),
('X', 3.20),
('WIN2', 1.85),
('UNDER', 1.75),
('OVER', 2.05),
('KO1', 4.50),
('KO2', 6.00),
('PTS1', 2.80),
('PTS2', 3.10)
]
for outcome_name, coefficient in outcomes:
outcome = MatchOutcomeTemplateModel(
match_id=template.id,
column_name=outcome_name,
float_value=coefficient
)
session.add(outcome)
session.commit()
def main():
print("🔄 Starting test fixture creation...")
# Initialize database manager
db_path = get_user_data_dir() / "mbetterclient.db"
db_manager = DatabaseManager(str(db_path))
if not db_manager.initialize():
raise RuntimeError("Failed to initialize database")
session = db_manager.get_session()
try:
print("🗑️ Removing all existing matches and bets (keeping templates)...")
# Remove all bets and bet details first (due to foreign key constraints)
session.query(BetDetailModel).delete()
session.query(BetModel).delete()
# Remove extraction stats (references matches)
session.query(ExtractionStatsModel).delete()
# Remove all match outcomes (due to foreign key constraints)
session.query(MatchOutcomeModel).delete()
# Remove all matches (but keep templates)
session.query(MatchModel).delete()
session.commit()
print("✅ All existing matches and bets removed (templates preserved)")
# Check if templates exist, create some if needed
template_count = session.query(MatchTemplateModel).count()
if template_count == 0:
print("📋 No match templates found, creating basic templates...")
create_basic_templates(session)
template_count = session.query(MatchTemplateModel).count()
print(f"✅ Created {template_count} basic match templates")
# Calculate yesterday's date
yesterday = datetime.utcnow() - timedelta(days=1)
fixture_id = "test_yesterday_fixture"
print(f"📅 Creating 10 matches for yesterday fixture: {fixture_id}")
print(f"📅 Yesterday's date: {yesterday.date()}")
# Get all available templates
templates = session.query(MatchTemplateModel).options(joinedload(MatchTemplateModel.outcomes)).all()
if len(templates) < 10:
print(f"⚠️ Only {len(templates)} templates available, will reuse templates for remaining matches")
for i in range(10):
match_number = i + 1
status = 'done' if i < 5 else 'bet' # First 5 completed, next 5 in bet status
# Select template (reuse if we run out)
template = templates[i % len(templates)]
# Create match from template
match = MatchModel(
match_number=match_number,
fighter1_township=template.fighter1_township,
fighter2_township=template.fighter2_township,
venue_kampala_township=template.venue_kampala_township,
start_time=yesterday,
status=status,
fixture_id=fixture_id,
filename=template.filename,
file_sha1sum=template.file_sha1sum,
active_status=True,
zip_filename=template.zip_filename,
zip_sha1sum=template.zip_sha1sum,
zip_upload_status='completed',
zip_validation_status='valid',
fixture_active_time=int(yesterday.timestamp()),
result=None if status == 'bet' else 'WIN1', # Set result for completed matches
end_time=yesterday if status == 'done' else None,
done=(status == 'done'),
running=False
)
session.add(match)
session.flush() # Get the match ID
# Copy match outcomes from template
for template_outcome in template.outcomes:
outcome = MatchOutcomeModel(
match_id=match.id,
column_name=template_outcome.column_name,
float_value=template_outcome.float_value
)
session.add(outcome)
print(f"✅ Created match #{match_number}: {template.fighter1_township} vs {template.fighter2_township} - Status: {status}")
session.commit()
print("🎉 Test fixture creation completed!")
print(f"📊 Created fixture '{fixture_id}' with 10 matches:")
print(" - 5 completed matches (status='done')")
print(" - 5 matches in bet status (status='bet')")
print(f" - All matches dated: {yesterday.date()}")
except Exception as e:
print(f"❌ Error creating test fixture: {e}")
session.rollback()
raise
finally:
session.close()
if __name__ == "__main__":
main()
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script to create a yesterday fixture with 5 completed matches and 5 bet matches
for testing cross-day fixture handling.
"""
import sys
import os
from datetime import datetime, timedelta
from sqlalchemy.orm import sessionmaker
from sqlalchemy import create_engine
# Add the project root to the Python path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from mbetterclient.database.manager import DatabaseManager
from mbetterclient.database.models import MatchModel, MatchOutcomeModel, MatchTemplateModel, MatchOutcomeTemplateModel
from mbetterclient.config.settings import get_user_data_dir
def create_yesterday_fixture():
"""Create a fixture from yesterday with 5 completed and 5 bet matches"""
# Initialize database manager
db_path = get_user_data_dir() / "mbetterclient.db"
db_manager = DatabaseManager(str(db_path))
if not db_manager.initialize():
print("Failed to initialize database manager")
return False
Session = sessionmaker(bind=db_manager.engine)
session = Session()
try:
# Get yesterday's date
yesterday = datetime.utcnow() - timedelta(days=1)
yesterday_start = datetime.combine(yesterday.date(), datetime.min.time())
# Generate unique fixture ID
import uuid
fixture_id = f"yesterday_test_{uuid.uuid4().hex[:8]}"
print(f"Creating yesterday fixture: {fixture_id}")
# Get some match templates to use as base
templates = session.query(MatchTemplateModel).filter(
MatchTemplateModel.active_status == True
).limit(10).all()
if len(templates) < 10:
print(f"Warning: Only {len(templates)} templates available, need 10")
if len(templates) < 5:
print("Error: Need at least 5 templates")
return False
match_number = 1
# Create 5 completed matches
print("Creating 5 completed matches...")
for i in range(5):
template = templates[i % len(templates)]
# Create completed match
match = MatchModel(
match_number=match_number,
fighter1_township=template.fighter1_township,
fighter2_township=template.fighter2_township,
venue_kampala_township=template.venue_kampala_township,
start_time=yesterday_start + timedelta(hours=i), # Spread throughout yesterday
status='done',
fixture_id=fixture_id,
filename=template.filename,
file_sha1sum=template.file_sha1sum,
active_status=True,
zip_filename=template.zip_filename,
zip_sha1sum=template.zip_sha1sum,
zip_upload_status='completed',
zip_validation_status='valid',
fixture_active_time=int(yesterday_start.timestamp()),
result='WIN1', # Simple result
end_time=yesterday_start + timedelta(hours=i, minutes=30),
done=True,
running=False
)
session.add(match)
session.flush()
# Copy outcomes from template
for template_outcome in template.outcomes:
outcome = MatchOutcomeModel(
match_id=match.id,
column_name=template_outcome.column_name,
float_value=template_outcome.float_value
)
session.add(outcome)
print(f" Created completed match #{match_number}: {match.fighter1_township} vs {match.fighter2_township}")
match_number += 1
# Create 5 bet matches
print("Creating 5 bet matches...")
for i in range(5, 10):
template = templates[i % len(templates)]
# Create bet match
match = MatchModel(
match_number=match_number,
fighter1_township=template.fighter1_township,
fighter2_township=template.fighter2_township,
venue_kampala_township=template.venue_kampala_township,
start_time=yesterday_start + timedelta(hours=i), # Continue from yesterday
status='bet',
fixture_id=fixture_id,
filename=template.filename,
file_sha1sum=template.file_sha1sum,
active_status=True,
zip_filename=template.zip_filename,
zip_sha1sum=template.zip_sha1sum,
zip_upload_status='completed',
zip_validation_status='valid',
fixture_active_time=int(yesterday_start.timestamp()),
result=None, # No result yet
end_time=None,
done=False,
running=False
)
session.add(match)
session.flush()
# Copy outcomes from template
for template_outcome in template.outcomes:
outcome = MatchOutcomeModel(
match_id=match.id,
column_name=template_outcome.column_name,
float_value=template_outcome.float_value
)
session.add(outcome)
print(f" Created bet match #{match_number}: {match.fighter1_township} vs {match.fighter2_township}")
match_number += 1
session.commit()
print(f"\nSuccessfully created yesterday fixture '{fixture_id}' with:")
print(" - 5 completed matches")
print(" - 5 bet matches")
print(" - All matches from yesterday")
print("\nTo test: Start the game without providing a fixture_id.")
print("The system should create a new fixture for today and play the remaining 5 bet matches from yesterday first.")
return True
except Exception as e:
print(f"Error creating yesterday fixture: {e}")
session.rollback()
return False
finally:
session.close()
if __name__ == "__main__":
print("Creating test fixture with yesterday's matches...")
success = create_yesterday_fixture()
if success:
print("\nTest fixture created successfully!")
else:
print("\nFailed to create test fixture!")
sys.exit(1)
\ No newline at end of file
......@@ -29,6 +29,8 @@ class GamesThread(ThreadedComponent):
self._shutdown_event = threading.Event()
self.message_queue = None
self.waiting_for_validation_fixture: Optional[str] = None
self.pending_today_fixture_id: Optional[str] = None
self.pending_today_fixture_id: Optional[str] = None
def _get_today_venue_date(self) -> datetime.date:
"""Get today's date in venue timezone (for day change detection)"""
......@@ -83,6 +85,166 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to check/handle day change: {e}")
return False
def _check_venue_midnight_passed(self) -> bool:
"""Check if venue timezone midnight has passed since the last fixture creation.
Returns True if midnight has passed and new fixture should be created, False otherwise."""
try:
session = self.db_manager.get_session()
try:
# Get the most recent fixture creation time
latest_fixture = session.query(MatchModel).filter(
MatchModel.active_status == True
).order_by(MatchModel.created_at.desc()).first()
if not latest_fixture:
logger.debug("No fixtures found - midnight check not applicable")
return False
# Get the creation time of the latest fixture
latest_fixture_time = latest_fixture.created_at
# Convert to venue timezone
from ..utils.timezone_utils import utc_to_venue_datetime
venue_latest_time = utc_to_venue_datetime(latest_fixture_time, self.db_manager)
# Get current time in venue timezone
from ..utils.timezone_utils import get_current_venue_datetime
current_venue_time = get_current_venue_datetime(self.db_manager)
# Check if we've passed midnight since the last fixture was created
# Compare dates - if the current date is different from the fixture creation date
if current_venue_time.date() > venue_latest_time.date():
logger.info(f"Venue midnight passed! Last fixture created on {venue_latest_time.date()}, current date is {current_venue_time.date()}")
return True
else:
logger.debug(f"Venue midnight not passed. Last fixture: {venue_latest_time.date()}, Current: {current_venue_time.date()}")
return False
finally:
session.close()
except Exception as e:
logger.error(f"Failed to check venue midnight: {e}")
return False
def _is_fixture_from_yesterday(self, fixture_id: str, session) -> bool:
"""Check if the specified fixture has any matches from yesterday"""
try:
if not fixture_id:
return False
# Get today's date in venue timezone
today = self._get_today_venue_date()
# Get all active matches for this fixture
matches = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.active_status == True
).all()
for match in matches:
if match.start_time:
# Convert UTC start_time to venue timezone for date comparison
from ..utils.timezone_utils import utc_to_venue_datetime
venue_start_time = utc_to_venue_datetime(match.start_time, self.db_manager)
match_date = venue_start_time.date()
# Check if the match date is yesterday
if (today - match_date).days == 1:
logger.debug(f"Fixture {fixture_id} has match from yesterday: {match_date}, today: {today}")
return True
logger.debug(f"Fixture {fixture_id} has no matches from yesterday")
return False
except Exception as e:
logger.error(f"Failed to check if fixture {fixture_id} has matches from yesterday: {e}")
return False
def _create_new_fixture_for_continuation(self) -> Optional[str]:
"""Create a new fixture for continuation with new fixture_id and match numbers restarting from 1"""
try:
session = self.db_manager.get_session()
try:
# Create new fixture from match templates with match numbers starting from 1
logger.info("Creating new fixture for continuation from match templates")
# Select random match templates (aim for 5 matches)
template_matches = self._select_random_match_templates(5, session)
if template_matches:
fixture_id = self._create_new_fixture_from_templates_for_continuation(template_matches, session)
if fixture_id:
logger.info(f"Created new fixture {fixture_id} for continuation with {len(template_matches)} matches from templates")
return fixture_id
else:
logger.warning("Failed to create new fixture from templates for continuation")
return None
# If no templates available, try old completed matches
logger.info("No match templates available for continuation, trying to create fixture from old completed matches")
old_matches = self._select_random_completed_matches(5, session)
if old_matches:
fixture_id = self._create_new_fixture_from_old_matches_for_continuation(old_matches, session)
if fixture_id:
logger.info(f"Created new fixture {fixture_id} for continuation with {len(old_matches)} matches from old matches")
return fixture_id
else:
logger.warning("Failed to create new fixture from old matches for continuation")
return None
# No matches available at all
logger.warning("No match templates or old completed matches found for continuation - cannot create new fixture")
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to create new fixture for continuation: {e}")
return None
def _create_new_fixture_at_midnight(self) -> Optional[str]:
"""Create a new fixture at midnight with new fixture_id and match numbers restarting from 1"""
try:
session = self.db_manager.get_session()
try:
# Create new fixture from match templates with match numbers starting from 1
logger.info("Creating new fixture at midnight from match templates")
# Select random match templates (aim for 5 matches)
template_matches = self._select_random_match_templates(5, session)
if template_matches:
fixture_id = self._create_new_fixture_from_templates_at_midnight(template_matches, session)
if fixture_id:
logger.info(f"Created new fixture {fixture_id} at midnight with {len(template_matches)} matches from templates")
return fixture_id
else:
logger.warning("Failed to create new fixture from templates at midnight")
return None
# If no templates available, try old completed matches
logger.info("No match templates available at midnight, trying to create fixture from old completed matches")
old_matches = self._select_random_completed_matches(5, session)
if old_matches:
fixture_id = self._create_new_fixture_from_old_matches_at_midnight(old_matches, session)
if fixture_id:
logger.info(f"Created new fixture {fixture_id} at midnight with {len(old_matches)} matches from old matches")
return fixture_id
else:
logger.warning("Failed to create new fixture from old matches at midnight")
return None
# No matches available at all
logger.warning("No match templates or old completed matches found at midnight - cannot create new fixture")
return None
finally:
session.close()
except Exception as e:
logger.error(f"Failed to create new fixture at midnight: {e}")
return None
def _cleanup_stale_ingame_matches(self):
"""Clean up any stale 'ingame' matches from previous crashed sessions and old 'bet' fixtures"""
try:
......@@ -118,12 +280,16 @@ class GamesThread(ThreadedComponent):
else:
logger.info("No stale ingame matches found")
# PART 2: Clean up ALL old 'bet' fixtures from previous days
# PART 2: Clean up ALL old 'bet' fixtures from previous days (older than yesterday)
# Convert yesterday's date range to UTC for database query
yesterday_start = utc_start - timedelta(days=1)
yesterday_end = utc_end - timedelta(days=1)
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(utc_start, utc_end)
# Exclude today's and yesterday's matches to allow cross-day flow
~MatchModel.start_time.between(yesterday_start, utc_end)
).all()
if old_bet_matches:
......@@ -266,6 +432,26 @@ class GamesThread(ThreadedComponent):
if fixture_matches:
logger.info(f"Fixture {fixture_id} exists with {len(fixture_matches)} matches")
# Check if the fixture is from yesterday
is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)
if is_yesterday_fixture:
# Fixture is from yesterday - create new fixture for today but activate yesterday first
logger.info(f"Fixture {fixture_id} is from yesterday - creating today fixture and activating yesterday to play remaining matches first")
new_fixture_id = self._initialize_new_fixture()
if new_fixture_id:
logger.info(f"Created today fixture {new_fixture_id} - will play yesterday matches first, then switch to today")
# Store the today fixture ID for later use
self.pending_today_fixture_id = new_fixture_id
# Activate yesterday fixture to play remaining matches
self._activate_fixture(fixture_id, message)
return
else:
logger.warning("Could not create today fixture - activating yesterday fixture as fallback")
self._activate_fixture(fixture_id, message)
return
else:
# Fixture is from today - check if it has enough matches
# Check if there are at least 5 non-completed matches
non_completed_count = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
......@@ -281,7 +467,7 @@ class GamesThread(ThreadedComponent):
self._activate_fixture(fixture_id, message)
return
else:
# Not enough matches, create new ones from templates
# Not enough matches - add matches to existing fixture
matches_needed = 5 - non_completed_count
logger.info(f"Fixture {fixture_id} needs {matches_needed} more matches - creating from templates")
......@@ -351,7 +537,26 @@ class GamesThread(ThreadedComponent):
self._send_response(message, "error", "Could not create new fixture")
return
else:
# Some matches are not completed, activate this fixture
# Some matches are not completed, check if fixture is from yesterday
is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)
if is_yesterday_fixture:
# Fixture is from yesterday - activate yesterday fixture first to play remaining matches, create today fixture for later
logger.info(f"Fixture {fixture_id} is from yesterday and has remaining matches - activating yesterday fixture first, will create today fixture after completion")
new_fixture_id = self._initialize_new_fixture()
if new_fixture_id:
# Store the today fixture ID for later use
self.pending_today_fixture_id = new_fixture_id
logger.info(f"Created today fixture {new_fixture_id} - will play yesterday matches first, then switch to today")
# Activate yesterday fixture to play remaining matches
self._activate_fixture(fixture_id, message)
return
else:
logger.warning("Could not create today fixture - activating yesterday fixture as fallback")
self._activate_fixture(fixture_id, message)
return
else:
# Fixture is from today - check if enough matches are available
logger.info(f"Fixture {fixture_id} has active matches - activating")
# Check if at least 5 non-completed matches are available
......@@ -366,7 +571,6 @@ class GamesThread(ThreadedComponent):
if non_completed_count >= 5:
# Enough matches, activate
self._activate_fixture(fixture_id, message)
self._start_async_zip_validation(fixture_id)
return
else:
# Not enough matches, create new ones from templates
......@@ -725,22 +929,65 @@ class GamesThread(ThreadedComponent):
).count()
if active_count == 0:
logger.info(f"All matches completed for fixture {self.current_fixture_id} - checking day change for new fixture creation")
# Check if day has changed - if so, create new fixture with new fixture_id
day_changed = self._check_and_handle_day_change()
if day_changed:
logger.info("Day change detected - creating new fixture with new fixture_id")
new_fixture_id = self._initialize_new_fixture()
logger.info(f"All matches completed for fixture {self.current_fixture_id} - checking for new fixture creation")
# Check if we need to create new matches and if current fixture's first match is from yesterday
need_new_matches = True
current_fixture_is_yesterday = self._is_fixture_from_yesterday(self.current_fixture_id, session)
if need_new_matches:
if self.pending_today_fixture_id or current_fixture_is_yesterday:
# Fixture is from yesterday (either detected now or was pre-detected) - switch fixtures
if self.pending_today_fixture_id:
today_fixture_id = self.pending_today_fixture_id
self.pending_today_fixture_id = None # Clear it
logger.info(f"Yesterday fixture exhausted - switching to pre-created today fixture {today_fixture_id}")
# Switch to the pre-created today fixture
self.current_fixture_id = today_fixture_id
# Start the game with the today fixture
self._activate_fixture(today_fixture_id, Message(
type=MessageType.START_GAME,
sender=self.name,
recipient=self.name,
data={"fixture_id": today_fixture_id, "timestamp": time.time()},
correlation_id=None
))
return
else:
# Fallback: create new fixture if no pre-created one exists
logger.info("Current fixture is from yesterday and needs new matches - creating new fixture with new fixture_id and restarting match numbers from 1")
new_fixture_id = self._create_new_fixture_for_continuation()
if new_fixture_id:
logger.info(f"Created new fixture {new_fixture_id} for new day - switching to it")
logger.info(f"Created new fixture {new_fixture_id} for continuation - switching to new fixture for today's matches")
# Switch to the new fixture for today's matches
self.current_fixture_id = new_fixture_id
# Start the game with the new fixture
self._activate_fixture(new_fixture_id, Message(
type=MessageType.START_GAME,
sender=self.name,
recipient=self.name,
data={"fixture_id": new_fixture_id, "timestamp": time.time()},
correlation_id=None
))
return
else:
logger.warning("Could not create new fixture for new day")
# Fall through to create matches in current fixture
# No day change or failed to create new fixture - create matches in current fixture
logger.warning("Could not create new fixture for continuation - stopping game since current fixture is from yesterday")
# Cannot create new fixture and cannot add to yesterday's fixture - stop the game
self.game_active = False
completed_message = Message(
type=MessageType.GAME_STATUS,
sender=self.name,
data={
"status": "completed_cannot_create_new_fixture",
"fixture_id": self.current_fixture_id,
"timestamp": time.time()
}
)
self.message_bus.publish(completed_message)
self.current_fixture_id = None
return
else:
# Current fixture is from today - can add matches to it
logger.info(f"Creating new matches in current fixture {self.current_fixture_id}")
# First try: Create 5 new matches from match templates
......@@ -884,27 +1131,6 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to check if any ZIPs are being validated: {e}")
return False
def _are_any_zips_being_validated(self) -> bool:
"""Check if any ZIP files are currently being validated system-wide"""
try:
session = self.db_manager.get_session()
try:
# Check if any matches have ZIP validation actively in progress (not just pending)
validating_count = session.query(MatchModel).filter(
MatchModel.zip_validation_status == 'validating',
MatchModel.active_status == True,
MatchModel.zip_filename.isnot(None)
).count()
return validating_count > 0
finally:
session.close()
except Exception as e:
logger.error(f"Failed to check if any ZIPs are being validated: {e}")
return False
def _mark_all_zips_as_validated(self):
"""Mark all ZIP files as validated (used after fixture update completion)"""
try:
......@@ -1473,6 +1699,13 @@ class GamesThread(ThreadedComponent):
logger.info(f"⏰ Starting match timer for fixture {fixture_id}")
self._start_match_timer(fixture_id)
# Only start ZIP validation if not all ZIPs are already validated
if not self._are_all_zips_validated_for_fixture(fixture_id):
logger.info(f"📦 Starting ZIP validation for fixture {fixture_id}")
self._start_async_zip_validation(fixture_id)
else:
logger.info(f"📦 All ZIPs already validated for fixture {fixture_id} - skipping validation")
# Dispatch START_INTRO message
logger.info(f"🎬 Dispatching START_INTRO message for fixture {fixture_id}")
self._dispatch_start_intro(fixture_id)
......@@ -1702,6 +1935,18 @@ class GamesThread(ThreadedComponent):
try:
from .message_bus import MessageBuilder
# Check if the fixture is from yesterday - if so, don't add matches to it
session = self.db_manager.get_session()
try:
is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)
finally:
session.close()
if is_yesterday_fixture:
logger.info(f"🎬 Fixture {fixture_id} is from yesterday - will play remaining matches without adding new ones")
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
logger.info(f"🎬 Yesterday fixture {fixture_id} has {remaining_matches} remaining matches")
else:
# Check if there are at least 5 matches remaining in the fixture
logger.info(f"🎬 Checking minimum match count for fixture {fixture_id} before playing INTRO.mp4")
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id)
......@@ -3550,6 +3795,250 @@ class GamesThread(ThreadedComponent):
session.rollback()
return None
def _create_new_fixture_from_templates_for_continuation(self, template_matches: List[MatchTemplateModel], session) -> Optional[str]:
"""Create a new fixture for continuation with matches copied from match templates, ensuring match numbers start from 1"""
try:
# Generate a unique fixture ID for continuation creation
import uuid
fixture_id = f"continuation_template_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# For continuation fixture creation, always start match_number from 1
match_number = 1
for template_match in template_matches:
# Create a new match based on the template
new_match = MatchModel(
match_number=match_number,
fighter1_township=template_match.fighter1_township,
fighter2_township=template_match.fighter2_township,
venue_kampala_township=template_match.venue_kampala_township,
start_time=now,
status=new_match_status,
fixture_id=fixture_id,
filename=template_match.filename,
file_sha1sum=template_match.file_sha1sum,
active_status=True,
zip_filename=template_match.zip_filename,
zip_sha1sum=template_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
zip_validation_status='valid', # ZIP already validated from template
fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes from template
for template_outcome in template_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=template_outcome.column_name,
float_value=template_outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created match #{match_number} in continuation fixture {fixture_id} from template #{template_match.match_number}")
match_number += 1
session.commit()
logger.info(f"Created continuation fixture {fixture_id} with {len(template_matches)} matches from match templates (status: {new_match_status})")
return fixture_id
except Exception as e:
logger.error(f"Failed to create new fixture from templates for continuation: {e}")
session.rollback()
return None
def _create_new_fixture_from_templates_at_midnight(self, template_matches: List[MatchTemplateModel], session) -> Optional[str]:
"""Create a new fixture at midnight with matches copied from match templates, ensuring match numbers start from 1"""
try:
# Generate a unique fixture ID for midnight creation
import uuid
fixture_id = f"midnight_template_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# For midnight fixture creation, always start match_number from 1
match_number = 1
for template_match in template_matches:
# Create a new match based on the template
new_match = MatchModel(
match_number=match_number,
fighter1_township=template_match.fighter1_township,
fighter2_township=template_match.fighter2_township,
venue_kampala_township=template_match.venue_kampala_township,
start_time=now,
status=new_match_status,
fixture_id=fixture_id,
filename=template_match.filename,
file_sha1sum=template_match.file_sha1sum,
active_status=True,
zip_filename=template_match.zip_filename,
zip_sha1sum=template_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
zip_validation_status='valid', # ZIP already validated from template
fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes from template
for template_outcome in template_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=template_outcome.column_name,
float_value=template_outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created match #{match_number} in midnight fixture {fixture_id} from template #{template_match.match_number}")
match_number += 1
session.commit()
logger.info(f"Created midnight fixture {fixture_id} with {len(template_matches)} matches from match templates (status: {new_match_status})")
return fixture_id
except Exception as e:
logger.error(f"Failed to create new fixture from templates at midnight: {e}")
session.rollback()
return None
def _create_new_fixture_from_old_matches_for_continuation(self, old_matches: List[MatchModel], session) -> Optional[str]:
"""Create a new fixture for continuation with matches copied from old completed matches, ensuring match numbers start from 1"""
try:
# Generate a unique fixture ID for continuation creation
import uuid
fixture_id = f"continuation_recycle_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# For continuation fixture creation, always start match_number from 1
match_number = 1
for old_match in old_matches:
# Create a new match based on the old one
new_match = MatchModel(
match_number=match_number,
fighter1_township=old_match.fighter1_township,
fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township,
start_time=now,
status=new_match_status,
fixture_id=fixture_id,
filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum,
active_status=True,
zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
zip_validation_status='valid', # ZIP already validated from old match
fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes
for outcome in old_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=outcome.column_name,
float_value=outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created match #{match_number} in continuation fixture {fixture_id} from old match #{old_match.match_number}")
match_number += 1
session.commit()
logger.info(f"Created continuation fixture {fixture_id} with {len(old_matches)} matches from old completed matches (status: {new_match_status})")
return fixture_id
except Exception as e:
logger.error(f"Failed to create new fixture from old matches for continuation: {e}")
session.rollback()
return None
def _create_new_fixture_from_old_matches_at_midnight(self, old_matches: List[MatchModel], session) -> Optional[str]:
"""Create a new fixture at midnight with matches copied from old completed matches, ensuring match numbers start from 1"""
try:
# Generate a unique fixture ID for midnight creation
import uuid
fixture_id = f"midnight_recycle_{uuid.uuid4().hex[:8]}"
now = datetime.utcnow()
# Determine the status for new matches based on system state
new_match_status = self._determine_new_match_status(fixture_id, session)
# For midnight fixture creation, always start match_number from 1
match_number = 1
for old_match in old_matches:
# Create a new match based on the old one
new_match = MatchModel(
match_number=match_number,
fighter1_township=old_match.fighter1_township,
fighter2_township=old_match.fighter2_township,
venue_kampala_township=old_match.venue_kampala_township,
start_time=now,
status=new_match_status,
fixture_id=fixture_id,
filename=old_match.filename,
file_sha1sum=old_match.file_sha1sum,
active_status=True,
zip_filename=old_match.zip_filename,
zip_sha1sum=old_match.zip_sha1sum,
zip_upload_status='completed', # Assume ZIP is already available
zip_validation_status='valid', # ZIP already validated from old match
fixture_active_time=int(now.timestamp()),
result=None, # Reset result for new match
end_time=None, # Reset end time for new match
done=False, # Reset done flag for new match
running=False # Reset running flag for new match
)
session.add(new_match)
session.flush() # Get the ID
# Copy match outcomes
for outcome in old_match.outcomes:
new_outcome = MatchOutcomeModel(
match_id=new_match.id,
column_name=outcome.column_name,
float_value=outcome.float_value
)
session.add(new_outcome)
logger.debug(f"Created match #{match_number} in midnight fixture {fixture_id} from old match #{old_match.match_number}")
match_number += 1
session.commit()
logger.info(f"Created midnight fixture {fixture_id} with {len(old_matches)} matches from old completed matches (status: {new_match_status})")
return fixture_id
except Exception as e:
logger.error(f"Failed to create new fixture from old matches at midnight: {e}")
session.rollback()
return None
def _determine_game_status(self) -> str:
"""Determine the current game status for status requests"""
try:
......
......@@ -424,12 +424,17 @@ class MatchTimerComponent(ThreadedComponent):
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id, session)
logger.info(f"Fixture {fixture_id} has {remaining_matches} remaining matches")
if remaining_matches < 5:
# Check if fixture is from yesterday - do not add matches to yesterday fixtures
is_yesterday_fixture = self._is_fixture_from_yesterday(fixture_id, session)
if remaining_matches < 5 and not is_yesterday_fixture:
logger.info(f"Only {remaining_matches} matches remaining (minimum 5 required) - ensuring minimum matches")
self._ensure_minimum_matches_in_fixture(fixture_id, 5 - remaining_matches, session)
# Recount after adding matches
remaining_matches = self._count_remaining_matches_in_fixture(fixture_id, session)
logger.info(f"After ensuring minimum matches, fixture {fixture_id} now has {remaining_matches} remaining matches")
elif remaining_matches < 5 and is_yesterday_fixture:
logger.info(f"Fixture {fixture_id} is from yesterday - not adding matches, proceeding with {remaining_matches} remaining matches")
# Send START_INTRO message
start_intro_message = MessageBuilder.start_intro(
......@@ -520,6 +525,47 @@ class MatchTimerComponent(ThreadedComponent):
logger.error(f"Failed to count remaining matches in fixture {fixture_id}: {e}")
return 0
def _is_fixture_from_yesterday(self, fixture_id: str, session) -> bool:
"""Check if the specified fixture has any matches from yesterday"""
try:
if not fixture_id:
return False
# Get today's date in venue timezone
today = self._get_today_venue_date()
from ..database.models import MatchModel
# Get all active matches for this fixture
matches = session.query(MatchModel).filter(
MatchModel.fixture_id == fixture_id,
MatchModel.active_status == True
).all()
for match in matches:
if match.start_time:
# Convert UTC start_time to venue timezone for date comparison
from ..utils.timezone_utils import utc_to_venue_datetime
venue_start_time = utc_to_venue_datetime(match.start_time, self.db_manager)
match_date = venue_start_time.date()
# Check if the match date is yesterday
if (today - match_date).days == 1:
logger.debug(f"Fixture {fixture_id} has match from yesterday: {match_date}, today: {today}")
return True
logger.debug(f"Fixture {fixture_id} has no matches from yesterday")
return False
except Exception as e:
logger.error(f"Failed to check if fixture {fixture_id} has matches from yesterday: {e}")
return False
def _get_today_venue_date(self) -> datetime.date:
"""Get today's date in venue timezone (for day change detection)"""
from ..utils.timezone_utils import get_today_venue_date
return get_today_venue_date(self.db_manager)
def _ensure_minimum_matches_in_fixture(self, fixture_id: str, minimum_required: int, session):
"""Ensure fixture has at least minimum_required matches by creating new ones from templates or old matches"""
try:
......
......@@ -2826,12 +2826,14 @@ class Migration_038_AddWin1Win2Associations(DatabaseMigration):
'sort_order': sort_order
})
# Add WIN1 associations: X1 and 12 -> WIN1
# Add WIN1 associations: X1 and 12 -> WIN1, and WIN1 -> WIN1
associations = [
('X1', 'WIN1'),
('12', 'WIN1'),
('WIN1', 'WIN1'),
('X2', 'WIN2'),
('12', 'WIN2')
('12', 'WIN2'),
('WIN2', 'WIN2')
]
for outcome_name, extraction_result in associations:
......@@ -2866,8 +2868,10 @@ class Migration_038_AddWin1Win2Associations(DatabaseMigration):
associations = [
('X1', 'WIN1'),
('12', 'WIN1'),
('WIN1', 'WIN1'),
('X2', 'WIN2'),
('12', 'WIN2')
('12', 'WIN2'),
('WIN2', 'WIN2')
]
for outcome_name, extraction_result in associations:
......
......@@ -555,6 +555,10 @@ class MatchModel(BaseModel):
def get_outcomes_dict(self) -> Dict[str, float]:
"""Get match outcomes as a dictionary"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return empty dict to avoid lazy loading error
return {}
return {outcome.column_name: outcome.float_value for outcome in self.outcomes}
def add_outcome(self, column_name: str, float_value: float):
......@@ -575,6 +579,10 @@ class MatchModel(BaseModel):
"""Convert to dictionary with outcomes"""
result = super().to_dict(exclude_fields)
result['outcomes'] = self.get_outcomes_dict()
from sqlalchemy import inspect
if inspect(self).session is None:
result['outcome_count'] = 0
else:
result['outcome_count'] = len(self.outcomes)
return result
......@@ -699,22 +707,43 @@ class BetModel(BaseModel):
def get_total_amount(self) -> float:
"""Get total amount of all bet details"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return 0 to avoid lazy loading error
return 0.0
return sum(detail.amount for detail in self.bet_details)
def get_bet_count(self) -> int:
"""Get number of bet details"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return 0 to avoid lazy loading error
return 0
return len(self.bet_details)
def has_pending_bets(self) -> bool:
"""Check if bet has any pending bet details"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return False to avoid lazy loading error
return False
return any(detail.result == 'pending' for detail in self.bet_details)
def calculate_total_winnings(self) -> float:
"""Calculate total winnings from won bets"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return 0.0 to avoid lazy loading error
return 0.0
return sum(detail.win_amount for detail in self.bet_details if detail.result == 'win')
def get_overall_status(self) -> str:
"""Get overall bet status based on bet details"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return 'pending' to avoid lazy loading error
return 'pending'
if not self.bet_details:
return 'pending'
......@@ -747,6 +776,16 @@ class BetModel(BaseModel):
def to_dict(self, exclude_fields: Optional[List[str]] = None) -> Dict[str, Any]:
"""Convert to dictionary with bet details"""
result = super().to_dict(exclude_fields)
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, avoid lazy loading
result['bet_details'] = []
result['total_amount'] = 0.0
result['bet_count'] = 0
result['has_pending'] = False
result['overall_status'] = 'pending'
result['total_winnings'] = 0.0
else:
result['bet_details'] = [detail.to_dict() for detail in self.bet_details]
result['total_amount'] = self.get_total_amount()
result['bet_count'] = self.get_bet_count()
......@@ -1062,6 +1101,10 @@ class MatchTemplateModel(BaseModel):
def get_outcomes_dict(self) -> Dict[str, float]:
"""Get match outcomes as a dictionary"""
from sqlalchemy import inspect
if inspect(self).session is None:
# Object is not bound to a session, return empty dict to avoid lazy loading error
return {}
return {outcome.column_name: outcome.float_value for outcome in self.outcomes}
def add_outcome(self, column_name: str, float_value: float):
......@@ -1082,6 +1125,10 @@ class MatchTemplateModel(BaseModel):
"""Convert to dictionary with outcomes"""
result = super().to_dict(exclude_fields)
result['outcomes'] = self.get_outcomes_dict()
from sqlalchemy import inspect
if inspect(self).session is None:
result['outcome_count'] = 0
else:
result['outcome_count'] = len(self.outcomes)
return result
......
......@@ -422,10 +422,10 @@ class OverlayWebChannel(QObject):
return None
def _get_fixture_data_from_database(self) -> Optional[List[Dict[str, Any]]]:
"""Get fixture data directly from database"""
"""Get fixture data directly from database, showing both today and yesterday fixtures until yesterday's are complete"""
try:
from ..database.models import MatchModel, MatchOutcomeModel
from datetime import datetime
from datetime import datetime, timedelta
# Use the database manager passed to this channel
if not self.db_manager:
......@@ -435,24 +435,55 @@ class OverlayWebChannel(QObject):
session = self.db_manager.get_session()
try:
# Get today's date in UTC (consistent with database storage)
today = datetime.utcnow().date()
# Get today's date in venue timezone for proper date handling
from ..utils.timezone_utils import get_today_venue_date
today_venue = get_today_venue_date(self.db_manager)
yesterday_venue = today_venue - timedelta(days=1)
# Convert venue dates to UTC datetime ranges for database queries
from ..utils.timezone_utils import venue_to_utc_datetime
today_start_utc = venue_to_utc_datetime(datetime.combine(today_venue, datetime.min.time()), self.db_manager)
today_end_utc = venue_to_utc_datetime(datetime.combine(today_venue, datetime.max.time()), self.db_manager)
yesterday_start_utc = venue_to_utc_datetime(datetime.combine(yesterday_venue, datetime.min.time()), self.db_manager)
yesterday_end_utc = venue_to_utc_datetime(datetime.combine(yesterday_venue, datetime.max.time()), self.db_manager)
# Get active matches for today (non-terminal states)
active_matches = session.query(MatchModel).filter(
today_matches = session.query(MatchModel).filter(
MatchModel.start_time.isnot(None),
MatchModel.start_time >= today_start_utc,
MatchModel.start_time < today_end_utc,
MatchModel.status.notin_(['done', 'end', 'cancelled', 'failed', 'paused']),
MatchModel.active_status == True
).order_by(MatchModel.start_time.asc()).all()
# Check if there are any incomplete matches from yesterday
yesterday_incomplete_matches = session.query(MatchModel).filter(
MatchModel.start_time.isnot(None),
MatchModel.start_time >= datetime.combine(today, datetime.min.time()),
MatchModel.start_time < datetime.combine(today, datetime.max.time()),
MatchModel.start_time >= yesterday_start_utc,
MatchModel.start_time < yesterday_end_utc,
MatchModel.status.notin_(['done', 'end', 'cancelled', 'failed', 'paused']),
MatchModel.active_status == True
).order_by(MatchModel.start_time.asc()).limit(5).all()
).order_by(MatchModel.start_time.asc()).all()
# Combine matches: yesterday first (if any incomplete), then today
all_matches = []
if yesterday_incomplete_matches:
logger.debug(f"Found {len(yesterday_incomplete_matches)} incomplete matches from yesterday - including them")
all_matches.extend(yesterday_incomplete_matches)
else:
logger.debug("No incomplete matches from yesterday found")
all_matches.extend(today_matches)
# Limit to 5 matches total
display_matches = all_matches[:5]
if not active_matches:
logger.debug("No active matches found")
if not display_matches:
logger.debug("No active matches found for today or yesterday")
return []
fixture_data = []
for match in active_matches:
for match in display_matches:
# Get outcomes for this match
outcomes = session.query(MatchOutcomeModel).filter(
MatchOutcomeModel.match_id == match.id
......@@ -473,7 +504,7 @@ class OverlayWebChannel(QObject):
}
fixture_data.append(match_data)
logger.debug(f"Retrieved {len(fixture_data)} matches from database")
logger.debug(f"Retrieved {len(fixture_data)} matches from database (yesterday: {len(yesterday_incomplete_matches)}, today: {len(today_matches)})")
return fixture_data
finally:
......
......@@ -378,7 +378,7 @@
<div class="overlay-container">
<div class="fixtures-panel" id="fixturesPanel">
<div class="fixtures-title">Next 5 matches:</div>
<div class="fixtures-title" id="fixturesTitle">next 5 matches:</div>
<div class="next-match-info" id="nextMatchInfo" style="display: none;"></div>
<div class="countdown-timer" id="countdownTimer" style="display: none;"></div>
<div class="loading-message" id="loadingMessage" style="display: none;">Loading fixture data...</div>
......@@ -944,6 +944,44 @@
countdownTimer.style.display = 'block';
}
// Update fixtures title based on content
function updateFixturesTitle(fixturesData) {
const fixturesTitle = document.getElementById('fixturesTitle');
if (!fixturesTitle) return;
// Check if we have yesterday's matches (incomplete from previous day)
const now = new Date();
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
let hasYesterdayMatches = false;
let hasTodayMatches = false;
fixturesData.forEach(match => {
if (match.start_time) {
const matchDate = new Date(match.start_time);
const matchDay = new Date(matchDate.getFullYear(), matchDate.getMonth(), matchDate.getDate());
if (matchDay.getTime() === yesterday.getTime()) {
hasYesterdayMatches = true;
} else if (matchDay.getTime() === today.getTime()) {
hasTodayMatches = true;
}
}
});
if (hasYesterdayMatches && hasTodayMatches) {
fixturesTitle.textContent = 'Today & Yesterday matches:';
} else if (hasYesterdayMatches) {
fixturesTitle.textContent = 'Yesterday matches:';
} else {
fixturesTitle.textContent = 'Today matches:';
}
console.log(`Updated fixtures title: ${fixturesTitle.textContent} (yesterday: ${hasYesterdayMatches}, today: ${hasTodayMatches})`);
}
// Render the fixtures table
function renderFixtures() {
debugTime('Starting renderFixtures function');
......@@ -953,6 +991,7 @@
const noMatches = document.getElementById('noMatches');
const tableHeader = document.getElementById('tableHeader');
const tableBody = document.getElementById('tableBody');
const fixturesTitle = document.getElementById('fixturesTitle');
loadingMessage.style.display = 'none';
noMatches.style.display = 'none';
......@@ -960,7 +999,7 @@
if (!fixturesData || fixturesData.length === 0) {
debugTime('No fixtures data available');
showNoMatches('No matches available for today');
showNoMatches('No matches available');
return;
}
......
......@@ -176,3 +176,13 @@ def venue_to_utc_datetime(venue_dt: datetime, db_manager) -> datetime:
else:
venue_dt = venue_dt.replace(tzinfo=venue_tz)
return venue_dt.astimezone(timezone.utc)
def get_current_venue_datetime(db_manager) -> datetime:
"""Get current datetime in venue timezone"""
venue_tz = get_venue_timezone(db_manager)
if HAS_PYTZ:
return datetime.now(venue_tz)
else:
# Without pytz, assume venue_tz is already a timezone object
return datetime.now(venue_tz)
\ No newline at end of file
......@@ -584,6 +584,51 @@ def fixture_details(fixture_id):
flash("Error loading fixture details", "error")
return render_template('errors/500.html'), 500
@main_bp.route('/matches-templates/<int:template_id>')
@login_required
def matches_template_details(template_id):
"""Match template details page for editing odds"""
try:
# Restrict cashier users from accessing template details
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/matches_template_details.html',
user=current_user,
template_id=template_id,
page_title=f"Template Details - Template #{template_id}")
except Exception as e:
logger.error(f"Match template details page error: {e}")
flash("Error loading template details", "error")
return render_template('errors/500.html'), 500
@main_bp.route('/templates-details')
@login_required
def templates_details():
"""Templates details page listing all template matches"""
try:
# Restrict cashier users from accessing templates details
if hasattr(current_user, 'role') and current_user.role == 'cashier':
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
elif hasattr(current_user, 'is_cashier_user') and current_user.is_cashier_user():
flash("Access denied", "error")
return redirect(url_for('main.cashier_dashboard'))
return render_template('dashboard/templates_details.html',
user=current_user,
page_title="Fixture Template Details")
except Exception as e:
logger.error(f"Templates details page error: {e}")
flash("Error loading templates details", "error")
return render_template('errors/500.html'), 500
@main_bp.route('/matches/<int:match_id>/<fixture_id>')
@login_required
......@@ -2669,6 +2714,155 @@ def reset_fixtures():
return jsonify({"error": str(e)}), 500
@api_bp.route('/matches-templates')
@get_api_auth_decorator()
def get_matches_templates():
"""Get matches_templates as a single fixture template"""
try:
from ..database.models import MatchTemplateModel, MatchOutcomeTemplateModel
from collections import defaultdict
session = api_bp.db_manager.get_session()
try:
# Get all match templates
templates = session.query(MatchTemplateModel).order_by(MatchTemplateModel.match_number.asc()).all()
if not templates:
return jsonify({
"success": True,
"templates": [],
"total": 0
})
# Use the first template to get fixture-level info
first_template = templates[0]
# Create a single fixture template that contains all matches
fixture_template_data = {
'id': first_template.id, # Use first template's ID for compatibility
'fixture_id': 'template_fixture', # Fixed fixture ID for templates
'match_number': 1, # Single fixture representation
'fighter1_township': first_template.fighter1_township, # Use first match's fighter
'fighter2_township': first_template.fighter2_township, # Use first match's fighter
'venue_kampala_township': first_template.venue_kampala_township, # Use first match's venue
'start_time': None, # No start time for templates
'created_at': first_template.created_at.isoformat(),
'fixture_status': 'template', # Special status for templates
'match_count': len(templates),
'matches': [template.to_dict() for template in templates] # Include all matches
}
return jsonify({
"success": True,
"templates": [fixture_template_data],
"total": 1
})
finally:
session.close()
except Exception as e:
logger.error(f"API get matches templates error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/matches-templates/<int:template_id>')
@get_api_auth_decorator()
def get_matches_template_details(template_id):
"""Get details for a specific match template with its outcomes"""
try:
from ..database.models import MatchTemplateModel, MatchOutcomeTemplateModel
session = api_bp.db_manager.get_session()
try:
# Get the template
template = session.query(MatchTemplateModel).filter_by(id=template_id).first()
if not template:
return jsonify({"error": "Template not found"}), 404
# Get outcomes for this template
outcomes = session.query(MatchOutcomeTemplateModel).filter_by(match_id=template_id).all()
template_data = template.to_dict()
# Map outcomes to include 'odds' field (from float_value)
outcomes_data = []
for outcome in outcomes:
outcome_dict = outcome.to_dict()
# Add 'odds' field for frontend compatibility
outcome_dict['odds'] = outcome_dict.get('float_value', 0.0)
outcomes_data.append(outcome_dict)
template_data['outcomes'] = outcomes_data
template_data['outcome_count'] = len(outcomes)
return jsonify({
"success": True,
"template": template_data
})
finally:
session.close()
except Exception as e:
logger.error(f"API get matches template details error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/matches-templates/<int:template_id>/odds', methods=['POST'])
@get_api_auth_decorator()
def update_matches_template_odds(template_id):
"""Update odds for a match template"""
try:
from ..database.models import MatchTemplateModel, MatchOutcomeTemplateModel
data = request.get_json() or {}
odds_updates = data.get('odds', [])
if not odds_updates:
return jsonify({"error": "Odds updates are required"}), 400
session = api_bp.db_manager.get_session()
try:
# Verify template exists
template = session.query(MatchTemplateModel).filter_by(id=template_id).first()
if not template:
return jsonify({"error": "Template not found"}), 404
# Update odds for each outcome
updated_count = 0
for update in odds_updates:
outcome_id = update.get('outcome_id')
new_odds = update.get('odds')
if outcome_id is None or new_odds is None:
continue
# Find and update the outcome
outcome = session.query(MatchOutcomeTemplateModel).filter_by(id=outcome_id).first()
if outcome and outcome.match_id == template_id:
outcome.odds = float(new_odds)
updated_count += 1
session.commit()
logger.info(f"Updated {updated_count} odds for template {template_id}")
return jsonify({
"success": True,
"message": f"Updated {updated_count} odds successfully",
"updated_count": updated_count
})
finally:
session.close()
except Exception as e:
logger.error(f"API update matches template odds error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/api-client/status')
@get_api_auth_decorator()
def get_api_client_status():
......@@ -4462,9 +4656,52 @@ def get_cashier_bets():
bet_data = bet.to_dict()
bet_data['paid'] = bet.paid # Include paid status
# Get bet details
# Get bet details with match information
bet_details = session.query(BetDetailModel).filter_by(bet_id=bet.uuid).all()
bet_data['details'] = [detail.to_dict() for detail in bet_details]
bet_details_data = []
for detail in bet_details:
# Get match information
match = session.query(MatchModel).filter_by(id=detail.match_id).first()
# Parse winning_outcomes JSON string if it exists
winning_outcomes = []
if match and match.winning_outcomes:
try:
if isinstance(match.winning_outcomes, str):
winning_outcomes = json.loads(match.winning_outcomes)
elif isinstance(match.winning_outcomes, list):
winning_outcomes = match.winning_outcomes
except (json.JSONDecodeError, TypeError):
winning_outcomes = []
# Get odds for this outcome
odds = 0.0
if match:
outcomes_dict = match.get_outcomes_dict()
odds = outcomes_dict.get(detail.outcome, 0.0)
detail_dict = {
'id': detail.id,
'match_id': detail.match_id,
'outcome': detail.outcome,
'amount': float(detail.amount),
'odds': float(odds),
'potential_winning': float(detail.amount) * float(odds),
'result': detail.result,
'match': {
'match_number': match.match_number if match else 'Unknown',
'fighter1_township': match.fighter1_township if match else 'Unknown',
'fighter2_township': match.fighter2_township if match else 'Unknown',
'venue_kampala_township': match.venue_kampala_township if match else 'Unknown',
'status': match.status if match else 'Unknown',
'winning_outcomes': winning_outcomes
} if match else None
}
bet_details_data.append(detail_dict)
bet_data['details'] = bet_details_data
# Calculate total amount for this bet
bet_total = sum(float(detail.amount) for detail in bet_details)
......@@ -4930,42 +5167,63 @@ def get_available_matches_for_betting():
"""Get matches that are available for betting (status = 'bet') with actual match outcomes"""
try:
from ..database.models import MatchModel, MatchOutcomeModel
from datetime import datetime, date
from datetime import datetime, date, timedelta
session = api_bp.db_manager.get_session()
try:
# Get today's date
# Get today's and yesterday's dates
today = date.today()
yesterday = today - timedelta(days=1)
# Get matches in 'bet' status from today (or all if none today)
# Get matches in 'bet' status
matches_query = session.query(MatchModel).filter(
MatchModel.status == 'bet'
).order_by(MatchModel.match_number.asc())
# Try to filter by today first using venue timezone
# Get venue timezone for date filtering
venue_tz = get_venue_timezone(api_bp.db_manager)
local_start = datetime.combine(today, datetime.min.time())
local_end = datetime.combine(today, datetime.max.time())
# Convert venue local time to UTC for database queries
start_datetime = venue_to_utc_datetime(local_start, api_bp.db_manager)
end_datetime = venue_to_utc_datetime(local_end, api_bp.db_manager)
# Get today's matches
local_start_today = datetime.combine(today, datetime.min.time())
local_end_today = datetime.combine(today, datetime.max.time())
start_datetime_today = venue_to_utc_datetime(local_start_today, api_bp.db_manager)
end_datetime_today = venue_to_utc_datetime(local_end_today, api_bp.db_manager)
today_matches = matches_query.filter(
MatchModel.start_time >= start_datetime,
MatchModel.start_time < end_datetime
MatchModel.start_time >= start_datetime_today,
MatchModel.start_time < end_datetime_today
).all()
if today_matches:
matches = today_matches
else:
# Fallback to all matches in bet status
# Get yesterday's matches
local_start_yesterday = datetime.combine(yesterday, datetime.min.time())
local_end_yesterday = datetime.combine(yesterday, datetime.max.time())
start_datetime_yesterday = venue_to_utc_datetime(local_start_yesterday, api_bp.db_manager)
end_datetime_yesterday = venue_to_utc_datetime(local_end_yesterday, api_bp.db_manager)
yesterday_matches = matches_query.filter(
MatchModel.start_time >= start_datetime_yesterday,
MatchModel.start_time < end_datetime_yesterday
).all()
# Combine today's and yesterday's matches (yesterday first, then today)
matches = yesterday_matches + today_matches
# Remove duplicates (in case some matches span the date boundary)
seen_ids = set()
unique_matches = []
for match in matches:
if match.id not in seen_ids:
seen_ids.add(match.id)
unique_matches.append(match)
matches = unique_matches
# If no matches from today or yesterday, fallback to all matches in bet status
if not matches:
matches = matches_query.all()
matches_data = []
for match in matches:
match_data = match.to_dict()
# Get actual match outcomes from the database
match_outcomes = session.query(MatchOutcomeModel).filter_by(match_id=match.id).all()
......@@ -5008,8 +5266,37 @@ def get_available_matches_for_betting():
{'outcome_id': None, 'outcome_name': 'WIN2', 'outcome_value': None, 'display_name': 'WIN2'}
]
match_data['outcomes'] = betting_outcomes
match_data['outcomes_count'] = len(betting_outcomes)
# Build match data manually to avoid lazy loading issues
match_data = {
'id': match.id,
'match_number': match.match_number,
'fighter1_township': match.fighter1_township,
'fighter2_township': match.fighter2_township,
'venue_kampala_township': match.venue_kampala_township,
'start_time': match.start_time.isoformat() if match.start_time else None,
'end_time': match.end_time.isoformat() if match.end_time else None,
'result': match.result,
'winning_outcomes': match.winning_outcomes,
'under_over_result': match.under_over_result,
'done': match.done,
'running': match.running,
'status': match.status,
'fixture_active_time': match.fixture_active_time,
'filename': match.filename,
'file_sha1sum': match.file_sha1sum,
'fixture_id': match.fixture_id,
'active_status': match.active_status,
'zip_filename': match.zip_filename,
'zip_sha1sum': match.zip_sha1sum,
'zip_upload_status': match.zip_upload_status,
'zip_upload_progress': match.zip_upload_progress,
'zip_validation_status': match.zip_validation_status,
'created_by': match.created_by,
'created_at': match.created_at.isoformat() if match.created_at else None,
'updated_at': match.updated_at.isoformat() if match.updated_at else None,
'outcomes': betting_outcomes,
'outcomes_count': len(betting_outcomes)
}
matches_data.append(match_data)
return jsonify({
......@@ -5866,12 +6153,24 @@ def get_match_details():
odds = outcomes_dict.get(detail.outcome, 0.0)
potential_winning = float(detail.amount) * float(odds)
# Determine actual result status using is_bet_detail_winning function
# This checks if bet detail is winning based on match's winning outcomes
actual_result = detail.result
if is_bet_detail_winning(detail, match, session):
actual_result = 'won'
elif detail.result == 'lost':
actual_result = 'lost'
elif detail.result == 'cancelled':
actual_result = 'cancelled'
else:
actual_result = 'pending'
bet_data = {
'bet_id': detail.bet_id,
'bet_datetime': bet.bet_datetime.isoformat() if bet else None,
'outcome': detail.outcome,
'amount': float(detail.amount),
'result': detail.result,
'result': actual_result,
'potential_winning': potential_winning
}
bets_data.append(bet_data)
......
......@@ -535,6 +535,9 @@ function updateBetsTable(data, container) {
// Collect unique match numbers
const matchNumbers = [...new Set(bet.details ? bet.details.map(detail => detail.match ? detail.match.match_number : 'Unknown').filter(n => n !== 'Unknown') : [])];
// Collect outcomes for display
const outcomes = bet.details ? bet.details.map(detail => detail.outcome).join(', ') : 'N/A';
// Determine overall bet status based on details
let overallStatus = 'pending';
let statusBadge = '';
......@@ -567,7 +570,7 @@ function updateBetsTable(data, container) {
if (overallStatus === 'won' && bet.details) {
payoutAmount = bet.details
.filter(detail => detail.result === 'win')
.reduce((sum, detail) => sum + parseFloat(detail.win_amount || 0), 0);
.reduce((sum, detail) => sum + parseFloat(detail.potential_winning || 0), 0);
}
const payoutDisplay = payoutAmount > 0 ? formatCurrency(payoutAmount.toFixed(2)) : '-';
......@@ -576,7 +579,7 @@ function updateBetsTable(data, container) {
<td><strong>${bet.uuid.substring(0, 8)}...</strong></td>
<td>${bet.barcode_data ? bet.barcode_data.substring(0, 16) + '...' : 'N/A'}</td>
<td>${betDateTime}</td>
<td>${bet.details ? bet.details.length : 0} selections</td>
<td>${outcomes}</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
<td><strong class="currency-amount" data-amount="${totalAmount}">${formatCurrency(totalAmount)}</strong></td>
<td><strong class="currency-amount" data-amount="${payoutAmount}">${payoutDisplay}</strong></td>
......
......@@ -533,7 +533,10 @@ function updateBetsTable(data, container) {
const totalAmount = parseFloat(bet.total_amount).toFixed(2);
// Collect unique match numbers
const matchNumbers = [...new Set(bet.details ? bet.details.map(detail => detail.match_number || 'Unknown').filter(n => n !== 'Unknown') : [])];
const matchNumbers = [...new Set(bet.details ? bet.details.map(detail => detail.match ? detail.match.match_number : 'Unknown').filter(n => n !== 'Unknown') : [])];
// Collect outcomes for display
const outcomes = bet.details ? bet.details.map(detail => detail.outcome).join(', ') : 'N/A';
// Determine overall bet status based on details
let overallStatus = 'pending';
......@@ -567,7 +570,7 @@ function updateBetsTable(data, container) {
if (overallStatus === 'won' && bet.details) {
payoutAmount = bet.details
.filter(detail => detail.result === 'win')
.reduce((sum, detail) => sum + parseFloat(detail.win_amount || 0), 0);
.reduce((sum, detail) => sum + parseFloat(detail.potential_winning || 0), 0);
}
const payoutDisplay = payoutAmount > 0 ? formatCurrency(payoutAmount.toFixed(2)) : '-';
......@@ -576,7 +579,7 @@ function updateBetsTable(data, container) {
<td><strong>${bet.uuid.substring(0, 8)}...</strong></td>
<td>${bet.barcode_data ? bet.barcode_data.substring(0, 16) + '...' : 'N/A'}</td>
<td>${betDateTime}</td>
<td>${bet.details ? bet.details.length : 0} selections</td>
<td>${outcomes}</td>
<td>${matchNumbers.length > 0 ? matchNumbers.join(', ') : 'N/A'}</td>
<td><strong class="currency-amount" data-amount="${totalAmount}">${formatCurrency(totalAmount)}</strong></td>
<td><strong class="currency-amount" data-amount="${payoutAmount}">${payoutDisplay}</strong></td>
......
......@@ -20,42 +20,6 @@
{% endif %}
</div>
<!-- Filters and Search -->
<div class="card mb-4">
<div class="card-header">
<h5>Filter Options</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label for="upload-filter" class="form-label">Upload Status</label>
<select class="form-select" id="upload-filter">
<option value="">All Uploads</option>
<option value="pending">Pending</option>
<option value="uploading">Uploading</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="col-md-3">
<label for="past-filter" class="form-label">Past Fixtures</label>
<select class="form-select" id="past-filter">
<option value="hide">Hide Past</option>
<option value="show">Show Past</option>
</select>
</div>
<div class="col-md-4">
<label for="search-input" class="form-label">Search Fighters</label>
<input type="text" class="form-control" id="search-input" placeholder="Search by fighter names or venue">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary" id="refresh-btn">
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
</div>
</div>
</div>
</div>
<!-- Loading Spinner -->
<div id="loading" class="text-center my-4" style="display: none;">
......@@ -109,6 +73,43 @@
</div>
</div>
<!-- Filters and Search -->
<div class="card mb-4">
<div class="card-header">
<h5>Filter Options</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-3">
<label for="upload-filter" class="form-label">Upload Status</label>
<select class="form-select" id="upload-filter">
<option value="">All Uploads</option>
<option value="pending">Pending</option>
<option value="uploading">Uploading</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
</select>
</div>
<div class="col-md-3">
<label for="past-filter" class="form-label">Past Fixtures</label>
<select class="form-select" id="past-filter">
<option value="hide">Hide Past</option>
<option value="show">Show Past</option>
</select>
</div>
<div class="col-md-4">
<label for="search-input" class="form-label">Search Fighters</label>
<input type="text" class="form-control" id="search-input" placeholder="Search by fighter names or venue">
</div>
<div class="col-md-2 d-flex align-items-end">
<button class="btn btn-primary" id="refresh-btn">
<i class="fas fa-sync-alt me-1"></i>Refresh
</button>
</div>
</div>
</div>
</div>
<!-- Fixtures Table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
......@@ -121,6 +122,7 @@
<thead class="table-light">
<tr>
<th>Fixture #</th>
<th>Date</th>
<th>Fighters</th>
<th>Venue</th>
<th>Status</th>
......@@ -138,6 +140,65 @@
</div>
</div>
<!-- Fixture Template Section (Admin and Users only) -->
{% if current_user.is_admin or (hasattr(current_user, 'role') and current_user.role != 'cashier') %}
<div class="row mt-5">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3">
<div>
<h2><i class="fas fa-template-icon me-2"></i>Fixture template</h2>
<p class="mb-0">Manage reusable fixture configurations with customizable odds.</p>
</div>
</div>
<!-- Loading Spinner for Templates -->
<div id="templates-loading" class="text-center my-4" style="display: none;">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading match templates...</p>
</div>
<!-- Fixture Template Table -->
<div class="card">
<div class="card-header d-flex justify-content-between align-items-center">
<h5>Fixture Template</h5>
<span id="templates-filtered-count" class="badge bg-secondary">0 template</span>
</div>
<div class="card-body p-0" style="padding-bottom: 100px !important;">
<div class="table-responsive">
<table class="table table-hover mb-0" id="templates-table">
<thead class="table-light">
<tr>
<th>Fixture #</th>
<th>Date</th>
<th>Fighters</th>
<th>Venue</th>
<th>Status</th>
<th>Upload Status</th>
<th>Start Time</th>
<th>Created</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="templates-tbody">
<!-- Template content will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
<!-- Empty State for Templates -->
<div id="templates-empty-state" class="text-center my-5" style="display: none;">
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
<h4 class="text-muted">No Match Templates Found</h4>
<p class="text-muted">No match templates available. Templates are created from synchronized fixtures.</p>
</div>
</div>
</div>
{% endif %}
<!-- Empty State -->
<div id="empty-state" class="text-center my-5" style="display: none;">
<i class="fas fa-inbox fa-4x text-muted mb-3"></i>
......@@ -150,13 +211,18 @@
<script>
let allFixtures = [];
let allTemplates = [];
// Load fixtures on page load
// Load fixtures and templates on page load
document.addEventListener('DOMContentLoaded', function() {
loadFixtures();
loadTemplates();
// Event listeners
document.getElementById('refresh-btn').addEventListener('click', loadFixtures);
document.getElementById('refresh-btn').addEventListener('click', function() {
loadFixtures();
loadTemplates();
});
document.getElementById('upload-filter').addEventListener('change', filterFixtures);
document.getElementById('past-filter').addEventListener('change', filterFixtures);
document.getElementById('search-input').addEventListener('input', filterFixtures);
......@@ -168,7 +234,10 @@ document.addEventListener('DOMContentLoaded', function() {
}
// Auto-refresh fixtures every 15 seconds
setInterval(loadFixtures, 15000);
setInterval(function() {
loadFixtures();
loadTemplates();
}, 15000);
});
let isInitialLoad = true;
......@@ -366,12 +435,38 @@ function renderFixturesTable(fixtures) {
processedFixtures.add(fixtureId);
const startTimeDisplay = fixture.start_time ? new Date(fixture.start_time).toLocaleString() : 'Not set';
const fixtureDate = fixture.start_time ? new Date(fixture.start_time).toLocaleDateString() : 'Not set';
// Determine if this is yesterday, today, or other date
let dateBadge = '';
if (fixture.start_time) {
const matchDate = new Date(fixture.start_time);
const today = new Date();
const yesterday = new Date(today);
yesterday.setDate(yesterday.getDate() - 1);
const matchDay = new Date(matchDate.getFullYear(), matchDate.getMonth(), matchDate.getDate());
const todayDay = new Date(today.getFullYear(), today.getMonth(), today.getDate());
const yesterdayDay = new Date(yesterday.getFullYear(), yesterday.getMonth(), yesterday.getDate());
if (matchDay.getTime() === yesterdayDay.getTime()) {
dateBadge = '<span class="badge bg-warning">Yesterday</span>';
} else if (matchDay.getTime() === todayDay.getTime()) {
dateBadge = '<span class="badge bg-success">Today</span>';
} else {
dateBadge = '<span class="badge bg-secondary">' + fixtureDate + '</span>';
}
} else {
dateBadge = '<span class="badge bg-secondary">Not set</span>';
}
const newRowHTML = `
<td>
<strong>#` + fixture.match_number + `</strong>
<br>
<small class="text-muted">` + fixture.match_count + ` matches</small>
</td>
<td>` + dateBadge + `</td>
<td>
<div class="fw-bold">` + fixture.fighter1_township + `</div>
<small class="text-muted">vs</small>
......@@ -474,6 +569,160 @@ function getUploadStatusBadge(fixture) {
}
}
function loadTemplates() {
const loading = document.getElementById('templates-loading');
const refreshBtn = document.getElementById('refresh-btn');
// Only show loading spinner on initial load or manual refresh
if (isInitialLoad || refreshBtn.disabled) {
loading.style.display = 'block';
}
fetch('/api/matches-templates')
.then(response => response.json())
.then(data => {
if (data.success) {
allTemplates = data.templates;
renderTemplatesTable(allTemplates);
} else {
if (isInitialLoad) {
console.error('Error loading templates: ' + (data.error || 'Unknown error'));
}
}
})
.catch(error => {
console.error('Error:', error);
if (isInitialLoad) {
console.error('Failed to load templates: ' + error.message);
}
})
.finally(() => {
loading.style.display = 'none';
});
}
function renderTemplatesTable(templates) {
const tbody = document.getElementById('templates-tbody');
const filteredCount = document.getElementById('templates-filtered-count');
const emptyState = document.getElementById('templates-empty-state');
const templatesTable = document.querySelector('#templates-table').parentElement.parentElement;
filteredCount.textContent = templates.length + ' template';
// Show/hide empty state
if (templates.length === 0) {
emptyState.style.display = 'block';
templatesTable.style.display = 'none';
return;
} else {
emptyState.style.display = 'none';
templatesTable.style.display = 'block';
}
// Create a map of existing templates for comparison
const existingRows = Array.from(tbody.children);
const existingTemplates = new Map();
existingRows.forEach(row => {
const templateId = row.getAttribute('data-template-id');
if (templateId) {
existingTemplates.set(templateId, row);
}
});
// Track which templates we've processed
const processedTemplates = new Set();
templates.forEach((template, index) => {
const templateId = template.fixture_id.toString(); // Use fixture_id as identifier
processedTemplates.add(templateId);
// For templates, show creation date as the date
const fixtureDate = new Date(template.created_at).toLocaleDateString();
const dateBadge = '<span class="badge bg-secondary">' + fixtureDate + '</span>';
const newRowHTML = `
<td>
<strong>#` + template.match_number + `</strong>
<br>
<small class="text-muted">` + template.match_count + ` matches</small>
</td>
<td>` + dateBadge + `</td>
<td>
<div class="fw-bold">` + template.fighter1_township + `</div>
<small class="text-muted">vs</small>
<div class="fw-bold">` + template.fighter2_township + `</div>
</td>
<td>` + template.venue_kampala_township + `</td>
<td>` + getTemplateStatusBadge(template) + `</td>
<td><span class="badge bg-secondary"><i class="fas fa-clock me-1"></i>N/A</span></td>
<td>
<small class="text-info">Not scheduled</small>
</td>
<td>
<small class="text-muted">
` + new Date(template.created_at).toLocaleString() + `
</small>
</td>
<td>
<a href="/templates-details" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i>Details
</a>
</td>
`;
const existingRow = existingTemplates.get(templateId);
if (existingRow) {
// Update existing row only if content has changed
if (existingRow.innerHTML !== newRowHTML) {
existingRow.innerHTML = newRowHTML;
existingRow.style.backgroundColor = '#fff3cd'; // Highlight changed row briefly
setTimeout(() => {
existingRow.style.backgroundColor = '';
}, 1000);
}
} else {
// Add new row
const row = document.createElement('tr');
row.setAttribute('data-template-id', templateId);
row.innerHTML = newRowHTML;
row.style.backgroundColor = '#d4edda'; // Highlight new row briefly
tbody.appendChild(row);
setTimeout(() => {
row.style.backgroundColor = '';
}, 1000);
}
});
// Remove rows that are no longer in the data
existingTemplates.forEach((row, templateId) => {
if (!processedTemplates.has(templateId)) {
row.style.backgroundColor = '#f8d7da'; // Highlight removed row briefly
setTimeout(() => {
if (row.parentNode) {
row.parentNode.removeChild(row);
}
}, 500);
}
});
}
function getTemplateStatusBadge(template) {
const status = template.fixture_status || template.status || 'pending';
switch (status) {
case 'template':
return '<span class="badge bg-info"><i class="fas fa-template-icon me-1"></i>Template</span>';
case 'pending':
return '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Pending</span>';
case 'active':
return '<span class="badge bg-success"><i class="fas fa-check me-1"></i>Active</span>';
case 'inactive':
return '<span class="badge bg-secondary"><i class="fas fa-pause me-1"></i>Inactive</span>';
default:
return '<span class="badge bg-secondary"><i class="fas fa-question me-1"></i>Unknown</span>';
}
}
function resetFixtures() {
const confirmMessage = 'WARNING: This will permanently delete ALL fixture data including:\n\n' +
'• All synchronized matches and outcomes\n' +
......@@ -502,8 +751,9 @@ function resetFixtures() {
.then(data => {
if (data.success) {
alert(`Fixtures reset successfully!\n\nRemoved:\n• ${data.removed.matches} matches\n• ${data.removed.outcomes} outcomes\n• ${data.removed.templates} match templates\n• ${data.removed.template_outcomes} template outcomes\n• ${data.removed.zip_files} ZIP files`);
// Reload fixtures to show empty state
// Reload fixtures and templates to show empty state
loadFixtures();
loadTemplates();
} else {
alert('Error resetting fixtures: ' + (data.error || 'Unknown error'));
}
......
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.fixtures') }}">Fixtures</a></li>
<li class="breadcrumb-item active" aria-current="page">Template Details</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="fas fa-boxing-glove me-2"></i>Match Template Details</h1>
<p class="mb-0 text-muted">Edit odds for this match template</p>
</div>
<a href="{{ url_for('main.fixtures') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Fixtures
</a>
</div>
<!-- Loading Spinner -->
<div id="loading" class="text-center my-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading template details...</p>
</div>
<!-- Error Message -->
<div id="error-message" class="alert alert-danger" style="display: none;">
<i class="fas fa-exclamation-triangle me-2"></i>
<span id="error-text"></span>
</div>
<!-- Success Message -->
<div id="success-message" class="alert alert-success" style="display: none;">
<i class="fas fa-check-circle me-2"></i>
<span id="success-text"></span>
</div>
<!-- Template Details Content -->
<div id="template-content" style="display: none;">
<!-- Template Information Card -->
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header">
<h5><i class="fas fa-info-circle me-2"></i>Template Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<table class="table table-borderless">
<tr>
<td><strong>Template ID:</strong></td>
<td><span class="badge bg-primary fs-6" id="template-id"></span></td>
</tr>
<tr>
<td><strong>Fixture ID:</strong></td>
<td><span class="badge bg-secondary fs-6" id="fixture-id"></span></td>
</tr>
<tr>
<td><strong>Match #:</strong></td>
<td><span class="fw-bold" id="match-number"></span></td>
</tr>
</table>
</div>
<div class="col-md-4">
<table class="table table-borderless">
<tr>
<td><strong>Fighter 1:</strong></td>
<td><span id="fighter1"></span></td>
</tr>
<tr>
<td><strong>Fighter 2:</strong></td>
<td><span id="fighter2"></span></td>
</tr>
<tr>
<td><strong>Venue:</strong></td>
<td><span id="venue"></span></td>
</tr>
</table>
</div>
<div class="col-md-4">
<table class="table table-borderless">
<tr>
<td><strong>Created:</strong></td>
<td><small class="text-muted" id="created-at"></small></td>
</tr>
<tr>
<td><strong>Outcomes:</strong></td>
<td><span class="badge bg-info" id="outcome-count"></span></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Edit Odds Card -->
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-edit me-2"></i>Edit Odds</h5>
<button id="save-odds-btn" class="btn btn-primary" onclick="saveOdds()">
<i class="fas fa-save me-1"></i>Save Odds
</button>
</div>
<div class="card-body">
<div id="no-outcomes" class="text-center text-muted py-4" style="display: none;">
<i class="fas fa-chart-line fa-3x mb-3"></i>
<h5>No Outcomes Available</h5>
<p>This template has no outcomes to edit.</p>
</div>
<div id="odds-form-container" style="display: none;">
<form id="odds-form">
<div class="table-responsive">
<table class="table table-hover">
<thead class="table-light">
<tr>
<th>Outcome ID</th>
<th>Column Name</th>
<th>Current Odds</th>
<th>New Odds</th>
</tr>
</thead>
<tbody id="outcomes-tbody">
<!-- Outcomes will be loaded here -->
</tbody>
</table>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script id="template-config" type="application/json">
{
"templateId": {{ template_id | tojson }}
}
</script>
<script>
const config = JSON.parse(document.getElementById('template-config').textContent);
const templateId = config.templateId;
let cachedTemplateData = null;
let isInitialLoad = true;
// Load template details on page load
document.addEventListener('DOMContentLoaded', function() {
loadTemplateDetails();
});
function loadTemplateDetails() {
const loading = document.getElementById('loading');
const errorMessage = document.getElementById('error-message');
const content = document.getElementById('template-content');
// Only show loading state on initial load
if (isInitialLoad) {
loading.style.display = 'block';
errorMessage.style.display = 'none';
content.style.display = 'none';
}
fetch(`/api/matches-templates/${templateId}`)
.then(response => response.json())
.then(data => {
if (data.success) {
// Check if data has actually changed
const currentDataString = JSON.stringify(data);
if (cachedTemplateData === currentDataString && !isInitialLoad) {
console.log('📦 No changes in template data, skipping update');
return;
}
cachedTemplateData = currentDataString;
if (isInitialLoad) {
renderTemplateDetails(data.template);
content.style.display = 'block';
} else {
updateTemplateDetails(data.template);
}
} else {
if (isInitialLoad) {
showError(data.error || 'Failed to load template details');
}
}
})
.catch(error => {
console.error('Error:', error);
if (isInitialLoad) {
showError('Network error: ' + error.message);
}
})
.finally(() => {
if (isInitialLoad) {
loading.style.display = 'none';
isInitialLoad = false;
}
});
}
function showError(message) {
document.getElementById('error-text').textContent = message;
document.getElementById('error-message').style.display = 'block';
document.getElementById('success-message').style.display = 'none';
}
function showSuccess(message) {
document.getElementById('success-text').textContent = message;
document.getElementById('success-message').style.display = 'block';
document.getElementById('error-message').style.display = 'none';
// Hide success message after 3 seconds
setTimeout(() => {
document.getElementById('success-message').style.display = 'none';
}, 3000);
}
function renderTemplateDetails(template) {
// Basic template information
document.getElementById('template-id').textContent = template.id;
document.getElementById('fixture-id').textContent = template.fixture_id;
document.getElementById('match-number').textContent = '#' + template.match_number;
document.getElementById('fighter1').textContent = template.fighter1_township;
document.getElementById('fighter2').textContent = template.fighter2_township;
document.getElementById('venue').textContent = template.venue_kampala_township;
document.getElementById('created-at').textContent = new Date(template.created_at).toLocaleString();
document.getElementById('outcome-count').textContent = template.outcome_count + ' outcomes';
// Render outcomes table
renderOutcomesTable(template.outcomes);
}
function updateTemplateDetails(template) {
// Update outcome count if changed
const outcomeCountEl = document.getElementById('outcome-count');
const newOutcomeCount = template.outcome_count + ' outcomes';
if (outcomeCountEl.textContent !== newOutcomeCount) {
outcomeCountEl.textContent = newOutcomeCount;
}
// Update outcomes table
renderOutcomesTable(template.outcomes);
}
function renderOutcomesTable(outcomes) {
const tbody = document.getElementById('outcomes-tbody');
const noOutcomes = document.getElementById('no-outcomes');
const oddsFormContainer = document.getElementById('odds-form-container');
if (!outcomes || outcomes.length === 0) {
noOutcomes.style.display = 'block';
oddsFormContainer.style.display = 'none';
return;
}
noOutcomes.style.display = 'none';
oddsFormContainer.style.display = 'block';
tbody.innerHTML = '';
outcomes.forEach(outcome => {
const row = document.createElement('tr');
const oddsValue = outcome.odds !== undefined && outcome.odds !== null ? outcome.odds : 0.0;
row.innerHTML = `
<td><span class="badge bg-secondary">${outcome.id}</span></td>
<td><strong>${outcome.column_name}</strong></td>
<td><span class="badge bg-info">${oddsValue.toFixed(2)}</span></td>
<td>
<input type="number"
class="form-control form-control-sm"
id="odds-${outcome.id}"
value="${oddsValue.toFixed(2)}"
step="0.01"
min="0"
style="width: 120px;">
</td>
`;
tbody.appendChild(row);
});
}
function saveOdds() {
const saveBtn = document.getElementById('save-odds-btn');
const tbody = document.getElementById('outcomes-tbody');
const rows = tbody.querySelectorAll('tr');
const oddsUpdates = [];
rows.forEach(row => {
const input = row.querySelector('input[type="number"]');
if (input) {
const outcomeId = parseInt(input.id.replace('odds-', ''));
const newOdds = parseFloat(input.value);
if (!isNaN(newOdds) && newOdds >= 0) {
oddsUpdates.push({
outcome_id: outcomeId,
odds: newOdds
});
}
}
});
if (oddsUpdates.length === 0) {
showError('No valid odds to update');
return;
}
// Disable save button and show loading state
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
fetch(`/api/matches-templates/${templateId}/odds`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
odds: oddsUpdates
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
showSuccess(data.message || 'Odds updated successfully');
// Reload template details to show updated odds
loadTemplateDetails();
} else {
showError(data.error || 'Failed to update odds');
}
})
.catch(error => {
console.error('Error:', error);
showError('Network error: ' + error.message);
})
.finally(() => {
// Re-enable save button
saveBtn.disabled = false;
saveBtn.innerHTML = '<i class="fas fa-save me-1"></i>Save Odds';
});
}
</script>
{% endblock %}
\ No newline at end of file
......@@ -80,7 +80,7 @@
<div class="card mb-4">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-list me-2"></i>Today's Matches Available for Betting
<i class="fas fa-list me-2"></i>Available Matches for Betting (Today & Yesterday)
<span class="badge bg-success ms-2" id="available-matches-count">0</span>
</h5>
</div>
......@@ -640,7 +640,7 @@ function updateAvailableMatchesDisplay(data, container) {
if (data.total === 0) {
container.innerHTML = `
<div class="text-center text-muted">
<i class="fas fa-info-circle me-2"></i>No matches available for betting today
<i class="fas fa-info-circle me-2"></i>No matches available for betting
<div class="mt-2">
<small>Matches must be in 'bet' status to accept wagers</small>
</div>
......@@ -716,17 +716,6 @@ function updateAvailableMatchesDisplay(data, container) {
// Add event listeners for amount inputs only
container.querySelectorAll('.amount-input').forEach(input => {
input.addEventListener('input', function() {
// Clear other outcomes for this match when entering an amount
const matchId = this.getAttribute('data-match-id');
const currentOutcome = this.getAttribute('data-outcome');
// Clear all other amount inputs for this match
container.querySelectorAll(`.amount-input[data-match-id="${matchId}"]`).forEach(otherInput => {
if (otherInput !== this) {
otherInput.value = '';
}
});
updateBetSummary();
});
});
......
{% extends "base.html" %}
{% block title %}{{ page_title }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<!-- Navigation -->
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{{ url_for('main.fixtures') }}">Fixtures</a></li>
<li class="breadcrumb-item active" aria-current="page">Fixture Template</li>
</ol>
</nav>
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1><i class="fas fa-boxing-glove me-2"></i>Fixture Template</h1>
<p class="mb-0 text-muted">All matches in the fixture template</p>
</div>
<a href="{{ url_for('main.fixtures') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i>Back to Fixtures
</a>
</div>
<!-- Loading Spinner -->
<div id="loading" class="text-center my-5">
<div class="spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
<p class="mt-2">Loading template matches...</p>
</div>
<!-- Error Message -->
<div id="error-message" class="alert alert-danger" style="display: none;">
<i class="fas fa-exclamation-triangle me-2"></i>
<span id="error-text"></span>
</div>
<!-- Template Details Content -->
<div id="template-content" style="display: none;">
<!-- Template Information Card -->
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header">
<h5><i class="fas fa-info-circle me-2"></i>Template Information</h5>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-4">
<table class="table table-borderless">
<tr>
<td><strong>Template ID:</strong></td>
<td><span class="badge bg-primary fs-6" id="template-id"></span></td>
</tr>
<tr>
<td><strong>Fixture ID:</strong></td>
<td><span class="badge bg-secondary fs-6" id="fixture-id"></span></td>
</tr>
<tr>
<td><strong>Match Count:</strong></td>
<td><span class="fw-bold" id="match-count"></span></td>
</tr>
</table>
</div>
<div class="col-md-4">
<table class="table table-borderless">
<tr>
<td><strong>Venue:</strong></td>
<td><span id="venue"></span></td>
</tr>
<tr>
<td><strong>Created:</strong></td>
<td><small class="text-muted" id="created-at"></small></td>
</tr>
</table>
</div>
<div class="col-md-4">
<table class="table table-borderless">
<tr>
<td><strong>Sample Fighters:</strong></td>
<td><small class="text-muted" id="sample-fighters"></small></td>
</tr>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Matches in Template -->
<div class="row">
<div class="col-12">
<div class="card mb-4">
<div class="card-header d-flex justify-content-between align-items-center">
<h5><i class="fas fa-list me-2"></i>Matches in Template</h5>
<span id="matches-count" class="badge bg-secondary">0 matches</span>
</div>
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Match #</th>
<th>Fighters</th>
<th>Status</th>
<th>Outcomes</th>
<th>Actions</th>
</tr>
</thead>
<tbody id="matches-tbody">
<!-- Matches will be loaded here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
let cachedTemplateData = null;
let isInitialLoad = true;
// Load template details on page load
document.addEventListener('DOMContentLoaded', function() {
loadTemplateDetails();
// Set up auto-refresh every 15 seconds
setInterval(loadTemplateDetails, 15000);
});
function loadTemplateDetails() {
const loading = document.getElementById('loading');
const errorMessage = document.getElementById('error-message');
const content = document.getElementById('template-content');
// Only show loading state on initial load
if (isInitialLoad) {
loading.style.display = 'block';
errorMessage.style.display = 'none';
content.style.display = 'none';
}
fetch('/api/matches-templates')
.then(response => response.json())
.then(data => {
if (data.success) {
// Check if data has actually changed
const currentDataString = JSON.stringify(data);
if (cachedTemplateData === currentDataString && !isInitialLoad) {
console.log('📦 No changes in template data, skipping update');
return;
}
cachedTemplateData = currentDataString;
// Get the first template from the templates array
const template = data.templates && data.templates.length > 0 ? data.templates[0] : null;
if (!template) {
if (isInitialLoad) {
showError('No template data available');
}
return;
}
if (isInitialLoad) {
renderTemplateDetails(template);
content.style.display = 'block';
} else {
updateTemplateDetails(template);
}
} else {
if (isInitialLoad) {
showError(data.error || 'Failed to load template details');
}
}
})
.catch(error => {
console.error('Error:', error);
if (isInitialLoad) {
showError('Network error: ' + error.message);
}
})
.finally(() => {
if (isInitialLoad) {
loading.style.display = 'none';
isInitialLoad = false;
}
});
}
function showError(message) {
document.getElementById('error-text').textContent = message;
document.getElementById('error-message').style.display = 'block';
}
function renderTemplateDetails(template) {
// Safety check for template object
if (!template) {
showError('Template data is not available');
return;
}
// Basic template information
document.getElementById('template-id').textContent = template.id || 'N/A';
document.getElementById('fixture-id').textContent = template.fixture_id || 'N/A';
document.getElementById('match-count').textContent = (template.match_count || 0) + ' matches';
document.getElementById('venue').textContent = template.venue_kampala_township || 'N/A';
document.getElementById('created-at').textContent = template.created_at ? new Date(template.created_at).toLocaleString() : 'N/A';
// Sample fighters (from first match)
if (template.matches && template.matches.length > 0) {
const firstMatch = template.matches[0];
if (firstMatch) {
document.getElementById('sample-fighters').textContent =
(firstMatch.fighter1_township || 'Unknown') + ' vs ' + (firstMatch.fighter2_township || 'Unknown');
}
}
// Render matches table
renderMatchesTable(template.matches || []);
}
function updateTemplateDetails(template) {
// Update match count
const matchCountEl = document.getElementById('match-count');
const newMatchCount = template.match_count + ' matches';
if (matchCountEl.textContent !== newMatchCount) {
matchCountEl.textContent = newMatchCount;
}
// Update matches table with smooth transitions
updateMatchesTable(template.matches || []);
}
function renderMatchesTable(matches) {
const tbody = document.getElementById('matches-tbody');
const matchesCount = document.getElementById('matches-count');
if (!matches || !Array.isArray(matches)) {
matchesCount.textContent = '0 matches';
tbody.innerHTML = '<tr><td colspan="5" class="text-center text-muted">No matches available</td></tr>';
return;
}
matchesCount.textContent = matches.length + ' matches';
tbody.innerHTML = '';
matches.forEach(match => {
if (!match || !match.id) {
console.warn('Invalid match data:', match);
return;
}
const row = document.createElement('tr');
const outcomesCount = match.outcome_count || 0;
row.innerHTML = `
<td><strong>#${match.match_number || 'N/A'}</strong></td>
<td>
<div class="fw-bold">${match.fighter1_township || 'Unknown'}</div>
<small class="text-muted">vs</small>
<div class="fw-bold">${match.fighter2_township || 'Unknown'}</div>
</td>
<td>${getStatusBadge(match)}</td>
<td><span class="badge bg-light text-dark">${outcomesCount} outcomes</span></td>
<td>
<a href="/matches-templates/${match.id}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i>View
</a>
</td>
`;
row.setAttribute('data-match-id', match.id);
tbody.appendChild(row);
});
}
function updateMatchesTable(matches) {
const tbody = document.getElementById('matches-tbody');
const matchesCount = document.getElementById('matches-count');
matchesCount.textContent = matches.length + ' matches';
// Get existing rows
const existingRows = Array.from(tbody.children);
const existingMatches = new Map();
existingRows.forEach(row => {
const matchId = row.getAttribute('data-match-id');
if (matchId) {
existingMatches.set(parseInt(matchId), row);
}
});
const processedMatches = new Set();
matches.forEach(match => {
processedMatches.add(match.id);
const outcomesCount = match.outcome_count || 0;
const newRowHTML = `
<td><strong>#${match.match_number}</strong></td>
<td>
<div class="fw-bold">${match.fighter1_township}</div>
<small class="text-muted">vs</small>
<div class="fw-bold">${match.fighter2_township}</div>
</td>
<td>${getStatusBadge(match)}</td>
<td><span class="badge bg-light text-dark">${outcomesCount} outcomes</span></td>
<td>
<a href="/matches-templates/${match.id}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i>View
</a>
</td>
`;
const existingRow = existingMatches.get(match.id);
if (existingRow) {
// Update existing row only if content changed
if (existingRow.innerHTML !== newRowHTML) {
existingRow.innerHTML = newRowHTML;
existingRow.style.backgroundColor = '#fff3cd'; // Highlight briefly
setTimeout(() => {
existingRow.style.backgroundColor = '';
}, 1000);
}
} else {
// Add new row
const row = document.createElement('tr');
row.setAttribute('data-match-id', match.id);
row.innerHTML = newRowHTML;
row.style.backgroundColor = '#d4edda'; // Highlight new row
tbody.appendChild(row);
setTimeout(() => {
row.style.backgroundColor = '';
}, 1000);
}
});
// Remove rows no longer in data
existingMatches.forEach((row, matchId) => {
if (!processedMatches.has(matchId)) {
row.style.backgroundColor = '#f8d7da'; // Highlight removed row
setTimeout(() => {
if (row.parentNode) {
row.parentNode.removeChild(row);
}
}, 500);
}
});
}
function getStatusBadge(match) {
const status = match.status;
switch (status) {
case 'pending':
return '<span class="badge bg-warning"><i class="fas fa-clock me-1"></i>Pending</span>';
case 'scheduled':
return '<span class="badge bg-secondary"><i class="fas fa-calendar me-1"></i>Scheduled</span>';
case 'bet':
return '<span class="badge bg-primary"><i class="fas fa-money-bill me-1"></i>Bet</span>';
case 'ingame':
return '<span class="badge bg-success"><i class="fas fa-gamepad me-1"></i>In Game</span>';
case 'done':
return '<span class="badge bg-dark"><i class="fas fa-stop me-1"></i>Done</span>';
case 'cancelled':
return '<span class="badge bg-danger"><i class="fas fa-times me-1"></i>Cancelled</span>';
case 'failed':
return '<span class="badge bg-danger"><i class="fas fa-exclamation-triangle me-1"></i>Failed</span>';
case 'paused':
return '<span class="badge bg-warning"><i class="fas fa-pause me-1"></i>Paused</span>';
default:
return '<span class="badge bg-secondary"><i class="fas fa-question me-1"></i>Unknown</span>';
}
}
</script>
{% endblock %}
\ No newline at end of file
#!/usr/bin/env python3
"""
Test script to verify the cross-day fixture fix.
This script creates a yesterday fixture with 5 completed matches and 5 bet matches,
then tests that starting the game without fixture_id activates the yesterday fixture first.
"""
import sys
import os
import time
from datetime import datetime, timedelta
from pathlib import Path
# Add the project root to Python path
project_root = Path(__file__).parent
sys.path.insert(0, str(project_root))
from mbetterclient.database.manager import DatabaseManager
from mbetterclient.database.models import MatchModel, MatchOutcomeModel
from mbetterclient.core.games_thread import GamesThread
from mbetterclient.core.message_bus import MessageBus, Message, MessageType
from mbetterclient.config.settings import get_user_data_dir
def create_yesterday_fixture_with_remaining_matches(db_manager):
"""Create a fixture from yesterday with 5 completed matches and 5 bet matches"""
print("Creating yesterday fixture with remaining matches...")
session = db_manager.get_session()
try:
# Get yesterday's date in venue timezone
from mbetterclient.utils.timezone_utils import get_today_venue_date
yesterday = get_today_venue_date(db_manager) - timedelta(days=1)
# Convert to UTC for database storage
from mbetterclient.utils.timezone_utils import venue_to_utc_datetime
yesterday_start = datetime.combine(yesterday, datetime.min.time())
yesterday_utc = venue_to_utc_datetime(yesterday_start, db_manager)
# Generate fixture ID
import uuid
fixture_id = f"yesterday_test_{uuid.uuid4().hex[:8]}"
# Create 5 completed matches
for i in range(1, 6):
match = MatchModel(
match_number=i,
fighter1_township=f"Fighter {i}A",
fighter2_township=f"Fighter {i}B",
venue_kampala_township=f"Venue {i}",
start_time=yesterday_utc + timedelta(minutes=i*10),
status='done',
fixture_id=fixture_id,
active_status=True,
fixture_active_time=int(time.time()),
result=f'WIN{i}A',
end_time=yesterday_utc + timedelta(minutes=i*10 + 5),
done=True,
running=False,
filename=f"match_{i}_completed.mp4",
file_sha1sum=f"sha1_{i}_completed"
)
session.add(match)
session.flush()
# Add some outcomes
outcome = MatchOutcomeModel(
match_id=match.id,
column_name=f'WIN{i}A',
float_value=1.85
)
session.add(outcome)
# Create 5 bet matches (remaining)
for i in range(6, 11):
match = MatchModel(
match_number=i,
fighter1_township=f"Fighter {i}A",
fighter2_township=f"Fighter {i}B",
venue_kampala_township=f"Venue {i}",
start_time=yesterday_utc + timedelta(minutes=i*10),
status='bet',
fixture_id=fixture_id,
active_status=True,
fixture_active_time=int(time.time()),
result=None,
end_time=None,
done=False,
running=False,
filename=f"match_{i}_bet.mp4",
file_sha1sum=f"sha1_{i}_bet"
)
session.add(match)
session.flush()
# Add some outcomes
outcome = MatchOutcomeModel(
match_id=match.id,
column_name=f'WIN{i}A',
float_value=1.85
)
session.add(outcome)
session.commit()
print(f"Created yesterday fixture {fixture_id} with 5 completed and 5 bet matches")
return fixture_id
except Exception as e:
session.rollback()
print(f"Failed to create yesterday fixture: {e}")
return None
finally:
session.close()
def test_cross_day_fixture_logic():
"""Test the cross-day fixture logic"""
print("Testing cross-day fixture logic...")
# Initialize database manager
db_path = get_user_data_dir() / "mbetterclient.db"
db_manager = DatabaseManager(str(db_path))
if not db_manager.initialize():
print("Failed to initialize database")
return False
# Create yesterday fixture
yesterday_fixture_id = create_yesterday_fixture_with_remaining_matches(db_manager)
if not yesterday_fixture_id:
print("Failed to create test fixture")
return False
# Initialize message bus and games thread
message_bus = MessageBus()
games_thread = GamesThread("test_games_thread", message_bus, db_manager)
# Mock the initialization (we don't need full initialization for this test)
games_thread.db_manager = db_manager
games_thread.current_fixture_id = None
games_thread.game_active = False
# Create a test message for START_GAME without fixture_id
test_message = Message(
type=MessageType.START_GAME,
sender="test",
recipient="games_thread",
data={"timestamp": time.time()},
correlation_id="test_123"
)
# Mock the _send_response method to capture responses
responses = []
original_send_response = games_thread._send_response
def mock_send_response(message, status, response_message=None):
responses.append({"status": status, "message": response_message})
print(f"Mock response: {status} - {response_message}")
games_thread._send_response = mock_send_response
# Mock the _activate_fixture method to capture activation calls
activations = []
original_activate_fixture = games_thread._activate_fixture
def mock_activate_fixture(fixture_id, message):
activations.append(fixture_id)
print(f"Mock activation: fixture {fixture_id}")
# Set the current fixture to simulate activation
games_thread.current_fixture_id = fixture_id
games_thread.game_active = True
games_thread._activate_fixture = mock_activate_fixture
# Mock the _initialize_new_fixture method
original_initialize_new_fixture = games_thread._initialize_new_fixture
def mock_initialize_new_fixture():
# Return a mock today fixture ID
import uuid
return f"today_test_{uuid.uuid4().hex[:8]}"
games_thread._initialize_new_fixture = mock_initialize_new_fixture
try:
# Call the handler
games_thread._handle_start_game(test_message)
# Check results
print(f"Activations: {activations}")
print(f"Responses: {responses}")
# Verify that yesterday fixture was activated first
if len(activations) >= 1 and activations[0] == yesterday_fixture_id:
print("✅ SUCCESS: Yesterday fixture was activated first")
return True
else:
print(f"❌ FAILURE: Expected yesterday fixture {yesterday_fixture_id} to be activated first, got {activations}")
return False
except Exception as e:
print(f"Test failed with exception: {e}")
import traceback
traceback.print_exc()
return False
finally:
# Restore original methods
games_thread._send_response = original_send_response
games_thread._activate_fixture = original_activate_fixture
games_thread._initialize_new_fixture = original_initialize_new_fixture
def cleanup_test_data(db_manager, fixture_id):
"""Clean up test data"""
if not fixture_id:
return
print(f"Cleaning up test fixture {fixture_id}...")
session = db_manager.get_session()
try:
# Delete matches and outcomes
matches = session.query(MatchModel).filter(MatchModel.fixture_id == fixture_id).all()
for match in matches:
session.query(MatchOutcomeModel).filter(MatchOutcomeModel.match_id == match.id).delete()
session.query(MatchModel).filter(MatchModel.fixture_id == fixture_id).delete()
session.commit()
print(f"Cleaned up test fixture {fixture_id}")
except Exception as e:
print(f"Failed to cleanup test data: {e}")
session.rollback()
finally:
session.close()
def main():
"""Main test function"""
print("Cross-Day Fixture Fix Test")
print("=" * 40)
# Initialize database manager
db_path = get_user_data_dir() / "mbetterclient.db"
db_manager = DatabaseManager(str(db_path))
try:
# Run the test
success = test_cross_day_fixture_logic()
if success:
print("\n🎉 All tests passed! The cross-day fixture fix is working correctly.")
else:
print("\n💥 Test failed! The cross-day fixture fix needs more work.")
return success
except Exception as e:
print(f"Test execution failed: {e}")
import traceback
traceback.print_exc()
return False
finally:
db_manager.close()
if __name__ == "__main__":
success = main()
sys.exit(0 if success else 1)
\ 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