feat: Add extraction management page and server time digital clock

- Add comprehensive extraction management page with drag-and-drop interface
- Implement server time digital clock showing time from mbetterc machine
- Create database models for extraction associations and game configuration
- Add database migration for new tables with proper indexing
- Implement REST API endpoints for extraction management
- Add server time API endpoint for accurate time synchronization
- Update navigation to include extraction management page
- Add digital clock to all dashboard interfaces including cashier dashboard
- Implement default outcome associations (WIN1, X, WIN2)
- Add time limit configuration for UNDER/OVER outcomes
- Update documentation with comprehensive extraction system details
- Add troubleshooting section for extraction and clock features

Features:
- Drag-and-drop outcome association management
- Multi-association support for outcomes
- Trash bin removal functionality
- Real-time server time display (24-hour format)
- Cross-interface clock availability
- Database persistence for all settings
- Professional UI with responsive design
- Error handling and graceful fallbacks
parent 40636f79
...@@ -2,6 +2,42 @@ ...@@ -2,6 +2,42 @@
All notable changes to this project will be documented in this file. All notable changes to this project will be documented in this file.
## [1.2.5] - 2025-08-26
### Added
- **Extraction Management Page**: Complete drag-and-drop interface for managing outcome associations with extraction results
- **Server Time Digital Clock**: Prominent 24-hour format clock showing server time across all dashboard interfaces
- **Database Models for Extraction**: New `ExtractionAssociationModel` and `GameConfigModel` for storing outcome associations and game settings
- **Database Migration System**: Migration_014 for extraction associations and game configuration tables
- **API Endpoints for Extraction**: Complete REST API for managing outcome associations and game configuration
- **Drag & Drop Functionality**: Interactive outcome management with visual feedback and association persistence
- **Multi-Association Support**: Outcomes can be associated with multiple extraction results simultaneously
- **Trash Bin Removal**: Context-aware removal of associations based on drag source location
- **Game Configuration Panel**: Time limit settings for UNDER/OVER outcomes with database persistence
- **Server Time API**: `/api/server-time` endpoint providing server timestamp for accurate time display
- **Cross-Interface Clock**: Digital clock appears on all authenticated pages including cashier dashboard
### Enhanced
- **Web Dashboard Navigation**: Added extraction management page to main navigation menu
- **User Experience**: Silent drag-and-drop operations without alert interruptions
- **Time Synchronization**: Automatic server time sync every 30 seconds with offset calculation
- **Responsive Design**: Clock and extraction interface work seamlessly across all screen sizes
- **Error Handling**: Graceful fallbacks for server time API failures
### Fixed
- **Drag & Drop Detection**: Fixed trash bin drop zone detection with proper CSS classes
- **Multiple Associations**: Resolved issue preventing outcomes from being associated with multiple results
- **Alert Interruptions**: Removed popup alerts during drag-and-drop operations for smoother workflow
- **Clock Positioning**: Optimized clock placement in cashier dashboard navbar
### Technical Details
- **Database Schema**: Added extraction_associations and game_config tables with proper indexing
- **API Integration**: RESTful endpoints for CRUD operations on extraction data
- **JavaScript Architecture**: Modular drag-and-drop system with event delegation and state management
- **CSS Styling**: Professional clock design with hover effects and responsive breakpoints
- **Time Management**: Client-server time offset calculation for accurate server time display
- **Cross-Platform Compatibility**: Extraction system works consistently across different browsers and devices
## [1.2.4] - 2025-08-22 ## [1.2.4] - 2025-08-22
### Added ### Added
......
...@@ -490,6 +490,240 @@ Content-Type: application/json ...@@ -490,6 +490,240 @@ Content-Type: application/json
} }
``` ```
## Extraction Management System
The application includes a comprehensive extraction management system for managing outcome associations with extraction results, featuring an intuitive drag-and-drop interface and real-time server time display.
### Extraction Management Features
- **Drag & Drop Interface**: Interactive outcome management with visual feedback
- **Multi-Association Support**: Outcomes can be associated with multiple extraction results
- **Trash Bin Removal**: Context-aware removal of associations based on drag location
- **Real-Time Persistence**: Immediate database updates with association changes
- **Server Time Clock**: Prominent 24-hour format clock showing server time
- **Cross-Interface Display**: Clock appears on all authenticated dashboard pages
### Extraction Management Architecture
The extraction system consists of several integrated components:
1. **Database Models**: `ExtractionAssociationModel` and `GameConfigModel` for data persistence
2. **Web Interface**: Complete drag-and-drop interface with real-time updates
3. **API Endpoints**: RESTful endpoints for CRUD operations on extraction data
4. **Server Time Service**: `/api/server-time` endpoint for accurate time display
5. **Migration System**: Database migration for schema updates
### Extraction Management Usage
#### Accessing the Interface
Navigate to the extraction management interface:
1. Open web dashboard at `http://localhost:5001`
2. Login with your credentials
3. Click "Extraction" in the navigation menu
4. Access drag-and-drop interface and time configuration
#### Managing Outcome Associations
1. **View Available Outcomes**: All outcomes are displayed in the top area
2. **Drag to Associate**: Drag outcomes to WIN1, X, or WIN2 columns
3. **Multi-Association**: Outcomes can be associated with multiple results
4. **Remove Associations**: Drag associated outcomes to the trash bin
5. **Real-Time Updates**: Changes are saved immediately to the database
#### Configuring Game Settings
1. **Time Limits**: Configure time limits for UNDER/OVER outcomes
2. **Database Persistence**: Settings are stored in the game_config table
3. **Default Values**: System includes sensible default configurations
### Server Time Clock
#### Clock Features
- **Server Time Display**: Shows time from the machine running mbetterc
- **24-Hour Format**: HH:MM:SS format for professional display
- **Real-Time Updates**: Updates every second with server time offset
- **Auto-Sync**: Re-syncs with server every 30 seconds for accuracy
- **Cross-Interface**: Appears on all authenticated dashboard pages
#### Technical Implementation
The clock uses a client-server time offset calculation:
```javascript
// Fetch server time and calculate offset
const serverTimestamp = data.timestamp;
const clientTimestamp = Date.now();
serverTimeOffset = serverTimestamp - clientTimestamp;
// Display server time using offset
const now = Date.now() + serverTimeOffset;
const date = new Date(now);
```
### Extraction Management Configuration
#### Database Schema
**extraction_associations table**:
```sql
CREATE TABLE extraction_associations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
outcome_name VARCHAR(255) NOT NULL,
extraction_result VARCHAR(50) NOT NULL,
is_default BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(outcome_name, extraction_result)
);
```
**game_config table**:
```sql
CREATE TABLE game_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value TEXT NOT NULL,
value_type VARCHAR(20) DEFAULT 'string',
description VARCHAR(500),
is_system BOOLEAN DEFAULT FALSE,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
```
#### Default Associations
The system includes default outcome associations:
- **WIN1**: WIN1, X1, K01, RET1, PTS1
- **X**: DRAW, 12, X1, X2, DKO
- **WIN2**: WIN2, X2, K02, RET2, PTS2
### Extraction Management API
#### Get All Associations
```http
GET /api/extraction/associations
Authorization: Bearer <token>
```
**Response:**
```json
{
"success": true,
"associations": [
{
"id": 1,
"outcome_name": "WIN1",
"extraction_result": "WIN1",
"is_default": true
}
]
}
```
#### Save Associations
```http
POST /api/extraction/associations
Authorization: Bearer <token>
Content-Type: application/json
{
"associations": [
{
"outcome_name": "WIN1",
"extraction_result": "WIN1"
}
]
}
```
#### Get Available Outcomes
```http
GET /api/extraction/outcomes
Authorization: Bearer <token>
```
**Response:**
```json
{
"success": true,
"outcomes": ["WIN1", "DRAW", "WIN2", "X1", "X2"]
}
```
#### Get Server Time
```http
GET /api/server-time
```
**Response:**
```json
{
"success": true,
"server_time": "2025-08-26T15:30:45.123456",
"timestamp": 1724683845123
}
```
#### Update Game Configuration
```http
POST /api/extraction/config
Authorization: Bearer <token>
Content-Type: application/json
{
"under_over_time_limit": 90
}
```
### Extraction Management Troubleshooting
#### Drag & Drop Issues
**Symptoms**: Outcomes won't drag, associations not saving
**Solutions**:
1. Verify JavaScript is enabled in browser
2. Check browser compatibility (Chrome, Firefox, Safari, Edge)
3. Clear browser cache and reload page
4. Check browser console for JavaScript errors
5. Ensure user has proper permissions
#### Clock Time Issues
**Symptoms**: Clock shows wrong time, not updating
**Solutions**:
1. Check server time API endpoint: `/api/server-time`
2. Verify network connectivity to server
3. Check browser time zone settings
4. Clear browser cache and reload
5. Check JavaScript console for errors
#### Database Issues
**Symptoms**: Associations not persisting, configuration not saving
**Solutions**:
1. Check database connectivity and permissions
2. Verify migration has been applied: `Migration_014`
3. Check database logs for errors
4. Ensure proper file permissions on database file
5. Verify SQLite installation and compatibility
#### Performance Issues
**Symptoms**: Interface slow, updates delayed
**Solutions**:
1. Check network latency to server
2. Reduce number of concurrent users
3. Optimize database queries and indexing
4. Consider browser resource usage
5. Check server CPU and memory usage
## Screen Casting System ## Screen Casting System
The application includes a comprehensive screen casting system with Chromecast integration, providing complete screen capture and streaming capabilities. The application includes a comprehensive screen casting system with Chromecast integration, providing complete screen capture and streaming capabilities.
......
...@@ -7,6 +7,8 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -7,6 +7,8 @@ A cross-platform multimedia client application with video playback, web dashboar
- **PyQt Video Player**: Fullscreen video playback with dual overlay system (WebEngine and native Qt widgets) - **PyQt Video Player**: Fullscreen video playback with dual overlay system (WebEngine and native Qt widgets)
- **Screen Casting System**: Complete screen capture and Chromecast streaming with web-based controls and device discovery - **Screen Casting System**: Complete screen capture and Chromecast streaming with web-based controls and device discovery
- **Template Management System**: Upload, manage, and live-reload HTML overlay templates with persistent storage - **Template Management System**: Upload, manage, and live-reload HTML overlay templates with persistent storage
- **Extraction Management**: Complete drag-and-drop interface for managing outcome associations with extraction results
- **Server Time Digital Clock**: Prominent 24-hour format clock showing server time across all dashboard interfaces
- **Web Dashboard**: Authentication, user management, configuration interface, and admin system controls - **Web Dashboard**: Authentication, user management, configuration interface, and admin system controls
- **REST API Client**: Configurable external API integration with automatic retry - **REST API Client**: Configurable external API integration with automatic retry
- **Multi-threaded Architecture**: Five threads with Queue-based message passing and proper daemon thread management - **Multi-threaded Architecture**: Five threads with Queue-based message passing and proper daemon thread management
...@@ -33,6 +35,22 @@ A cross-platform multimedia client application with video playback, web dashboar ...@@ -33,6 +35,22 @@ A cross-platform multimedia client application with video playback, web dashboar
-**Real-Time Status Updates**: Live status monitoring with proper button state management and streaming feedback -**Real-Time Status Updates**: Live status monitoring with proper button state management and streaming feedback
-**Command-Line Control**: Screen casting enabled by default with `--no-screen-cast` flag for opt-out configuration -**Command-Line Control**: Screen casting enabled by default with `--no-screen-cast` flag for opt-out configuration
### Version 1.2.5 (August 2025)
-**Extraction Management Page**: Complete drag-and-drop interface for managing outcome associations with extraction results
-**Server Time Digital Clock**: Prominent 24-hour format clock showing server time across all dashboard interfaces
-**Database Models for Extraction**: New `ExtractionAssociationModel` and `GameConfigModel` for storing outcome associations and game settings
-**Database Migration System**: Migration_014 for extraction associations and game configuration tables
-**API Endpoints for Extraction**: Complete REST API for managing outcome associations and game configuration
-**Drag & Drop Functionality**: Interactive outcome management with visual feedback and association persistence
-**Multi-Association Support**: Outcomes can be associated with multiple extraction results simultaneously
-**Trash Bin Removal**: Context-aware removal of associations based on drag source location
-**Game Configuration Panel**: Time limit settings for UNDER/OVER outcomes with database persistence
-**Server Time API**: `/api/server-time` endpoint providing server timestamp for accurate time display
-**Cross-Interface Clock**: Digital clock appears on all authenticated pages including cashier dashboard
-**Silent Drag & Drop**: No alert interruptions during drag-and-drop operations for smoother workflow
-**Time Synchronization**: Automatic server time sync every 30 seconds with offset calculation
### Version 1.2.3 (August 2025) ### Version 1.2.3 (August 2025)
-**Boxing Match Database**: Added comprehensive `matches` and `match_outcomes` database tables adapted from mbetterd MySQL schema -**Boxing Match Database**: Added comprehensive `matches` and `match_outcomes` database tables adapted from mbetterd MySQL schema
...@@ -250,6 +268,13 @@ Threads communicate via Python Queues with structured messages: ...@@ -250,6 +268,13 @@ Threads communicate via Python Queues with structured messages:
- `POST /api/video/upload` - Upload video file for playback - `POST /api/video/upload` - Upload video file for playback
- `POST /api/overlay` - Update overlay content and switch templates - `POST /api/overlay` - Update overlay content and switch templates
#### Extraction Management
- `GET /api/extraction/associations` - Get all outcome associations
- `POST /api/extraction/associations` - Save outcome associations
- `GET /api/extraction/outcomes` - Get available outcomes for association
- `POST /api/extraction/config` - Update game configuration settings
- `GET /api/server-time` - Get current server time for clock synchronization
### Message Types ### Message Types
#### Video Control #### Video Control
......
...@@ -817,6 +817,171 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration): ...@@ -817,6 +817,171 @@ class Migration_012_RemoveFixtureIdUniqueConstraint(DatabaseMigration):
return False return False
class Migration_013_AddStatusFieldToMatches(DatabaseMigration):
"""Add status field to matches table for match status tracking"""
def __init__(self):
super().__init__("013", "Add status field to matches table")
def up(self, db_manager) -> bool:
"""Add status column to matches table"""
try:
with db_manager.engine.connect() as conn:
# Check if status column already exists
result = conn.execute(text("PRAGMA table_info(matches)"))
columns = [row[1] for row in result.fetchall()]
if 'status' not in columns:
# Add status column with default value 'pending'
conn.execute(text("""
ALTER TABLE matches
ADD COLUMN status VARCHAR(20) DEFAULT 'pending' NOT NULL
"""))
# Add index for status column
conn.execute(text("""
CREATE INDEX IF NOT EXISTS ix_matches_status ON matches(status)
"""))
conn.commit()
logger.info("Status column added to matches table")
else:
logger.info("Status column already exists in matches table")
return True
except Exception as e:
logger.error(f"Failed to add status field to matches: {e}")
return False
def down(self, db_manager) -> bool:
"""Remove status column - SQLite doesn't support DROP COLUMN easily"""
logger.warning("SQLite doesn't support DROP COLUMN - status column will remain")
return True
class Migration_014_AddExtractionAndGameConfigTables(DatabaseMigration):
"""Add extraction_associations and game_config tables for extraction management"""
def __init__(self):
super().__init__("014", "Add extraction_associations and game_config tables")
def up(self, db_manager) -> bool:
"""Create extraction_associations and game_config tables"""
try:
with db_manager.engine.connect() as conn:
# Create extraction_associations table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS extraction_associations (
id INTEGER PRIMARY KEY AUTOINCREMENT,
outcome_name VARCHAR(255) NOT NULL,
extraction_result VARCHAR(50) NOT NULL,
is_default BOOLEAN DEFAULT FALSE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE(outcome_name, extraction_result)
)
"""))
# Create game_config table
conn.execute(text("""
CREATE TABLE IF NOT EXISTS game_config (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value TEXT NOT NULL,
value_type VARCHAR(20) DEFAULT 'string',
description VARCHAR(500),
is_system BOOLEAN DEFAULT FALSE NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
"""))
# Create indexes for extraction_associations table
indexes = [
"CREATE INDEX IF NOT EXISTS ix_extraction_associations_outcome_name ON extraction_associations(outcome_name)",
"CREATE INDEX IF NOT EXISTS ix_extraction_associations_extraction_result ON extraction_associations(extraction_result)",
"CREATE INDEX IF NOT EXISTS ix_extraction_associations_composite ON extraction_associations(outcome_name, extraction_result)",
]
# Create indexes for game_config table
indexes.extend([
"CREATE INDEX IF NOT EXISTS ix_game_config_key ON game_config(config_key)",
])
for index_sql in indexes:
conn.execute(text(index_sql))
# Insert default extraction associations
default_associations = [
('WIN1', 'WIN1', True),
('X1', 'WIN1', True),
('K01', 'WIN1', True),
('KO1', 'WIN1', True),
('RET1', 'WIN1', True),
('PTS1', 'WIN1', True),
('DRAW', 'X', True),
('12', 'X', True),
('X1', 'X', True),
('X2', 'X', True),
('DKO', 'X', True),
('WIN2', 'WIN2', True),
('X2', 'WIN2', True),
('K02', 'WIN2', True),
('KO2', 'WIN2', True),
('RET2', 'WIN2', True),
('PTS2', 'WIN2', True),
]
for outcome_name, extraction_result, is_default in default_associations:
conn.execute(text("""
INSERT OR IGNORE INTO extraction_associations
(outcome_name, extraction_result, is_default, created_at, updated_at)
VALUES (:outcome_name, :extraction_result, :is_default, datetime('now'), datetime('now'))
"""), {
'outcome_name': outcome_name,
'extraction_result': extraction_result,
'is_default': is_default
})
# Insert default game 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': 'under_over_time_limit',
'config_value': '90',
'value_type': 'int',
'description': 'Time limit in seconds between UNDER and OVER outcomes',
'is_system': False
})
conn.commit()
logger.info("Extraction associations and game config tables created successfully")
return True
except Exception as e:
logger.error(f"Failed to create extraction and game config tables: {e}")
return False
def down(self, db_manager) -> bool:
"""Drop extraction_associations and game_config tables"""
try:
with db_manager.engine.connect() as conn:
# Drop tables in reverse order (if there were foreign keys)
conn.execute(text("DROP TABLE IF EXISTS game_config"))
conn.execute(text("DROP TABLE IF EXISTS extraction_associations"))
conn.commit()
logger.info("Extraction associations and game config tables dropped")
return True
except Exception as e:
logger.error(f"Failed to drop extraction and game config tables: {e}")
return False
# Registry of all migrations in order # Registry of all migrations in order
MIGRATIONS: List[DatabaseMigration] = [ MIGRATIONS: List[DatabaseMigration] = [
Migration_001_InitialSchema(), Migration_001_InitialSchema(),
...@@ -831,6 +996,8 @@ MIGRATIONS: List[DatabaseMigration] = [ ...@@ -831,6 +996,8 @@ MIGRATIONS: List[DatabaseMigration] = [
Migration_010_AddRunningFieldToMatches(), Migration_010_AddRunningFieldToMatches(),
Migration_011_AddFixtureActiveTimeToMatches(), Migration_011_AddFixtureActiveTimeToMatches(),
Migration_012_RemoveFixtureIdUniqueConstraint(), Migration_012_RemoveFixtureIdUniqueConstraint(),
Migration_013_AddStatusFieldToMatches(),
Migration_014_AddExtractionAndGameConfigTables(),
] ]
......
...@@ -7,13 +7,23 @@ import hashlib ...@@ -7,13 +7,23 @@ import hashlib
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
from sqlalchemy import ( from sqlalchemy import (
Column, Integer, String, Text, DateTime, Boolean, Float, Column, Integer, String, Text, DateTime, Boolean, Float,
JSON, ForeignKey, UniqueConstraint, Index, create_engine JSON, ForeignKey, UniqueConstraint, Index, create_engine, Enum
) )
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship, sessionmaker from sqlalchemy.orm import relationship, sessionmaker
from werkzeug.security import generate_password_hash, check_password_hash from werkzeug.security import generate_password_hash, check_password_hash
# Enum for match status
class MatchStatus(str, Enum):
PENDING = "pending"
SCHEDULED = "scheduled"
BET = "bet"
INGAME = "ingame"
CANCELLED = "cancelled"
FAILED = "failed"
PAUSED = "paused"
Base = declarative_base() Base = declarative_base()
...@@ -472,6 +482,7 @@ class MatchModel(BaseModel): ...@@ -472,6 +482,7 @@ class MatchModel(BaseModel):
result = Column(String(255), comment='Match result/outcome') result = Column(String(255), comment='Match result/outcome')
done = Column(Boolean, default=False, nullable=False, comment='Match completion flag (0=pending, 1=done)') 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)') running = Column(Boolean, default=False, nullable=False, comment='Match running flag (0=not running, 1=running)')
status = Column(Enum('pending', 'scheduled', 'bet', 'ingame', 'cancelled', 'failed', 'paused'), default='pending', nullable=False, comment='Match status enum')
fixture_active_time = Column(Integer, nullable=True, comment='Unix timestamp when fixture became active on server') fixture_active_time = Column(Integer, nullable=True, comment='Unix timestamp when fixture became active on server')
# File metadata # File metadata
...@@ -569,13 +580,81 @@ class MatchOutcomeModel(BaseModel): ...@@ -569,13 +580,81 @@ class MatchOutcomeModel(BaseModel):
Index('ix_match_outcomes_composite', 'match_id', 'column_name'), Index('ix_match_outcomes_composite', 'match_id', 'column_name'),
UniqueConstraint('match_id', 'column_name', name='uq_match_outcomes_match_column'), UniqueConstraint('match_id', 'column_name', name='uq_match_outcomes_match_column'),
) )
match_id = Column(Integer, ForeignKey('matches.id', ondelete='CASCADE'), nullable=False, comment='Foreign key to matches table') match_id = Column(Integer, ForeignKey('matches.id', ondelete='CASCADE'), nullable=False, comment='Foreign key to matches table')
column_name = Column(String(255), nullable=False, comment='Result column name from fixture file') column_name = Column(String(255), nullable=False, comment='Result column name from fixture file')
float_value = Column(Float, nullable=False, comment='Float value with precision') float_value = Column(Float, nullable=False, comment='Float value with precision')
# Relationships # Relationships
match = relationship('MatchModel', back_populates='outcomes') match = relationship('MatchModel', back_populates='outcomes')
def __repr__(self):
return f'<MatchOutcome {self.column_name}={self.float_value} for Match {self.match_id}>'
class ExtractionAssociationModel(BaseModel):
"""Associations between match outcomes and extraction results"""
__tablename__ = 'extraction_associations'
__table_args__ = (
Index('ix_extraction_associations_outcome_name', 'outcome_name'),
Index('ix_extraction_associations_extraction_result', 'extraction_result'),
Index('ix_extraction_associations_composite', 'outcome_name', 'extraction_result'),
UniqueConstraint('outcome_name', 'extraction_result', name='uq_extraction_associations_outcome_result'),
)
outcome_name = Column(String(255), nullable=False, comment='Match outcome name (e.g., WIN1, DRAW, X1, etc.)')
extraction_result = Column(String(50), nullable=False, comment='Extraction result category (WIN1, X, WIN2)')
is_default = Column(Boolean, default=False, nullable=False, comment='Whether this is a default association')
def __repr__(self):
return f'<ExtractionAssociation {self.outcome_name} -> {self.extraction_result}>'
class GameConfigModel(BaseModel):
"""Game configuration settings"""
__tablename__ = 'game_config'
__table_args__ = (
Index('ix_game_config_key', 'config_key'),
UniqueConstraint('config_key', name='uq_game_config_key'),
)
config_key = Column(String(100), nullable=False, unique=True, comment='Configuration key')
config_value = Column(Text, nullable=False, comment='Configuration value as text')
value_type = Column(String(20), default='string', nullable=False, comment='Type of value: string, int, float, bool')
description = Column(String(500), comment='Description of the configuration setting')
is_system = Column(Boolean, default=False, nullable=False, comment='Whether this is a system setting')
def get_typed_value(self) -> Any:
"""Get value converted to proper type"""
if self.value_type == 'int':
try:
return int(self.config_value)
except (ValueError, TypeError):
return 0
elif self.value_type == 'float':
try:
return float(self.config_value)
except (ValueError, TypeError):
return 0.0
elif self.value_type == 'bool':
return self.config_value.lower() in ('true', '1', 'yes', 'on')
else:
return self.config_value
def set_typed_value(self, value: Any):
"""Set value with automatic type detection"""
if isinstance(value, bool):
self.config_value = str(value).lower()
self.value_type = 'bool'
elif isinstance(value, int):
self.config_value = str(value)
self.value_type = 'int'
elif isinstance(value, float):
self.config_value = str(value)
self.value_type = 'float'
else:
self.config_value = str(value)
self.value_type = 'string'
def __repr__(self): def __repr__(self):
return f'<MatchOutcome {self.column_name}={self.float_value} for Match {self.match_id}>' return f'<GameConfig {self.config_key}={self.config_value}>'
\ No newline at end of file \ No newline at end of file
This diff is collapsed.
...@@ -10,6 +10,52 @@ ...@@ -10,6 +10,52 @@
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}">
<style>
.navbar-clock {
background: rgba(255, 255, 255, 0.1);
border: 2px solid rgba(255, 255, 255, 0.3);
border-radius: 8px;
padding: 8px 16px;
font-family: 'Courier New', monospace;
font-weight: bold;
font-size: 1.2rem;
color: #ffffff;
text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.5);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
transition: all 0.3s ease;
}
.navbar-clock:hover {
background: rgba(255, 255, 255, 0.2);
border-color: rgba(255, 255, 255, 0.5);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.3);
}
.navbar-clock i {
color: #ffffff;
filter: drop-shadow(1px 1px 1px rgba(0, 0, 0, 0.5));
}
#clock-time {
font-size: 1.3rem;
letter-spacing: 2px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.navbar-clock {
font-size: 1rem;
padding: 6px 12px;
}
#clock-time {
font-size: 1.1rem;
letter-spacing: 1px;
}
}
</style>
{% block head %}{% endblock %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
...@@ -50,6 +96,12 @@ ...@@ -50,6 +96,12 @@
<i class="fas fa-list-ul me-1"></i>Fixtures <i class="fas fa-list-ul me-1"></i>Fixtures
</a> </a>
</li> </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"> <li class="nav-item">
<a class="nav-link {% if request.endpoint == 'main.video_test' %}active{% endif %}" <a class="nav-link {% if request.endpoint == 'main.video_test' %}active{% endif %}"
href="{{ url_for('main.video_test') }}"> href="{{ url_for('main.video_test') }}">
...@@ -87,7 +139,15 @@ ...@@ -87,7 +139,15 @@
</li> </li>
{% endif %} {% endif %}
</ul> </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>
</div>
</div>
<ul class="navbar-nav"> <ul class="navbar-nav">
<li class="nav-item dropdown"> <li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"> <a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown">
...@@ -179,7 +239,63 @@ ...@@ -179,7 +239,63 @@
var config = JSON.parse(document.getElementById('dashboard-config').textContent); var config = JSON.parse(document.getElementById('dashboard-config').textContent);
Dashboard.init(config); Dashboard.init(config);
} }
// Initialize digital clock
initializeClock();
}); });
function initializeClock() {
const clockElement = document.getElementById('clock-time');
if (!clockElement) return;
let serverTimeOffset = 0; // Offset between server and client time
let lastServerTime = null;
function fetchServerTime() {
return fetch('/api/server-time')
.then(response => response.json())
.then(data => {
if (data.success) {
const serverTimestamp = data.timestamp;
const clientTimestamp = Date.now();
serverTimeOffset = serverTimestamp - clientTimestamp;
lastServerTime = serverTimestamp;
return serverTimestamp;
} else {
throw new Error('Failed to get server time');
}
})
.catch(error => {
console.error('Error fetching server time:', error);
// Fallback to client time if server time is unavailable
return Date.now();
});
}
function updateClock() {
const now = Date.now() + serverTimeOffset;
const date = new Date(now);
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
const seconds = String(date.getSeconds()).padStart(2, '0');
const timeString = `${hours}:${minutes}:${seconds}`;
clockElement.textContent = timeString;
}
// Fetch server time initially and set up updates
fetchServerTime().then(() => {
// Update immediately with server time
updateClock();
// Update display every second (using client time + offset)
setInterval(updateClock, 1000);
// Sync with server time every 30 seconds
setInterval(fetchServerTime, 30000);
});
}
</script> </script>
{% endif %} {% endif %}
......
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