Top bar on the overlays

parent 27663e61
......@@ -2615,6 +2615,68 @@ class Migration_033_AddBarcodeFieldsToBets(DatabaseMigration):
return True
class Migration_034_AddDefaultLicenseText(DatabaseMigration):
"""Add default license text configuration for overlay templates"""
def __init__(self):
super().__init__("034", "Add default license text configuration for overlay templates")
def up(self, db_manager) -> bool:
"""Add default license text to configuration table"""
try:
with db_manager.engine.connect() as conn:
# Check if license text configuration already exists
result = conn.execute(text("""
SELECT COUNT(*) FROM configuration WHERE key = 'license_text'
"""))
exists = result.scalar() > 0
if not exists:
default_license_text = 'LICENSED BY THE NATIONAL LOTTERIES AND GAMING REGULATORY BOARD - LICENSE NUMBER NLGBR-GB-82-006. Gambling can be addictive and psychologically harmful not allowed for minors'
conn.execute(text("""
INSERT INTO configuration
(key, value, value_type, description, is_system, created_at, updated_at)
VALUES (:key, :value, :value_type, :description, :is_system, datetime('now'), datetime('now'))
"""), {
'key': 'license_text',
'value': default_license_text,
'value_type': 'string',
'description': 'License text displayed on overlay templates',
'is_system': False
})
logger.info("Added default license text configuration")
else:
logger.info("License text configuration already exists")
conn.commit()
logger.info("Default license text configuration migration completed successfully")
return True
except Exception as e:
logger.error(f"Failed to add default license text configuration: {e}")
return False
def down(self, db_manager) -> bool:
"""Remove default license text configuration"""
try:
with db_manager.engine.connect() as conn:
# Remove the license text configuration
conn.execute(text("""
DELETE FROM configuration WHERE key = 'license_text'
"""))
conn.commit()
logger.info("Default license text configuration removed")
return True
except Exception as e:
logger.error(f"Failed to remove default license text configuration: {e}")
return False
# Registry of all migrations in order
# Registry of all migrations in order
......@@ -2652,6 +2714,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_031_AddWinningOutcomesFields(),
Migration_032_FixExtractionAssociationDefaults(),
Migration_033_AddBarcodeFieldsToBets(),
Migration_034_AddDefaultLicenseText(),
]
......
......@@ -29,6 +29,31 @@
font-size: 10px;
z-index: 9999;
}
/* License Top Bar */
.license-bar {
position: absolute;
top: 0;
left: 0;
right: 0;
background: rgba(0, 123, 255, 0.40);
border-radius: 0 0 20px 20px;
padding: 15px 30px;
text-align: center;
z-index: 1000;
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.2);
border-top: none;
}
.license-text {
color: rgba(255, 255, 255, 0.95);
font-size: 16px;
font-weight: bold;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.6);
line-height: 1.4;
word-wrap: break-word;
}
.overlay-container {
position: absolute;
......@@ -211,6 +236,11 @@
</style>
</head>
<body>
<!-- License Top Bar -->
<div class="license-bar" id="licenseBar">
<div class="license-text" id="licenseText">Loading license text...</div>
</div>
<div class="overlay-container">
<div class="message-panel" id="messagePanel">
<div class="message-icon" id="messageIcon">📢</div>
......@@ -225,6 +255,7 @@
let currentTitle = 'Townships Combat League:';
let currentMessage = 'Waiting for game to start...';
let currentIcon = '🥊';
let licenseText = 'Loading license text...';
// Function to update overlay data (called by Qt WebChannel)
function updateOverlayData(data) {
......@@ -251,15 +282,48 @@
const titleElement = document.getElementById('messageTitle');
const contentElement = document.getElementById('messageContent');
const iconElement = document.getElementById('messageIcon');
// Update content
titleElement.textContent = currentTitle;
contentElement.textContent = currentMessage;
iconElement.textContent = currentIcon;
// Restart animations
restartAnimations();
}
// Fetch license text from API
async function fetchLicenseText() {
try {
// Try to fetch from the web dashboard API
const response = await fetch('http://127.0.0.1:5001/api/config/license-text');
if (response.ok) {
const data = await response.json();
if (data.success) {
licenseText = data.license_text || '';
updateLicenseDisplay();
}
}
} catch (error) {
console.log('Could not fetch license text from API, using default');
// Keep default text if API is not available
}
}
// Update license text display
function updateLicenseDisplay() {
const licenseBar = document.getElementById('licenseBar');
const licenseElement = document.getElementById('licenseText');
if (licenseElement && licenseBar) {
if (licenseText && licenseText.trim() !== '') {
licenseElement.textContent = licenseText;
licenseBar.style.display = 'block';
} else {
licenseBar.style.display = 'none';
}
}
}
// Restart animations
function restartAnimations() {
......@@ -283,6 +347,7 @@
document.addEventListener('DOMContentLoaded', function() {
console.log('Text message overlay initialized');
updateMessageDisplay();
fetchLicenseText();
});
// Qt WebChannel initialization (when available)
......
......@@ -115,6 +115,36 @@
animation: fadeInScale 1s ease-out forwards;
}
.results-bottom-bar {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(0, 123, 255, 0.85);
border-radius: 15px;
padding: 15px 20px;
max-width: 90%;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.1);
color: white;
font-size: 24px;
text-align: left;
z-index: 1001;
}
.results-title {
font-weight: bold;
margin-bottom: 10px;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.match-result {
font-size: 20px;
margin: 5px 0;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
}
.match-title {
font-size: 48px;
font-weight: bold;
......@@ -460,6 +490,14 @@
No matches available for betting
</div>
</div>
<!-- Results Bottom Bar -->
<div class="results-bottom-bar" id="resultsBottomBar" style="display: none;">
<div class="results-title">RESULTS of the last 3 matches:</div>
<div id="resultsContent">
<!-- Match results will be populated by JavaScript -->
</div>
</div>
</div>
<script>
......@@ -1129,6 +1167,9 @@
// Get timer state and start countdown
getTimerStateAndStartCountdown();
debugTime('Countdown initialization completed');
// Fetch and display last match results
fetchLastMatchResults();
}
// Show no matches message
......@@ -1139,6 +1180,92 @@
noMatches.textContent = message;
noMatches.style.display = 'block';
}
// Fetch and display last 3 match results
async function fetchLastMatchResults() {
try {
debugTime('Fetching last match results');
// Try to fetch from web API if webServerBaseUrl is available
if (webServerBaseUrl) {
const response = await fetch(`${webServerBaseUrl}/api/fixtures`);
if (response.ok) {
const fixturesData = await response.json();
if (fixturesData.success && fixturesData.fixtures) {
// Flatten all matches from all fixtures and filter completed ones
const allMatches = [];
fixturesData.fixtures.forEach(fixture => {
if (fixture.matches) {
fixture.matches.forEach(match => {
if (match.status === 'done' && match.result) {
allMatches.push(match);
}
});
}
});
// Sort by end_time or created_at descending (most recent first)
allMatches.sort((a, b) => {
const aTime = new Date(a.end_time || a.updated_at || a.created_at);
const bTime = new Date(b.end_time || b.updated_at || b.created_at);
return bTime - aTime;
});
// Take last 3 matches
const lastMatches = allMatches.slice(0, 3);
if (lastMatches.length > 0) {
renderMatchResults(lastMatches);
return;
}
}
}
}
// Fallback: try WebChannel if available
if (window.overlay && typeof window.overlay.getCompletedMatches === 'function') {
const completedMatchesJson = await window.overlay.getCompletedMatches();
const completedMatches = JSON.parse(completedMatchesJson);
if (completedMatches && completedMatches.length > 0) {
const lastMatches = completedMatches.slice(-3); // Last 3 matches
renderMatchResults(lastMatches);
return;
}
}
// No results available
debugTime('No match results available');
document.getElementById('resultsBottomBar').style.display = 'none';
} catch (error) {
console.log('DEBUG: Failed to fetch match results:', error);
document.getElementById('resultsBottomBar').style.display = 'none';
}
}
// Render match results in the bottom bar
function renderMatchResults(matches) {
const resultsContent = document.getElementById('resultsContent');
if (!resultsContent) return;
let resultsHTML = '';
matches.forEach(match => {
const matchNumber = match.match_number || match.id || 'N/A';
const fighter1 = match.fighter1_township || match.fighter1 || 'Fighter 1';
const fighter2 = match.fighter2_township || match.fighter2 || 'Fighter 2';
const result = match.result || 'Unknown';
const underOver = match.under_over_result || '';
const resultText = underOver ? `${result} (${underOver})` : result;
resultsHTML += `<div class="match-result">Match #${matchNumber}:: ${fighter1} vs ${fighter2}: ${resultText}</div>`;
});
resultsContent.innerHTML = resultsHTML;
document.getElementById('resultsBottomBar').style.display = 'block';
debugTime(`Rendered ${matches.length} match results`);
}
// Initialize when DOM is loaded
document.addEventListener('DOMContentLoaded', function() {
......
......@@ -1169,6 +1169,95 @@ def set_match_interval():
return jsonify({"error": str(e)}), 500
@api_bp.route('/config/license-text')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_license_text():
"""Get license text configuration"""
try:
if api_bp.db_manager:
session = api_bp.db_manager.get_session()
try:
from ..database.models import ConfigurationModel
config = session.query(ConfigurationModel).filter_by(key='license_text').first()
if config:
license_text = config.get_typed_value()
else:
# Return default if not set
license_text = 'LICENSED BY THE NATIONAL LOTTERIES AND GAMING REGULATORY BOARD - LICENSE NUMBER NLGBR-GB-82-006. Gambling can be addictive and psychologically harmful not allowed for minors'
return jsonify({
"success": True,
"license_text": license_text
})
finally:
session.close()
else:
return jsonify({"error": "Database manager not available"}), 500
except Exception as e:
logger.error(f"API get license text error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/config/license-text', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def set_license_text():
"""Set license text configuration"""
try:
data = request.get_json() or {}
license_text = data.get('license_text', '').strip()
if not license_text:
return jsonify({"error": "license_text is required"}), 400
# Validate length
if len(license_text) > 500:
return jsonify({"error": "License text must be 500 characters or less"}), 400
if api_bp.db_manager:
session = api_bp.db_manager.get_session()
try:
from ..database.models import ConfigurationModel
from datetime import datetime
# Check if config exists
config = session.query(ConfigurationModel).filter_by(key='license_text').first()
if config:
# Update existing config
config.set_typed_value(license_text)
config.updated_at = datetime.utcnow()
else:
# Create new config
config = ConfigurationModel(
key='license_text',
config_value=license_text,
value_type='string',
description='License text displayed on overlay templates',
is_system=False
)
session.add(config)
session.commit()
logger.info(f"License text updated: {len(license_text)} characters")
return jsonify({
"success": True,
"message": "License text updated successfully",
"license_text": license_text
})
finally:
session.close()
else:
return jsonify({"error": "Database manager not available"}), 500
except Exception as e:
logger.error(f"API set license text error: {e}")
return jsonify({"error": str(e)}), 500
@api_bp.route('/config/test-connection', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
......
......@@ -127,6 +127,35 @@
</div>
</div>
<!-- License Configuration -->
<div class="row mb-4">
<div class="col-12">
<div class="card">
<div class="card-header">
<h5 class="card-title mb-0">
<i class="fas fa-file-contract me-2"></i>License Text Configuration
</h5>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Overlay License Text</label>
<textarea class="form-control" id="license-text" rows="4" maxlength="500"
placeholder="Enter license text to display on overlays...">LICENSED BY THE NATIONAL LOTTERIES AND GAMING REGULATORY BOARD - LICENSE NUMBER NLGBR-GB-82-006. Gambling can be addictive and psychologically harmful not allowed for minors</textarea>
<div class="form-text">This text will be displayed as a top bar on all overlay templates. Maximum 500 characters.</div>
</div>
<div class="d-flex gap-2">
<button class="btn btn-outline-primary" id="btn-save-license">
<i class="fas fa-save me-1"></i>Save License Text
</button>
<button class="btn btn-outline-secondary" id="btn-reset-license">
<i class="fas fa-undo me-1"></i>Reset to Default
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Recent Activity & System Info -->
<div class="row">
<div class="col-md-8">
......@@ -427,6 +456,9 @@ document.addEventListener('DOMContentLoaded', function() {
// Load betting mode configuration
loadBettingMode();
// Load license text configuration
loadLicenseText();
// Quick action buttons
document.getElementById('btn-play-video').addEventListener('click', function() {
......@@ -458,6 +490,15 @@ document.addEventListener('DOMContentLoaded', function() {
document.getElementById('betting-mode-select').addEventListener('change', function() {
saveBettingMode(this.value);
});
// License text buttons
document.getElementById('btn-save-license').addEventListener('click', function() {
saveLicenseText();
});
document.getElementById('btn-reset-license').addEventListener('click', function() {
resetLicenseText();
});
// Admin shutdown button (only exists if user is admin)
const shutdownBtn = document.getElementById('btn-shutdown-app');
......@@ -841,7 +882,7 @@ function loadAvailableTemplates() {
// Clear loading options
videoTemplateSelect.innerHTML = '';
overlayTemplateSelect.innerHTML = '';
// Keep text as default and clear loading options for message template
messageTemplateSelect.innerHTML = '<option value="text" selected>Text Message</option>';
......@@ -878,7 +919,7 @@ function loadAvailableTemplates() {
if (defaultOverlayOption) {
defaultOverlayOption.selected = true;
}
// Text template should remain selected for messages
messageTemplateSelect.value = 'text';
} else {
......@@ -910,5 +951,66 @@ function loadAvailableTemplates() {
});
}
function loadLicenseText() {
fetch('/api/config/license-text')
.then(response => response.json())
.then(data => {
if (data.success && data.license_text) {
document.getElementById('license-text').value = data.license_text;
}
})
.catch(error => {
console.error('Error loading license text:', error);
// Keep default value if loading fails
});
}
function saveLicenseText() {
const licenseText = document.getElementById('license-text').value.trim();
if (licenseText.length > 500) {
alert('License text must be 500 characters or less');
return;
}
const saveBtn = document.getElementById('btn-save-license');
const originalText = saveBtn.innerHTML;
// Show loading state
saveBtn.disabled = true;
saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin me-1"></i>Saving...';
fetch('/api/config/license-text', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
license_text: licenseText
})
})
.then(response => response.json())
.then(data => {
if (data.success) {
alert('License text saved successfully!');
} else {
alert('Failed to save license text: ' + (data.error || 'Unknown error'));
}
})
.catch(error => {
alert('Error saving license text: ' + error.message);
})
.finally(() => {
// Restore button state
saveBtn.disabled = false;
saveBtn.innerHTML = originalText;
});
}
function resetLicenseText() {
const defaultText = 'LICENSED BY THE NATIONAL LOTTERIES AND GAMING REGULATORY BOARD - LICENSE NUMBER NLGBR-GB-82-006. Gambling can be addictive and psychologically harmful not allowed for minors';
document.getElementById('license-text').value = defaultText;
}
</script>
{% endblock %}
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