Version 1.2.8: Offline CDN Fallback & Enhanced Cashier Interface

Features:
- Added comprehensive offline CDN fallback system with local Bootstrap and FontAwesome
- Enhanced cashier interface with streamlined navigation (logo + clock + user menu only)
- Implemented automatic CDN-to-local resource switching for network environments
- Added PyInstaller integration for offline functionality in executable builds

Technical Improvements:
- Fixed FontAwesome icon display issues with proper CDN priority and local fallback
- Optimized cashier navbar layout for focused, distraction-free interface
- Added local vendor directory with complete Bootstrap 5.3.0 and FontAwesome 6.0.0
- Implemented JavaScript-based resource availability detection
- Enhanced PyInstaller build configuration for automatic vendor file inclusion

Documentation Updates:
- Updated README.md with version 1.2.8 improvements
- Enhanced CHANGELOG.md with detailed feature descriptions
- Added comprehensive offline capabilities section to DOCUMENTATION.md
- Documented cashier interface navigation and access control

Files Modified:
- mbetterclient/web_dashboard/templates/base.html (CDN fallback, navbar layout)
- mbetterclient/web_dashboard/static/vendor/ (new directory with CDN resources)
- CHANGELOG.md, README.md, DOCUMENTATION.md (documentation updates)
- build.py (PyInstaller integration verified)

