Almost there...

parent 398d40ba
......@@ -753,18 +753,17 @@ class MbetterClientApplication:
logger.error(f"Failed to handle log entry: {e}")
def _handle_start_game_message(self, message: Message):
"""Handle START_GAME message - only cancel the start-timer as its job is done"""
"""Handle START_GAME message - timer will be cancelled when game actually starts successfully"""
try:
# The core should only cancel its start-timer when START_GAME is received
# The timer will be cancelled when the game status indicates successful start
# The actual START_GAME processing is done by games_thread
logger.info("START_GAME message received - cancelling command line start-timer as it has completed its job")
self._cancel_game_timer()
logger.info("START_GAME message received - timer will be managed based on game start outcome")
except Exception as e:
logger.error(f"Failed to handle START_GAME message: {e}")
def _handle_game_status_response(self, message: Message):
"""Handle GAME_STATUS responses, particularly for timer-initiated START_GAME failures"""
"""Handle GAME_STATUS responses, managing timer based on game start outcome"""
try:
status = message.data.get("status", "unknown")
sender = message.sender
......@@ -776,7 +775,7 @@ class MbetterClientApplication:
return
# Check if this is a failure response that should trigger timer restart
failure_statuses = ["waiting_for_downloads", "discarded", "error", "no_matches"]
failure_statuses = ["waiting_for_downloads", "discarded", "error", "no_matches", "no_fixtures_available"]
if status in failure_statuses:
logger.info(f"START_GAME failed with status '{status}' from {sender} - restarting timer")
......@@ -791,8 +790,9 @@ class MbetterClientApplication:
else:
logger.warning("No original timer interval available for restart")
elif status == "started":
logger.info(f"START_GAME succeeded with status '{status}' from {sender} - timer job completed")
# Game started successfully, clear timer state
logger.info(f"START_GAME succeeded with status '{status}' from {sender} - cancelling timer")
# Game started successfully, cancel the timer and clear timer state
self._cancel_game_timer()
self._original_timer_interval = None
except Exception as e:
......
This diff is collapsed.
......@@ -574,14 +574,30 @@ class MatchTimerComponent(ThreadedComponent):
completed_matches = query.all()
if len(completed_matches) < count:
logger.warning(f"Only {len(completed_matches)} completed matches available (excluding same fighters), requested {count}")
return completed_matches
# Select random matches
selected_matches = random.sample(completed_matches, count)
logger.info(f"Selected {len(selected_matches)} random completed matches (excluding same fighters as last match)")
return selected_matches
if len(completed_matches) >= count:
# Select random matches
selected_matches = random.sample(completed_matches, count)
logger.info(f"Selected {len(selected_matches)} random completed matches (excluding same fighters as last match)")
return selected_matches
else:
# Not enough matches with exclusions
if completed_matches:
logger.warning(f"Only {len(completed_matches)} completed matches available (excluding same fighters), requested {count} - returning available")
return completed_matches
else:
# No matches found with exclusions, recycle the oldest match from database
logger.warning("🚨 No matches available after exclusions - recycling the oldest match from database")
oldest_match = session.query(MatchModel).filter(
MatchModel.status.in_(['done', 'end', 'cancelled', 'failed']),
MatchModel.active_status == True
).order_by(MatchModel.created_at.asc()).first()
if oldest_match:
logger.info(f"♻️ Recycled oldest match: {oldest_match.match_number} ({oldest_match.fighter1_township} vs {oldest_match.fighter2_township})")
return [oldest_match]
else:
logger.error("🚨 No completed matches found in database at all")
return []
except Exception as e:
logger.error(f"Failed to select random completed matches excluding same fighters: {e}")
......
......@@ -37,10 +37,10 @@ class DatabaseManager:
try:
# Ensure database directory exists
self.db_path.parent.mkdir(parents=True, exist_ok=True)
# Create database URL
db_url = f"sqlite:///{self.db_path}"
# Create engine with proper SQLite configuration
self.engine = create_engine(
db_url,
......@@ -51,7 +51,7 @@ class DatabaseManager:
'timeout': 30
}
)
# Configure SQLite for better performance and reliability
with self.engine.connect() as conn:
conn.execute(text("PRAGMA journal_mode=WAL"))
......@@ -60,35 +60,117 @@ class DatabaseManager:
conn.execute(text("PRAGMA temp_store=MEMORY"))
conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB
conn.commit()
# Create session factory
self.session_factory = sessionmaker(bind=self.engine)
self.Session = scoped_session(self.session_factory)
# Create all tables
Base.metadata.create_all(self.engine)
# Mark as initialized so migrations can use get_session()
self._initialized = True
# Run database migrations
if not run_migrations(self):
logger.error("Database migrations failed")
self._initialized = False
return False
# Create default admin user if none exists
self._create_default_admin()
# Initialize default templates
self._initialize_default_templates()
logger.info("Database manager initialized successfully")
return True
except SQLAlchemyError as e:
logger.warning(f"Failed to initialize database at {self.db_path}: {e}")
# Try fallback locations if the primary location fails
return self._initialize_with_fallback()
except Exception as e:
logger.error(f"Failed to initialize database manager: {e}")
return False
def _initialize_with_fallback(self) -> bool:
"""Try to initialize database in fallback locations"""
fallback_paths = [
# Try current working directory
Path.cwd() / "mbetterclient.db",
# Try user's home directory
Path.home() / ".mbetterclient.db",
# Try temp directory as last resort
Path("/tmp") / "mbetterclient.db"
]
for fallback_path in fallback_paths:
try:
logger.info(f"Trying fallback database location: {fallback_path}")
# Ensure fallback directory exists
fallback_path.parent.mkdir(parents=True, exist_ok=True)
# Create database URL
db_url = f"sqlite:///{fallback_path}"
# Create engine with proper SQLite configuration
self.engine = create_engine(
db_url,
echo=False,
pool_pre_ping=True,
connect_args={
'check_same_thread': False,
'timeout': 30
}
)
# Configure SQLite for better performance and reliability
with self.engine.connect() as conn:
conn.execute(text("PRAGMA journal_mode=WAL"))
conn.execute(text("PRAGMA synchronous=NORMAL"))
conn.execute(text("PRAGMA cache_size=10000"))
conn.execute(text("PRAGMA temp_store=MEMORY"))
conn.execute(text("PRAGMA mmap_size=268435456")) # 256MB
conn.commit()
# Create session factory
self.session_factory = sessionmaker(bind=self.engine)
self.Session = scoped_session(self.session_factory)
# Create all tables
Base.metadata.create_all(self.engine)
# Mark as initialized so migrations can use get_session()
self._initialized = True
# Update the db_path to the working fallback path
self.db_path = fallback_path
logger.warning(f"Using fallback database location: {fallback_path}")
# Run database migrations
if not run_migrations(self):
logger.error("Database migrations failed")
self._initialized = False
return False
# Create default admin user if none exists
self._create_default_admin()
# Initialize default templates
self._initialize_default_templates()
logger.info("Database manager initialized successfully with fallback location")
return True
except Exception as e:
logger.warning(f"Fallback location {fallback_path} also failed: {e}")
continue
logger.error("All database initialization attempts failed")
return False
def get_session(self):
"""Get database session"""
......
......@@ -669,7 +669,7 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration):
# SQLite doesn't support ALTER TABLE DROP CONSTRAINT directly
# We need to recreate the table without the UNIQUE constraint
# Step 1: Create new table without UNIQUE constraint on fixture_id
conn.execute(text("""
CREATE TABLE IF NOT EXISTS matches_new (
......@@ -682,6 +682,8 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration):
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
winning_outcomes TEXT NULL,
under_over_result VARCHAR(50) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
......@@ -696,6 +698,7 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration):
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
zip_validation_status VARCHAR(20) DEFAULT 'pending',
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
......@@ -752,12 +755,12 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration):
GROUP BY fixture_id
HAVING COUNT(*) > 1
"""))
duplicates = result.fetchall()
if duplicates:
logger.error(f"Cannot add UNIQUE constraint - duplicate fixture_ids found: {[row[0] for row in duplicates]}")
return False
# Recreate table with UNIQUE constraint on fixture_id
conn.execute(text("""
CREATE TABLE IF NOT EXISTS matches_new (
......@@ -766,24 +769,28 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration):
fighter1_township VARCHAR(255) NOT NULL,
fighter2_township VARCHAR(255) NOT NULL,
venue_kampala_township VARCHAR(255) NOT NULL,
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
winning_outcomes TEXT NULL,
under_over_result VARCHAR(50) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
fixture_active_time INTEGER NULL,
filename VARCHAR(1024) NOT NULL,
file_sha1sum VARCHAR(255) NOT NULL,
fixture_id VARCHAR(255) NOT NULL UNIQUE,
active_status BOOLEAN DEFAULT FALSE,
zip_filename VARCHAR(1024) NULL,
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
zip_validation_status VARCHAR(20) DEFAULT 'pending',
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
......@@ -2166,6 +2173,8 @@ class Migration_029_ChangeMatchNumberToUniqueWithinFixture(DatabaseMigration):
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
winning_outcomes TEXT NULL,
under_over_result VARCHAR(50) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
......@@ -2180,6 +2189,7 @@ class Migration_029_ChangeMatchNumberToUniqueWithinFixture(DatabaseMigration):
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
zip_validation_status VARCHAR(20) DEFAULT 'pending',
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
......@@ -2258,6 +2268,8 @@ class Migration_029_ChangeMatchNumberToUniqueWithinFixture(DatabaseMigration):
start_time DATETIME NULL,
end_time DATETIME NULL,
result VARCHAR(255) NULL,
winning_outcomes TEXT NULL,
under_over_result VARCHAR(50) NULL,
done BOOLEAN DEFAULT FALSE NOT NULL,
running BOOLEAN DEFAULT FALSE NOT NULL,
status VARCHAR(20) DEFAULT 'pending' NOT NULL,
......@@ -2272,6 +2284,7 @@ class Migration_029_ChangeMatchNumberToUniqueWithinFixture(DatabaseMigration):
zip_sha1sum VARCHAR(255) NULL,
zip_upload_status VARCHAR(20) DEFAULT 'pending',
zip_upload_progress REAL DEFAULT 0.0,
zip_validation_status VARCHAR(20) DEFAULT 'pending',
created_by INTEGER NULL REFERENCES users(id) ON DELETE SET NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
......@@ -2360,6 +2373,55 @@ class Migration_030_AddZipValidationStatus(DatabaseMigration):
logger.warning("SQLite doesn't support DROP COLUMN - zip_validation_status column will remain")
return True
class Migration_031_AddWinningOutcomesFields(DatabaseMigration):
"""Add winning_outcomes and under_over_result fields to matches table"""
def __init__(self):
super().__init__("031", "Add winning_outcomes and under_over_result fields to matches table")
def up(self, db_manager) -> bool:
"""Add winning_outcomes and under_over_result columns to matches table"""
try:
with db_manager.engine.connect() as conn:
# Check if columns already exist
result = conn.execute(text("PRAGMA table_info(matches)"))
columns = [row[1] for row in result.fetchall()]
if 'winning_outcomes' not in columns:
# Add winning_outcomes column (JSON array)
conn.execute(text("""
ALTER TABLE matches
ADD COLUMN winning_outcomes TEXT
"""))
logger.info("winning_outcomes column added to matches table")
else:
logger.info("winning_outcomes column already exists in matches table")
if 'under_over_result' not in columns:
# Add under_over_result column (string)
conn.execute(text("""
ALTER TABLE matches
ADD COLUMN under_over_result VARCHAR(50)
"""))
logger.info("under_over_result column added to matches table")
else:
logger.info("under_over_result column already exists in matches table")
conn.commit()
return True
except Exception as e:
logger.error(f"Failed to add winning outcomes fields to matches: {e}")
return False
def down(self, db_manager) -> bool:
"""Remove winning_outcomes and under_over_result columns - SQLite doesn't support DROP COLUMN easily"""
logger.warning("SQLite doesn't support DROP COLUMN - winning_outcomes and under_over_result columns will remain")
return True
# Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(),
......@@ -2392,6 +2454,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_028_AddFixtureRefreshIntervalConfig(),
Migration_029_ChangeMatchNumberToUniqueWithinFixture(),
Migration_030_AddZipValidationStatus(),
Migration_031_AddWinningOutcomesFields(),
]
......
......@@ -488,7 +488,9 @@ class MatchModel(BaseModel):
# Match timing and results
start_time = Column(DateTime, comment='Match start time')
end_time = Column(DateTime, comment='Match end time')
result = Column(String(255), comment='Match result/outcome')
result = Column(String(255), comment='Match result/outcome (main result only, e.g. RET2)')
winning_outcomes = Column(JSON, comment='Array of winning outcomes from extraction associations (e.g., ["WIN1", "X1", "12"])')
under_over_result = Column(String(50), comment='UNDER/OVER result if applicable')
done = Column(Boolean, default=False, nullable=False, comment='Match completion flag (0=pending, 1=done)')
running = Column(Boolean, default=False, nullable=False, comment='Match running flag (0=not running, 1=running)')
status = Column(Enum('pending', 'scheduled', 'bet', 'ingame', 'done', 'cancelled', 'failed', 'paused'), default='pending', nullable=False, comment='Match status enum')
......
This diff is collapsed.
......@@ -214,7 +214,7 @@
<div class="overlay-container">
<div class="message-panel" id="messagePanel">
<div class="message-icon" id="messageIcon">📢</div>
<div class="message-title" id="messageTitle">Mbetter Game</div>
<div class="message-title" id="messageTitle">Townships Combat League</div>
<div class="message-content" id="messageContent">Waiting for game to start....</div>
</div>
</div>
......@@ -222,7 +222,7 @@
<script>
// Global variables for overlay data handling
let overlayData = {};
let currentTitle = 'Mbetter system:';
let currentTitle = 'Townships Combat League:';
let currentMessage = 'Waiting for game to start...';
let currentIcon = '🥊';
......
......@@ -1210,7 +1210,6 @@
betItem.innerHTML = `
<div class="bet-outcome">${outcome.outcome || 'Unknown'}</div>
<div class="bet-amount">$${outcome.amount ? outcome.amount.toFixed(2) : '0.00'}</div>
`;
betsList.appendChild(betItem);
......@@ -1311,4 +1310,4 @@
The template implements its own WebChannel setup and data polling mechanism.
-->
</body>
</html>
\ No newline at end of file
</html>
......@@ -80,6 +80,8 @@
<th><i class="fas fa-target me-1"></i>Outcome</th>
<th><i class="fas fa-euro-sign me-1"></i>Amount</th>
<th><i class="fas fa-flag me-1"></i>Result</th>
<th><i class="fas fa-trophy me-1"></i>Winning Outcomes</th>
<th><i class="fas fa-balance-scale me-1"></i>Under/Over</th>
<th><i class="fas fa-cogs me-1"></i>Actions</th>
</tr>
</thead>
......@@ -109,6 +111,22 @@
<span class="badge bg-secondary">Cancelled</span>
{% endif %}
</td>
<td>
{% if detail.match.winning_outcomes %}
{% for outcome in detail.match.winning_outcomes %}
<span class="badge bg-success">{{ outcome }}</span>
{% endfor %}
{% else %}
<span class="text-muted">Not available</span>
{% endif %}
</td>
<td>
{% if detail.match.under_over_result %}
<span class="badge bg-info">{{ detail.match.under_over_result }}</span>
{% else %}
<span class="text-muted">Not available</span>
{% endif %}
</td>
<td>
{% if detail.result == 'pending' %}
<button class="btn btn-sm btn-outline-danger btn-delete-detail"
......
......@@ -80,6 +80,8 @@
<th><i class="fas fa-target me-1"></i>Outcome</th>
<th><i class="fas fa-euro-sign me-1"></i>Amount</th>
<th><i class="fas fa-flag me-1"></i>Result</th>
<th><i class="fas fa-trophy me-1"></i>Winning Outcomes</th>
<th><i class="fas fa-balance-scale me-1"></i>Under/Over</th>
<th><i class="fas fa-cogs me-1"></i>Actions</th>
</tr>
</thead>
......@@ -109,6 +111,22 @@
<span class="badge bg-secondary">Cancelled</span>
{% endif %}
</td>
<td>
{% if detail.match.winning_outcomes %}
{% for outcome in detail.match.winning_outcomes %}
<span class="badge bg-success">{{ outcome }}</span>
{% endfor %}
{% else %}
<span class="text-muted">Not available</span>
{% endif %}
</td>
<td>
{% if detail.match.under_over_result %}
<span class="badge bg-info">{{ detail.match.under_over_result }}</span>
{% else %}
<span class="text-muted">Not available</span>
{% endif %}
</td>
<td>
{% if detail.result == 'pending' %}
<button class="btn btn-sm btn-outline-danger btn-delete-detail"
......
......@@ -118,6 +118,8 @@
<th>Start Time</th>
<th>End Time</th>
<th>Result</th>
<th>Winning Outcomes</th>
<th>Under/Over</th>
<th>Outcomes</th>
<th>Actions</th>
</tr>
......@@ -304,6 +306,20 @@ function showError(message) {
document.getElementById('error-message').style.display = 'block';
}
function formatWinningOutcomes(winningOutcomes) {
// Safely format winning outcomes, handling cases where it's not an array
if (Array.isArray(winningOutcomes) && winningOutcomes.length > 0) {
return winningOutcomes.join(', ');
} else if (winningOutcomes && typeof winningOutcomes === 'string') {
return winningOutcomes;
} else if (winningOutcomes && typeof winningOutcomes === 'object') {
// If it's an object, try to stringify it
return JSON.stringify(winningOutcomes);
} else {
return 'Not available';
}
}
function renderFixtureDetails(fixture, matches) {
// Basic fixture information
document.getElementById('fixture-id').textContent = fixture.fixture_id;
......@@ -394,6 +410,8 @@ function renderMatchesTable(matches) {
const startTimeDisplay = match.start_time ? new Date(match.start_time).toLocaleString() : 'Not set';
const endTimeDisplay = match.end_time ? new Date(match.end_time).toLocaleString() : 'Not set';
const resultDisplay = match.result || 'Not available';
const winningOutcomesDisplay = formatWinningOutcomes(match.winning_outcomes);
const underOverDisplay = match.under_over_result || 'Not available';
const outcomesCount = match.outcome_count || 0;
row.innerHTML = `
......@@ -407,6 +425,8 @@ function renderMatchesTable(matches) {
<td><small class="text-info">${startTimeDisplay}</small></td>
<td><small class="text-success">${endTimeDisplay}</small></td>
<td><small class="text-muted">${resultDisplay}</small></td>
<td><small class="text-primary">${winningOutcomesDisplay}</small></td>
<td><small class="text-warning">${underOverDisplay}</small></td>
<td><span class="badge bg-light text-dark">${outcomesCount} outcomes</span></td>
<td>
<a href="/matches/${match.id}/${fixtureId}" class="btn btn-sm btn-outline-primary">
......@@ -443,6 +463,8 @@ function updateMatchesTable(matches) {
const startTimeDisplay = match.start_time ? new Date(match.start_time).toLocaleString() : 'Not set';
const endTimeDisplay = match.end_time ? new Date(match.end_time).toLocaleString() : 'Not set';
const resultDisplay = match.result || 'Not available';
const winningOutcomesDisplay = formatWinningOutcomes(match.winning_outcomes);
const underOverDisplay = match.under_over_result || 'Not available';
const outcomesCount = match.outcome_count || 0;
const newRowHTML = `
......@@ -456,6 +478,8 @@ function updateMatchesTable(matches) {
<td><small class="text-info">${startTimeDisplay}</small></td>
<td><small class="text-success">${endTimeDisplay}</small></td>
<td><small class="text-muted">${resultDisplay}</small></td>
<td><small class="text-primary">${winningOutcomesDisplay}</small></td>
<td><small class="text-warning">${underOverDisplay}</small></td>
<td><span class="badge bg-light text-dark">${outcomesCount} outcomes</span></td>
<td>
<a href="/matches/${match.id}/${fixtureId}" class="btn btn-sm btn-outline-primary">
......
......@@ -84,6 +84,14 @@
<td><strong>Result:</strong></td>
<td><span id="result" class="text-muted">Not available</span></td>
</tr>
<tr id="winning-outcomes-row" style="display: none;">
<td><strong>Winning Outcomes:</strong></td>
<td><span id="winning-outcomes" class="badge bg-success"></span></td>
</tr>
<tr id="under-over-row" style="display: none;">
<td><strong>Under/Over:</strong></td>
<td><span id="under-over-result" class="badge bg-info"></span></td>
</tr>
<tr>
<td><strong>Fixture ID:</strong></td>
<td><small class="text-muted" id="fixture-id"></small></td>
......@@ -283,6 +291,20 @@ function showError(message) {
document.getElementById('error-message').style.display = 'block';
}
function formatWinningOutcomes(winningOutcomes) {
// Safely format winning outcomes, handling cases where it's not an array
if (Array.isArray(winningOutcomes) && winningOutcomes.length > 0) {
return winningOutcomes.join(', ');
} else if (winningOutcomes && typeof winningOutcomes === 'string') {
return winningOutcomes;
} else if (winningOutcomes && typeof winningOutcomes === 'object') {
// If it's an object, try to stringify it
return JSON.stringify(winningOutcomes);
} else {
return '';
}
}
function updateMatchDetails(match) {
// Update status badge with highlighting
const statusBadge = document.getElementById('match-status-badge');
......@@ -336,6 +358,42 @@ function updateMatchDetails(match) {
}
}
// Update winning outcomes if changed
const winningOutcomesRow = document.getElementById('winning-outcomes-row');
const winningOutcomesEl = document.getElementById('winning-outcomes');
const currentWinningOutcomes = formatWinningOutcomes(match.winning_outcomes);
if (currentWinningOutcomes !== winningOutcomesEl.textContent) {
if (currentWinningOutcomes) {
winningOutcomesEl.textContent = currentWinningOutcomes;
winningOutcomesRow.style.display = 'table-row';
winningOutcomesEl.style.backgroundColor = '#d1ecf1';
setTimeout(() => {
winningOutcomesEl.style.backgroundColor = '';
}, 1000);
} else {
winningOutcomesRow.style.display = 'none';
}
}
// Update under/over result if changed
const underOverRow = document.getElementById('under-over-row');
const underOverEl = document.getElementById('under-over-result');
const currentUnderOver = match.under_over_result || '';
if (currentUnderOver !== underOverEl.textContent) {
if (currentUnderOver) {
underOverEl.textContent = currentUnderOver;
underOverRow.style.display = 'table-row';
underOverEl.style.backgroundColor = '#d1ecf1';
setTimeout(() => {
underOverEl.style.backgroundColor = '';
}, 1000);
} else {
underOverRow.style.display = 'none';
}
}
// Update upload status
const uploadStatusEl = document.getElementById('upload-status');
const newUploadStatus = getUploadStatusBadge(match);
......@@ -388,6 +446,19 @@ function renderMatchDetails(match) {
document.getElementById('result').classList.remove('text-muted');
}
// Winning outcomes
const winningOutcomesText = formatWinningOutcomes(match.winning_outcomes);
if (winningOutcomesText) {
document.getElementById('winning-outcomes').textContent = winningOutcomesText;
document.getElementById('winning-outcomes-row').style.display = 'table-row';
}
// Under/Over result
if (match.under_over_result) {
document.getElementById('under-over-result').textContent = match.under_over_result;
document.getElementById('under-over-row').style.display = 'table-row';
}
// File information
document.getElementById('filename').textContent = match.filename;
document.getElementById('file-sha1sum').textContent = match.file_sha1sum;
......
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