This release ensures MbetterClient works seamlessly in offline network environments while providing a cleaner, more focused cashier interface.
parent d28eb70d
......@@ -2,13 +2,36 @@
All notable changes to this project will be documented in this file.
## [1.2.8] - 2025-08-26
### Added
- **Offline CDN Fallback System**: Local copies of Bootstrap CSS/JS and FontAwesome with automatic fallback for offline networks
- **Enhanced Cashier Interface**: Streamlined navigation bar with clock and user menu positioned on the right side
- **PyInstaller CDN Integration**: All vendor files automatically included in executable builds for offline functionality
### Fixed
- **FontAwesome Icon Display**: Resolved icon rendering issues by prioritizing CDN with local fallback
- **Cashier Navigation Layout**: Fixed navbar positioning to show essential elements (clock, user menu) on right side
- **CDN Resource Loading**: Implemented proper fallback mechanism for offline network environments
### Enhanced
- **Web Dashboard Performance**: Optimized resource loading with CDN priority and local fallback
- **Cross-Platform Compatibility**: Enhanced offline functionality across Windows, Linux, and macOS
- **User Experience**: Cleaner cashier interface focused on essential controls
### Technical Details
- **CDN Fallback Implementation**: Added local vendor directory with Bootstrap and FontAwesome resources
- **Navbar Layout Optimization**: Streamlined cashier dashboard with essential elements only
- **PyInstaller Integration**: Automatic inclusion of all static resources in executable builds
- **Offline Network Support**: Application functions completely without internet connectivity
## [1.2.7] - 2025-08-26
### Added
- **Betting Mode Configuration**: Complete betting mode management system with web dashboard integration
- **Database Migration System**: Migration_015_AddBettingModeTable for betting_modes table with proper schema
- **Betting Mode API**: RESTful endpoints for getting and setting user betting modes
- **Configuration UI Enhancement**: Added betting mode settings section to configuration page
- **Global Betting Mode Configuration**: Complete global betting mode management system determining how START_GAME affects match status
- **Database Migration System**: Migration_016_ConvertBettingModeToGlobal for migrating from user-specific to global system setting
- **Global Betting Mode API**: RESTful endpoints for getting and setting global betting mode (admin only)
- **Configuration UI Enhancement**: Added global betting mode settings section to configuration page with admin restrictions
### Fixed
- **API Client Health Monitoring**: Resolved "Unhealthy components detected: ['api_client']" warning
......@@ -16,19 +39,22 @@ All notable changes to this project will be documented in this file.
- **Thread Health Checks**: Enhanced error handling to prevent thread hangs and maintain health status
- **Database Operation Protection**: Added heartbeat calls during potentially slow database operations
- **HTTP Request Protection**: Added heartbeat calls before and after HTTP requests to prevent timeouts
- **Betting Mode Architecture**: Converted from per-user to global system setting for proper game flow control
### Enhanced
- **Error Handling**: Improved API client error handling with heartbeat updates even during failures
- **Logging**: Added debug logging for heartbeat updates during error handling
- **Performance**: Optimized heartbeat system to minimize overhead while ensuring health monitoring
- **Configuration Persistence**: Enhanced betting mode configuration with database persistence
- **Global Configuration**: Betting mode now stored in game_config table as system setting
- **Admin Controls**: Betting mode configuration restricted to admin users only
### Technical Details
- **API Client Heartbeat**: Added heartbeat calls in main loop, HTTP requests, database operations, and error handling
- **Betting Mode Model**: Created BettingModeModel with user-specific settings and database constraints
- **Migration System**: Added Migration_015 with proper SQLite schema and indexing
- **Web Dashboard Integration**: Complete betting mode UI with real-time feedback and validation
- **Cross-Platform Compatibility**: Betting mode system works consistently across all supported platforms
- **Global Betting Mode**: Converted from BettingModeModel to GameConfigModel with system-wide configuration
- **Migration System**: Added Migration_016 to convert existing user settings to global configuration
- **Games Thread Integration**: Updated games thread to use global betting mode from game_config table
- **Web Dashboard Integration**: Enhanced betting mode UI with global admin-only controls and clear system-wide impact messaging
- **Cross-Platform Compatibility**: Global betting mode system works consistently across all supported platforms
## [1.2.6] - 2025-08-26
......
......@@ -160,33 +160,37 @@ Edit configuration through the web dashboard or modify JSON files directly:
}
```
### Betting Mode Configuration
### Global Betting Mode Configuration
The application supports two betting modes that can be configured per user:
The application supports two global betting modes that determine how START_GAME messages affect match status:
#### Betting Modes
- **All Bets on Start**: Place all bets simultaneously when games begin (default)
- **One Bet at a Time**: Place bets individually for more controlled betting
- **All Bets on Start**: When START_GAME is triggered, ALL matches in the fixture become 'bet' status (default)
- **One Bet at a Time**: When START_GAME is triggered, only the first match becomes 'bet', others remain 'scheduled'
#### Configuration
Betting mode is configured through the web dashboard:
1. Navigate to Configuration → Betting Mode Settings
#### Configuration (Admin Only)
Global betting mode is configured through the web dashboard by administrators:
1. Navigate to Configuration → Global Betting Mode Settings (Admin Only)
2. Select desired betting mode from dropdown
3. Click "Save Betting Mode"
4. Settings are saved per user and persist across sessions
3. Click "Save Global Betting Mode"
4. Settings are saved globally and affect all users and game sessions
#### API Configuration
#### System Architecture
The global betting mode is stored in the `game_config` table as a system setting:
```json
{
"betting_mode": {
"user_id": 1,
"mode": "all_bets_on_start",
"created_at": "2025-08-26T11:30:00Z",
"updated_at": "2025-08-26T11:30:00Z"
}
"config_key": "betting_mode",
"config_value": "all_bets_on_start",
"value_type": "string",
"description": "Global betting mode: all_bets_on_start or one_bet_at_a_time",
"is_system": true
}
```
#### Impact on Game Flow
- **All Bets on Start**: START_GAME → pending → scheduled → ALL matches become 'bet'
- **One Bet at a Time**: START_GAME → pending → scheduled → FIRST match becomes 'bet', others stay 'scheduled'
### Environment Variables
Create a `.env` file in the project root:
......@@ -215,6 +219,58 @@ FLASK_ENV=production
FLASK_DEBUG=False
```
## Offline Capabilities
### CDN Fallback System
MbetterClient includes a comprehensive offline capability system that ensures the web interface functions completely without internet connectivity. The application includes local copies of all CDN resources with automatic fallback mechanisms.
#### Included Resources
- **Bootstrap CSS/JS**: Complete Bootstrap 5.3.0 framework (227KB CSS, 80KB JS)
- **FontAwesome**: Full FontAwesome 6.0.0 icon library with all webfonts (89KB CSS, 123KB+ webfonts)
- **Automatic Fallback**: JavaScript-based detection with seamless CDN-to-local switching
#### Fallback Mechanism
The application uses a multi-tier fallback system:
1. **Primary**: CDN resources for optimal performance
2. **Secondary**: Local resources when CDN fails
3. **Detection**: JavaScript-based availability checking
4. **Graceful Degradation**: Works completely offline
#### PyInstaller Integration
All vendor files are automatically included in PyInstaller builds:
- Located in `mbetterclient/web_dashboard/static/vendor/`
- Automatically detected by `build.py` data collection
- No additional configuration required
### Cashier Interface
The cashier dashboard provides a streamlined interface optimized for cashiers:
#### Navigation Layout
- **Left Side**: MbetterClient logo/brand only
- **Right Side**: Server time digital clock + User dropdown menu
- **Hidden Elements**: All other navigation links (Dashboard, Video Control, Templates, etc.)
#### Features
- **Focused Interface**: Clean, distraction-free environment
- **Essential Controls**: Start Games, Video Display, Overlay Update
- **Real-time Status**: Match status, system status, activity counters
- **Server Time Clock**: Prominent 24-hour format clock
- **User Management**: Change password, logout functionality
#### Access Control
- **Role-based Access**: Restricted to users with 'cashier' role
- **Automatic Redirect**: Non-cashier users redirected to main dashboard
- **Permission Validation**: Server-side role checking on all cashier routes
## Usage Guide
### Command Line Usage
......@@ -716,7 +772,7 @@ Content-Type: application/json
}
```
#### Get Betting Mode
#### Get Global Betting Mode
```http
GET /api/betting-mode
......@@ -728,11 +784,12 @@ Authorization: Bearer <token>
{
"success": true,
"betting_mode": "all_bets_on_start",
"user_id": 1
"is_default": false,
"description": "Global system setting that determines how START_GAME affects match status"
}
```
#### Update Betting Mode
#### Update Global Betting Mode (Admin Only)
```http
POST /api/betting-mode
......@@ -748,11 +805,19 @@ Content-Type: application/json
```json
{
"success": true,
"message": "Betting mode updated successfully",
"message": "Global betting mode set to 'one_bet_at_a_time'",
"betting_mode": "one_bet_at_a_time"
}
```
**Error Response (Non-Admin):**
```json
{
"success": false,
"error": "Admin access required"
}
```
## Match Timer System
The application includes a comprehensive match timer system with automatic match progression, visual countdown displays, and command-line timer configuration.
......
......@@ -24,6 +24,15 @@ A cross-platform multimedia client application with video playback, web dashboar
## Recent Improvements
### Version 1.2.8 (August 2025)
-**Offline CDN Fallback System**: Local copies of Bootstrap CSS/JS and FontAwesome with automatic fallback for offline networks
-**Enhanced Cashier Interface**: Streamlined navigation bar with clock and user menu positioned on the right side
-**PyInstaller CDN Integration**: All vendor files automatically included in executable builds for offline functionality
-**FontAwesome Icon Display**: Resolved icon rendering issues by prioritizing CDN with local fallback
-**Cashier Navigation Layout**: Fixed navbar positioning to show essential elements (clock, user menu) on right side
-**CDN Resource Loading**: Implemented proper fallback mechanism for offline network environments
### Version 1.2.7 (August 2025)
-**API Client Health Monitoring**: Fixed "Unhealthy components detected: ['api_client']" warning by implementing comprehensive heartbeat system
......
......@@ -833,20 +833,22 @@ class GamesThread(ThreadedComponent):
logger.error(f"Failed to rollback: {rollback_e}")
def _get_betting_mode_config(self) -> str:
"""Get betting mode configuration from database (default: 'all_bets_on_start')"""
"""Get global betting mode configuration from game config (default: 'all_bets_on_start')"""
try:
session = self.db_manager.get_session()
try:
from ..database.models import BettingModeModel
from ..database.models import GameConfigModel
# Get the first betting mode configuration (assuming it's system-wide)
# If user-specific, this would need to be modified to get user context
betting_mode_entry = session.query(BettingModeModel).first()
# Get global betting mode configuration from game_config table
betting_mode_config = session.query(GameConfigModel).filter_by(
config_key='betting_mode'
).first()
if betting_mode_entry:
return betting_mode_entry.mode
if betting_mode_config:
return betting_mode_config.get_typed_value()
else:
# Default to 'all_bets_on_start' if no configuration found
logger.debug("No betting mode configuration found, using default: 'all_bets_on_start'")
return 'all_bets_on_start'
finally:
......
......@@ -1031,6 +1031,130 @@ class Migration_015_AddBettingModeTable(DatabaseMigration):
return False
class Migration_016_ConvertBettingModeToGlobal(DatabaseMigration):
"""Convert betting mode from user-specific to global system setting"""
def __init__(self):
super().__init__("016", "Convert betting mode from user-specific to global system setting")
def up(self, db_manager) -> bool:
"""Migrate user betting modes to global game config and remove betting_modes table"""
try:
with db_manager.engine.connect() as conn:
# Step 1: Check if betting_modes table exists
result = conn.execute(text("""
SELECT name FROM sqlite_master
WHERE type='table' AND name='betting_modes'
"""))
betting_modes_exists = result.fetchone() is not None
if betting_modes_exists:
# Step 2: Get existing betting mode configurations
result = conn.execute(text("""
SELECT bm.mode, u.role, COUNT(*) as count
FROM betting_modes bm
JOIN users u ON bm.user_id = u.id
GROUP BY bm.mode, u.role
ORDER BY
CASE WHEN u.role = 'admin' THEN 1 ELSE 2 END,
count DESC
"""))
existing_modes = result.fetchall()
# Determine global setting: prefer admin user's choice, then most common
global_mode = 'all_bets_on_start' # Default
if existing_modes:
global_mode = existing_modes[0][0] # First result (admin preference or most common)
logger.info(f"Found existing betting modes, setting global mode to: {global_mode}")
else:
logger.info("No existing betting mode configurations found, using default: all_bets_on_start")
# Step 3: Add global betting mode to game_config
conn.execute(text("""
INSERT OR REPLACE INTO game_config
(config_key, config_value, value_type, description, is_system, created_at, updated_at)
VALUES (:config_key, :config_value, :value_type, :description, :is_system, datetime('now'), datetime('now'))
"""), {
'config_key': 'betting_mode',
'config_value': global_mode,
'value_type': 'string',
'description': 'Global betting mode: all_bets_on_start or one_bet_at_a_time',
'is_system': True
})
# Step 4: Drop the betting_modes table
conn.execute(text("DROP TABLE betting_modes"))
logger.info("Removed user-specific betting_modes table")
else:
# Table doesn't exist, just add the global config
conn.execute(text("""
INSERT OR IGNORE INTO game_config
(config_key, config_value, value_type, description, is_system, created_at, updated_at)
VALUES (:config_key, :config_value, :value_type, :description, :is_system, datetime('now'), datetime('now'))
"""), {
'config_key': 'betting_mode',
'config_value': 'all_bets_on_start',
'value_type': 'string',
'description': 'Global betting mode: all_bets_on_start or one_bet_at_a_time',
'is_system': True
})
logger.info("Added default global betting mode configuration")
conn.commit()
logger.info("Successfully converted betting mode to global system setting")
return True
except Exception as e:
logger.error(f"Failed to convert betting mode to global: {e}")
return False
def down(self, db_manager) -> bool:
"""Restore user-specific betting mode table"""
try:
with db_manager.engine.connect() as conn:
# Step 1: Get current global betting mode
result = conn.execute(text("""
SELECT config_value FROM game_config WHERE config_key = 'betting_mode'
"""))
global_mode_row = result.fetchone()
global_mode = global_mode_row[0] if global_mode_row else 'all_bets_on_start'
# Step 2: Recreate betting_modes table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS betting_modes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id INTEGER NOT NULL UNIQUE,
mode VARCHAR(50) NOT NULL DEFAULT 'all_bets_on_start',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE
)
"""))
# Step 3: Create index
conn.execute(text("""
CREATE INDEX IF NOT EXISTS ix_betting_modes_user_id ON betting_modes(user_id)
"""))
# Step 4: Set all existing users to use the global mode
conn.execute(text("""
INSERT INTO betting_modes (user_id, mode, created_at, updated_at)
SELECT id, :mode, datetime('now'), datetime('now')
FROM users
"""), {'mode': global_mode})
# Step 5: Remove global betting mode config
conn.execute(text("DELETE FROM game_config WHERE config_key = 'betting_mode'"))
conn.commit()
logger.info("Restored user-specific betting mode table")
return True
except Exception as e:
logger.error(f"Failed to restore user-specific betting mode: {e}")
return False
# Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(),
......@@ -1048,6 +1172,7 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_013_AddStatusFieldToMatches(),
Migration_014_AddExtractionAndGameConfigTables(),
Migration_015_AddBettingModeTable(),
Migration_016_ConvertBettingModeToGlobal(),
]
......
......@@ -96,7 +96,6 @@ class UserModel(BaseModel):
# Relationships
api_tokens = relationship('ApiTokenModel', back_populates='user', cascade='all, delete-orphan')
log_entries = relationship('LogEntryModel', back_populates='user')
betting_mode = relationship('BettingModeModel', back_populates='user', cascade='all, delete-orphan', uselist=False)
def set_password(self, password: str):
"""Set password hash using SHA-256 with salt (consistent with AuthManager)"""
......@@ -661,20 +660,3 @@ class GameConfigModel(BaseModel):
def __repr__(self):
return f'<GameConfig {self.config_key}={self.config_value}>'
class BettingModeModel(BaseModel):
"""Betting mode configuration"""
__tablename__ = 'betting_modes'
__table_args__ = (
Index('ix_betting_modes_user_id', 'user_id'),
UniqueConstraint('user_id', name='uq_betting_modes_user_id'),
)
user_id = Column(Integer, ForeignKey('users.id'), nullable=False, unique=True, comment='User who owns this betting mode setting')
mode = Column(String(50), nullable=False, default='all_bets_on_start', comment='Betting mode: all_bets_on_start or one_bet_at_a_time')
# Relationships
user = relationship('UserModel', back_populates='betting_mode')
def __repr__(self):
return f'<BettingMode user_id={self.user_id} mode={self.mode}>'
\ No newline at end of file
......@@ -2078,30 +2078,30 @@ def update_extraction_config():
@api_bp.route('/betting-mode')
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def get_betting_mode():
"""Get current user's betting mode"""
"""Get global betting mode configuration"""
try:
from ..database.models import BettingModeModel
user_id = getattr(current_user, 'id', None) or getattr(request, 'current_user', {}).get('user_id')
if not user_id:
return jsonify({"error": "User not authenticated"}), 401
from ..database.models import GameConfigModel
session = api_bp.db_manager.get_session()
try:
# Get user's betting mode
betting_mode = session.query(BettingModeModel).filter_by(user_id=user_id).first()
if betting_mode:
mode = betting_mode.mode
# Get global betting mode configuration
betting_mode_config = session.query(GameConfigModel).filter_by(
config_key='betting_mode'
).first()
if betting_mode_config:
mode = betting_mode_config.get_typed_value()
is_default = False
else:
# Return default if not set
mode = 'all_bets_on_start'
is_default = True
return jsonify({
"success": True,
"betting_mode": mode,
"is_default": betting_mode is None
"is_default": is_default,
"description": "Global system setting that determines how START_GAME affects match status"
})
finally:
......@@ -2114,15 +2114,11 @@ def get_betting_mode():
@api_bp.route('/betting-mode', methods=['POST'])
@api_bp.auth_manager.require_auth if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
@api_bp.auth_manager.require_admin if hasattr(api_bp, 'auth_manager') and api_bp.auth_manager else login_required
def set_betting_mode():
"""Set current user's betting mode"""
"""Set global betting mode configuration (admin only)"""
try:
from ..database.models import BettingModeModel
user_id = getattr(current_user, 'id', None) or getattr(request, 'current_user', {}).get('user_id')
if not user_id:
return jsonify({"error": "User not authenticated"}), 401
from ..database.models import GameConfigModel
data = request.get_json() or {}
mode = data.get('betting_mode')
......@@ -2137,28 +2133,33 @@ def set_betting_mode():
session = api_bp.db_manager.get_session()
try:
# Check if betting mode already exists for user
existing_mode = session.query(BettingModeModel).filter_by(user_id=user_id).first()
# Check if betting mode config already exists
existing_config = session.query(GameConfigModel).filter_by(
config_key='betting_mode'
).first()
if existing_mode:
# Update existing mode
existing_mode.mode = mode
existing_mode.updated_at = datetime.utcnow()
if existing_config:
# Update existing config
existing_config.set_typed_value(mode)
existing_config.updated_at = datetime.utcnow()
else:
# Create new betting mode
new_mode = BettingModeModel(
user_id=user_id,
mode=mode
# Create new global betting mode config
new_config = GameConfigModel(
config_key='betting_mode',
config_value=mode,
value_type='string',
description='Global betting mode: all_bets_on_start or one_bet_at_a_time',
is_system=True
)
session.add(new_mode)
session.add(new_config)
session.commit()
logger.info(f"Updated betting mode for user {user_id}: {mode}")
logger.info(f"Updated global betting mode: {mode}")
return jsonify({
"success": True,
"message": f"Betting mode set to '{mode}'",
"message": f"Global betting mode set to '{mode}'",
"betting_mode": mode
})
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -5,9 +5,15 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}{{ page_title | default("Dashboard") }} - {{ app_name }}{% endblock %}</title>
<!-- CSS from CDN -->
<!-- CSS with local fallback -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css">
<noscript>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/bootstrap/css/bootstrap.min.css') }}">
</noscript>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<noscript>
<link rel="stylesheet" href="{{ url_for('static', filename='vendor/fontawesome/all.min.css') }}">
</noscript>
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
<style>
......@@ -71,9 +77,10 @@
</button>
<div class="collapse navbar-collapse" id="navbarNav">
{% if request.endpoint != 'main.cashier_dashboard' %}
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.index' %}active{% endif %}"
<a class="nav-link {% if request.endpoint == 'main.index' %}active{% endif %}"
href="{{ url_for('main.index') }}">
<i class="fas fa-tachometer-alt me-1"></i>Dashboard
</a>
......@@ -102,12 +109,6 @@
<i class="fas fa-cogs me-1"></i>Extraction
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.extraction' %}active{% endif %}"
href="{{ url_for('main.extraction') }}">
<i class="fas fa-cogs me-1"></i>Extraction
</a>
</li>
<li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.video_test' %}active{% endif %}"
href="{{ url_for('main.video_test') }}">
......@@ -145,31 +146,33 @@
</li>
{% endif %}
</ul>
<!-- Digital Clock -->
<div class="d-flex align-items-center me-3">
<div id="digital-clock" class="navbar-clock">
<i class="fas fa-clock me-2"></i>
<span id="clock-time">--:--:--</span>
{% endif %}
<!-- Digital Clock and User Menu positioned on the right -->
<div class="ms-auto d-flex align-items-center">
<div class="d-flex align-items-center me-3">
<div id="digital-clock" class="navbar-clock">
<i class="fas fa-clock me-2"></i>
<span id="clock-time">--:--:--</span>
</div>
</div>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user me-1"></i>{{ current_user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('auth.change_password') }}">
<i class="fas fa-lock me-1"></i>Change Password
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt me-1"></i>Logout
</a></li>
</ul>
</li>
</ul>
</div>
<ul class="navbar-nav">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
<i class="fas fa-user me-1"></i>{{ current_user.username }}
</a>
<ul class="dropdown-menu dropdown-menu-end">
<li><a class="dropdown-item" href="{{ url_for('auth.change_password') }}">
<i class="fas fa-lock me-1"></i>Change Password
</a></li>
<li><hr class="dropdown-divider"></li>
<li><a class="dropdown-item" href="{{ url_for('auth.logout') }}">
<i class="fas fa-sign-out-alt me-1"></i>Logout
</a></li>
</ul>
</li>
</ul>
</div>
</div>
</nav>
......@@ -226,8 +229,14 @@
</div>
{% endif %}
<!-- JavaScript from CDN and local -->
<!-- JavaScript with local fallback -->
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
// Load local fallback if CDN fails
if (typeof bootstrap === 'undefined') {
document.write('<script src="{{ url_for("static", filename="vendor/bootstrap/js/bootstrap.bundle.min.js") }}"><\/script>');
}
</script>
<script src="{{ url_for('static', filename='js/dashboard.js') }}"></script>
{% if current_user.is_authenticated %}
......
......@@ -215,22 +215,26 @@
</div>
</div>
<!-- Betting Mode Settings -->
<!-- Global Betting Mode Settings (Admin Only) -->
<div class="card mb-4">
<div class="card-header">
<h5>Betting Mode Settings</h5>
<h5><i class="fas fa-cog"></i> Global Betting Mode Settings</h5>
<small class="text-muted">Admin Only - System-wide Configuration</small>
</div>
<div class="card-body">
<form id="betting-mode-config-form">
<div class="mb-3">
<label for="betting-mode" class="form-label">Betting Mode</label>
<label for="betting-mode" class="form-label">Global Betting Mode</label>
<select class="form-select" id="betting-mode">
<option value="all_bets_on_start">All Bets on Start - Place all bets when games begin</option>
<option value="one_bet_at_a_time">One Bet at a Time - Place bets individually</option>
<option value="all_bets_on_start">All Bets on Start - All matches become 'bet' status when START_GAME is triggered</option>
<option value="one_bet_at_a_time">One Bet at a Time - Only first match becomes 'bet', others remain 'scheduled'</option>
</select>
<div class="form-text">Choose how bets are placed during games</div>
<div class="form-text">
<strong>Global System Setting:</strong> Determines how START_GAME messages affect match status across all fixtures.
<br><small class="text-muted">This affects all users and all game sessions.</small>
</div>
</div>
<button type="submit" class="btn btn-primary">Save Betting Mode</button>
<button type="submit" class="btn btn-primary">Save Global Betting Mode</button>
</form>
<div id="betting-mode-status" class="mt-3"></div>
</div>
......@@ -478,12 +482,12 @@
});
});
// Load current betting mode on page load
// Load current global betting mode on page load
document.addEventListener('DOMContentLoaded', function() {
loadBettingMode();
});
// Load betting mode configuration
// Load global betting mode configuration
function loadBettingMode() {
fetch('/api/betting-mode')
.then(response => response.json())
......@@ -493,22 +497,25 @@
if (data.is_default) {
document.getElementById('betting-mode-status').innerHTML =
'<div class="alert alert-info"><small>Using default betting mode</small></div>';
'<div class="alert alert-info"><small><i class="fas fa-info-circle"></i> Using default global betting mode</small></div>';
} else {
document.getElementById('betting-mode-status').innerHTML =
'<div class="alert alert-success"><small><i class="fas fa-check-circle"></i> Global betting mode configured</small></div>';
}
} else {
console.error('Failed to load betting mode:', data.error);
document.getElementById('betting-mode-status').innerHTML =
'<div class="alert alert-warning"><small>Could not load current betting mode</small></div>';
'<div class="alert alert-warning"><small><i class="fas fa-exclamation-triangle"></i> Could not load current betting mode</small></div>';
}
})
.catch(error => {
console.error('Error loading betting mode:', error);
document.getElementById('betting-mode-status').innerHTML =
'<div class="alert alert-warning"><small>Error loading betting mode</small></div>';
'<div class="alert alert-warning"><small><i class="fas fa-exclamation-triangle"></i> Error loading betting mode</small></div>';
});
}
// Save betting mode configuration
// Save global betting mode configuration
document.getElementById('betting-mode-config-form').addEventListener('submit', function(e) {
e.preventDefault();
......@@ -518,7 +525,7 @@
// Clear previous status
statusDiv.innerHTML = '';
// Save betting mode
// Save global betting mode
fetch('/api/betting-mode', {
method: 'POST',
headers: {
......@@ -531,19 +538,19 @@
.then(response => response.json())
.then(data => {
if (data.success) {
statusDiv.innerHTML = '<div class="alert alert-success">Betting mode saved successfully: ' + data.message + '</div>';
statusDiv.innerHTML = '<div class="alert alert-success"><i class="fas fa-check-circle"></i> ' + data.message + '<br><small>This setting affects all users and game sessions.</small></div>';
// Auto-hide success message after 3 seconds
// Auto-hide success message after 5 seconds
setTimeout(() => {
statusDiv.innerHTML = '';
}, 3000);
statusDiv.innerHTML = '<div class="alert alert-success"><small><i class="fas fa-check-circle"></i> Global betting mode configured</small></div>';
}, 5000);
} else {
statusDiv.innerHTML = '<div class="alert alert-danger">Failed to save betting mode: ' + data.error + '</div>';
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Failed to save global betting mode: ' + data.error + '</div>';
}
})
.catch(error => {
console.error('Error saving betting mode:', error);
statusDiv.innerHTML = '<div class="alert alert-danger">Error saving betting mode: ' + error.message + '</div>';
statusDiv.innerHTML = '<div class="alert alert-danger"><i class="fas fa-exclamation-circle"></i> Error saving global betting mode: ' + error.message + '</div>';
});
});
</script>
......
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