2168 lines
83 KiB
Python
2168 lines
83 KiB
Python
from flask import Flask, render_template, request, redirect, jsonify
|
||
import json
|
||
import os
|
||
import random
|
||
import glob
|
||
from collections import defaultdict
|
||
from datetime import datetime
|
||
import re
|
||
|
||
app = Flask(__name__)
|
||
|
||
def is_mobile_device():
|
||
"""Check if the request is coming from a mobile device"""
|
||
user_agent = request.headers.get('User-Agent', '').lower()
|
||
mobile_patterns = [
|
||
r'android', r'iphone', r'ipad', r'ipod', r'blackberry',
|
||
r'iemobile', r'opera mini', r'mobile', r'tablet'
|
||
]
|
||
return any(re.search(pattern, user_agent) for pattern in mobile_patterns)
|
||
|
||
# Define streams globally so both routes can access them
|
||
STREAMS = [
|
||
{'name': 'Target1', 'url': 'http://192.168.0.134:9081'},
|
||
{'name': 'Target2', 'url': 'http://192.168.0.134:9082'},
|
||
{'name': 'Target3', 'url': 'http://192.168.0.134:9083'},
|
||
{'name': 'Target4', 'url': 'http://192.168.0.134:9084'},
|
||
{'name': 'Target5', 'url': 'http://192.168.0.134:9085'},
|
||
{'name': 'Target6', 'url': 'http://192.168.0.134:9086'},
|
||
]
|
||
|
||
# Settings file paths
|
||
SETTINGS_FILE = 'camera_settings.json'
|
||
PLAYERS_FILE = 'players.json'
|
||
TOURNAMENT_FILE = 'tournament_state.json'
|
||
RESULTS_FILE = 'tournament_results.json'
|
||
LEAGUE_FILE = 'league_state.json'
|
||
ARCHIVE_DIR = 'tournament_archives'
|
||
LEAGUE_ARCHIVE_DIR = 'league_archives'
|
||
|
||
# Default settings
|
||
DEFAULT_SETTINGS = {
|
||
'camera_titles': {
|
||
'1': 'Camera 1',
|
||
'2': 'Camera 2',
|
||
'3': 'Camera 3',
|
||
'4': 'Camera 4',
|
||
'5': 'Camera 5',
|
||
'6': 'Camera 6'
|
||
},
|
||
'display_options': {
|
||
'show_titles': True,
|
||
'title_size': 1.1
|
||
}
|
||
}
|
||
|
||
# Default players structure
|
||
DEFAULT_PLAYERS = {
|
||
'players': [
|
||
{'id': 1, 'name': 'Player 1', 'enabled': True},
|
||
{'id': 2, 'name': 'Player 2', 'enabled': True},
|
||
{'id': 3, 'name': 'Player 3', 'enabled': True},
|
||
{'id': 4, 'name': 'Player 4', 'enabled': True},
|
||
{'id': 5, 'name': 'Player 5', 'enabled': True},
|
||
{'id': 6, 'name': 'Player 6', 'enabled': True},
|
||
]
|
||
}
|
||
|
||
def load_settings():
|
||
"""Load settings from JSON file, create with defaults if not exists"""
|
||
try:
|
||
if os.path.exists(SETTINGS_FILE):
|
||
with open(SETTINGS_FILE, 'r') as f:
|
||
settings = json.load(f)
|
||
# Ensure all required keys exist (for backwards compatibility)
|
||
for key in DEFAULT_SETTINGS:
|
||
if key not in settings:
|
||
settings[key] = DEFAULT_SETTINGS[key]
|
||
for camera_id in DEFAULT_SETTINGS['camera_titles']:
|
||
if camera_id not in settings['camera_titles']:
|
||
settings['camera_titles'][camera_id] = DEFAULT_SETTINGS['camera_titles'][camera_id]
|
||
return settings
|
||
else:
|
||
return DEFAULT_SETTINGS.copy()
|
||
except (json.JSONDecodeError, IOError):
|
||
return DEFAULT_SETTINGS.copy()
|
||
|
||
def save_settings(settings):
|
||
"""Save settings to JSON file"""
|
||
try:
|
||
with open(SETTINGS_FILE, 'w') as f:
|
||
json.dump(settings, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_players():
|
||
"""Load players from JSON file, create with defaults if not exists"""
|
||
try:
|
||
if os.path.exists(PLAYERS_FILE):
|
||
with open(PLAYERS_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
save_players(DEFAULT_PLAYERS)
|
||
return DEFAULT_PLAYERS.copy()
|
||
except (json.JSONDecodeError, IOError):
|
||
return DEFAULT_PLAYERS.copy()
|
||
|
||
def save_players(players_data):
|
||
"""Save players to JSON file"""
|
||
try:
|
||
with open(PLAYERS_FILE, 'w') as f:
|
||
json.dump(players_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_tournament_state():
|
||
"""Load tournament state from JSON file"""
|
||
try:
|
||
if os.path.exists(TOURNAMENT_FILE):
|
||
with open(TOURNAMENT_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
return None
|
||
except (json.JSONDecodeError, IOError):
|
||
return None
|
||
|
||
def save_tournament_state(tournament_data):
|
||
"""Save tournament state to JSON file"""
|
||
try:
|
||
with open(TOURNAMENT_FILE, 'w') as f:
|
||
json.dump(tournament_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_league_state():
|
||
"""Load league state from JSON file"""
|
||
try:
|
||
if os.path.exists(LEAGUE_FILE):
|
||
with open(LEAGUE_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
return None
|
||
except (json.JSONDecodeError, IOError):
|
||
return None
|
||
|
||
def save_league_state(league_data):
|
||
"""Save league state to JSON file"""
|
||
try:
|
||
with open(LEAGUE_FILE, 'w') as f:
|
||
json.dump(league_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def load_results():
|
||
"""Load results from JSON file"""
|
||
try:
|
||
if os.path.exists(RESULTS_FILE):
|
||
with open(RESULTS_FILE, 'r') as f:
|
||
return json.load(f)
|
||
else:
|
||
return None
|
||
except (json.JSONDecodeError, IOError):
|
||
return None
|
||
|
||
def save_results(results_data):
|
||
"""Save results to JSON file"""
|
||
try:
|
||
with open(RESULTS_FILE, 'w') as f:
|
||
json.dump(results_data, f, indent=2)
|
||
return True
|
||
except IOError:
|
||
return False
|
||
|
||
def archive_tournament(tournament_data, results_data):
|
||
"""Archive completed tournament data"""
|
||
try:
|
||
# Create archive directory if it doesn't exist
|
||
if not os.path.exists(ARCHIVE_DIR):
|
||
os.makedirs(ARCHIVE_DIR)
|
||
|
||
# Create filename with timestamp
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
archive_filename = f"tournament_{timestamp}.json"
|
||
archive_path = os.path.join(ARCHIVE_DIR, archive_filename)
|
||
|
||
# Combine tournament and results data
|
||
archive_data = {
|
||
'tournament': tournament_data,
|
||
'results': results_data,
|
||
'archived_at': datetime.now().isoformat()
|
||
}
|
||
|
||
# Save to archive
|
||
with open(archive_path, 'w') as f:
|
||
json.dump(archive_data, f, indent=2)
|
||
|
||
print(f"Tournament archived to: {archive_path}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"Error archiving tournament: {e}")
|
||
return False
|
||
|
||
def archive_league(league_data):
|
||
"""Archive completed league data"""
|
||
try:
|
||
if not os.path.exists(LEAGUE_ARCHIVE_DIR):
|
||
os.makedirs(LEAGUE_ARCHIVE_DIR)
|
||
|
||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||
archive_filename = f"league_{timestamp}.json"
|
||
archive_path = os.path.join(LEAGUE_ARCHIVE_DIR, archive_filename)
|
||
|
||
archive_data = {
|
||
'league': league_data,
|
||
'archived_at': datetime.now().isoformat()
|
||
}
|
||
|
||
with open(archive_path, 'w') as f:
|
||
json.dump(archive_data, f, indent=2)
|
||
|
||
print(f"League archived to: {archive_path}")
|
||
return True
|
||
except Exception as e:
|
||
print(f"Error archiving league: {e}")
|
||
return False
|
||
|
||
def create_league(enabled_players, tournament_type):
|
||
"""Create a new league with 6 tournaments"""
|
||
league_id = f"league_{datetime.now().strftime('%Y%m%d_%H%M%S')}"
|
||
|
||
league_data = {
|
||
'league_id': league_id,
|
||
'created_at': datetime.now().isoformat(),
|
||
'tournament_type': tournament_type,
|
||
'total_tournaments': 6,
|
||
'current_tournament': 0, # Will be 1 when first tournament starts
|
||
'participants': {},
|
||
'completed_tournaments': [],
|
||
'league_finished': False
|
||
}
|
||
|
||
# Initialize participants
|
||
for player in enabled_players:
|
||
league_data['participants'][str(player['id'])] = {
|
||
'name': player['name'],
|
||
'joker_used': False,
|
||
'tournament_results': [],
|
||
'total_score': 0,
|
||
'final_score': 0, # Best 5 tournaments
|
||
'tournaments_participated': 0
|
||
}
|
||
|
||
return league_data
|
||
|
||
def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None):
|
||
"""Create draft groups of 6 players and organize rounds"""
|
||
if len(enabled_players) < 1:
|
||
return None
|
||
|
||
# Shuffle players for random grouping
|
||
players_copy = enabled_players.copy()
|
||
random.shuffle(players_copy)
|
||
|
||
# Create groups of up to 6
|
||
groups = []
|
||
for i in range(0, len(players_copy), 6):
|
||
group = players_copy[i:i+6]
|
||
groups.append(group)
|
||
|
||
# Create rounds
|
||
rounds = []
|
||
for i, group in enumerate(groups):
|
||
rounds.append({
|
||
'round_number': i + 1,
|
||
'players': group,
|
||
'status': 'pending' if i == 0 else 'waiting'
|
||
})
|
||
|
||
tournament_data = {
|
||
'rounds': rounds,
|
||
'created_at': datetime.now().isoformat(),
|
||
'total_players': len(enabled_players),
|
||
'total_rounds': len(rounds),
|
||
'current_round': 1,
|
||
'tournament_type': tournament_type
|
||
}
|
||
|
||
if league_tournament_number:
|
||
tournament_data['league_tournament_number'] = league_tournament_number
|
||
|
||
return tournament_data
|
||
|
||
def create_results_structure(tournament_data):
|
||
"""Create results structure for a tournament"""
|
||
if not tournament_data:
|
||
return None
|
||
|
||
tournament_type = tournament_data.get('tournament_type', '20_targets')
|
||
|
||
# Determine target count and shots per target
|
||
if tournament_type == '40_targets':
|
||
num_targets = 40
|
||
shots_per_target = 2
|
||
elif tournament_type == '4_targets':
|
||
num_targets = 4
|
||
shots_per_target = 5
|
||
else: # 20_targets (default)
|
||
num_targets = 20
|
||
shots_per_target = 2
|
||
|
||
results = {
|
||
'tournament_id': tournament_data.get('created_at', datetime.now().isoformat()),
|
||
'tournament_type': tournament_type,
|
||
'participants': {},
|
||
'tournament_finished': False,
|
||
'created_at': datetime.now().isoformat()
|
||
}
|
||
|
||
# Add league info if present
|
||
if 'league_tournament_number' in tournament_data:
|
||
results['league_tournament_number'] = tournament_data['league_tournament_number']
|
||
|
||
# Create structure for each participant
|
||
all_players = []
|
||
for round_data in tournament_data['rounds']:
|
||
all_players.extend(round_data['players'])
|
||
|
||
for player in all_players:
|
||
player_id = str(player['id'])
|
||
|
||
# Create target structure based on tournament type
|
||
targets = {}
|
||
for i in range(1, num_targets + 1):
|
||
target = {}
|
||
for j in range(1, shots_per_target + 1):
|
||
target[f'shot{j}'] = None
|
||
targets[str(i)] = target
|
||
|
||
results['participants'][player_id] = {
|
||
'name': player['name'],
|
||
'targets': targets,
|
||
'total_score': 0,
|
||
'completed': False
|
||
}
|
||
|
||
return results
|
||
|
||
def calculate_total_score(targets):
|
||
"""Calculate total score from targets, treating None as 0"""
|
||
total = 0
|
||
for target in targets.values():
|
||
for shot_key, shot_value in target.items():
|
||
if shot_key.startswith('shot') and shot_value is not None:
|
||
total += shot_value
|
||
return total
|
||
|
||
def is_participant_completed(targets):
|
||
"""Check if a participant has completed all targets (all shots entered, including 0s)"""
|
||
for target in targets.values():
|
||
for shot_key, shot_value in target.items():
|
||
if shot_key.startswith('shot') and shot_value is None:
|
||
return False
|
||
return True
|
||
|
||
def calculate_league_final_scores(league_data):
|
||
"""Calculate final league scores using best 5 tournaments (joker system)"""
|
||
for participant_id, participant in league_data['participants'].items():
|
||
tournament_scores = []
|
||
|
||
# Get all tournament scores where player participated
|
||
for result in participant['tournament_results']:
|
||
if result['participated']:
|
||
tournament_scores.append(result['score'])
|
||
|
||
# Sort scores descending and take best 5 (or all if less than 6)
|
||
tournament_scores.sort(reverse=True)
|
||
best_scores = tournament_scores[:5] if len(tournament_scores) > 5 else tournament_scores
|
||
|
||
participant['final_score'] = sum(best_scores)
|
||
participant['tournaments_participated'] = len(tournament_scores)
|
||
|
||
def get_league_final_rankings(league_data):
|
||
"""Get final league rankings sorted by final score"""
|
||
participants = []
|
||
for player_id, data in league_data['participants'].items():
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'final_score': data['final_score'],
|
||
'total_score': data['total_score'],
|
||
'tournaments_participated': data['tournaments_participated'],
|
||
'joker_used': data['joker_used'],
|
||
'tournament_results': data['tournament_results']
|
||
})
|
||
|
||
# Sort by final score (best 5 tournaments) descending
|
||
participants.sort(key=lambda x: x['final_score'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return participants
|
||
|
||
def get_current_round_data():
|
||
"""Get current round data from tournament"""
|
||
tournament_state = load_tournament_state()
|
||
if not tournament_state:
|
||
return None
|
||
|
||
current_round_num = tournament_state.get('current_round', 1)
|
||
|
||
# Find the current round
|
||
for round_data in tournament_state['rounds']:
|
||
if round_data['round_number'] == current_round_num:
|
||
return round_data
|
||
|
||
return None
|
||
|
||
def calculate_current_league_standings(league_data):
|
||
"""Calculate current league standings during active league"""
|
||
participants = []
|
||
for player_id, data in league_data['participants'].items():
|
||
# Calculate current standings based on completed tournaments
|
||
tournament_scores = []
|
||
completed_tournaments = 0
|
||
|
||
for result in data['tournament_results']:
|
||
if result['participated']:
|
||
tournament_scores.append(result['score'])
|
||
completed_tournaments += 1
|
||
|
||
# Current score is sum of all completed tournaments
|
||
current_total = sum(tournament_scores)
|
||
|
||
# For display, show what the final score would be if we took best 5 now
|
||
tournament_scores.sort(reverse=True)
|
||
projected_final = sum(tournament_scores[:5]) if len(tournament_scores) >= 5 else sum(tournament_scores)
|
||
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'current_total': current_total,
|
||
'projected_final': projected_final,
|
||
'tournaments_completed': completed_tournaments,
|
||
'joker_used': data['joker_used'],
|
||
'tournament_results': data['tournament_results']
|
||
})
|
||
|
||
# Sort by current total score (descending)
|
||
participants.sort(key=lambda x: x['current_total'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return participants
|
||
|
||
def get_league_current_standings(league_state):
|
||
"""Get current league standings including in-progress tournament"""
|
||
if not league_state:
|
||
return []
|
||
|
||
# Start with current league participants
|
||
participants = []
|
||
for player_id, data in league_state['participants'].items():
|
||
participant = {
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'total_score': data['total_score'],
|
||
'final_score': data['final_score'],
|
||
'tournaments_participated': data['tournaments_participated'],
|
||
'joker_used': data['joker_used'],
|
||
'tournament_results': data['tournament_results'],
|
||
'current_tournament_score': 0, # Score from current tournament
|
||
'current_tournament_participating': False
|
||
}
|
||
participants.append(participant)
|
||
|
||
# Add current tournament scores if available
|
||
current_results = load_results()
|
||
if current_results and not current_results.get('tournament_finished', False):
|
||
for player_id, result_data in current_results['participants'].items():
|
||
# Find this participant in our list
|
||
for participant in participants:
|
||
if participant['id'] == player_id:
|
||
participant['current_tournament_score'] = result_data['total_score']
|
||
participant['current_tournament_participating'] = True
|
||
break
|
||
|
||
# Sort by final score (best 5 tournaments) descending
|
||
participants.sort(key=lambda x: x['final_score'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return participants
|
||
|
||
# Add these functions after the existing helper functions in app.py
|
||
|
||
def get_archived_tournaments():
|
||
"""Get list of standalone archived tournaments (excluding league tournaments)"""
|
||
try:
|
||
if not os.path.exists(ARCHIVE_DIR):
|
||
return []
|
||
|
||
archives = []
|
||
for file_path in glob.glob(os.path.join(ARCHIVE_DIR, "tournament_*.json")):
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
# Extract metadata
|
||
filename = os.path.basename(file_path)
|
||
archived_at = data.get('archived_at', 'Unknown')
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
# Skip tournaments that are part of a league
|
||
if tournament_data.get('league_tournament_number') or results_data.get('league_tournament_number'):
|
||
continue
|
||
|
||
archive_info = {
|
||
'filename': filename,
|
||
'filepath': file_path,
|
||
'archived_at': archived_at,
|
||
'created_at': tournament_data.get('created_at', 'Unknown'),
|
||
'tournament_type': tournament_data.get('tournament_type', '20_targets'),
|
||
'total_players': tournament_data.get('total_players', 0),
|
||
'total_rounds': tournament_data.get('total_rounds', 0),
|
||
'tournament_finished': results_data.get('tournament_finished', False),
|
||
'participants_count': len(results_data.get('participants', {}))
|
||
}
|
||
|
||
archives.append(archive_info)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"Error reading archive {file_path}: {e}")
|
||
continue
|
||
|
||
# Sort by archived date (newest first)
|
||
archives.sort(key=lambda x: x['archived_at'], reverse=True)
|
||
return archives
|
||
except Exception as e:
|
||
print(f"Error getting archived tournaments: {e}")
|
||
return []
|
||
|
||
def get_archived_leagues():
|
||
"""Get list of all archived leagues"""
|
||
try:
|
||
if not os.path.exists(LEAGUE_ARCHIVE_DIR):
|
||
return []
|
||
|
||
archives = []
|
||
for file_path in glob.glob(os.path.join(LEAGUE_ARCHIVE_DIR, "league_*.json")):
|
||
try:
|
||
with open(file_path, 'r') as f:
|
||
data = json.load(f)
|
||
|
||
filename = os.path.basename(file_path)
|
||
archived_at = data.get('archived_at', 'Unknown')
|
||
league_data = data.get('league', {})
|
||
|
||
archive_info = {
|
||
'filename': filename,
|
||
'filepath': file_path,
|
||
'archived_at': archived_at,
|
||
'created_at': league_data.get('created_at', 'Unknown'),
|
||
'league_id': league_data.get('league_id', 'Unknown'),
|
||
'tournament_type': league_data.get('tournament_type', '20_targets'),
|
||
'total_tournaments': league_data.get('total_tournaments', 6),
|
||
'participants_count': len(league_data.get('participants', {})),
|
||
'league_finished': league_data.get('league_finished', False),
|
||
'completed_tournaments': len(league_data.get('completed_tournaments', []))
|
||
}
|
||
|
||
archives.append(archive_info)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"Error reading league archive {file_path}: {e}")
|
||
continue
|
||
|
||
# Sort by archived date (newest first)
|
||
archives.sort(key=lambda x: x['archived_at'], reverse=True)
|
||
return archives
|
||
except Exception as e:
|
||
print(f"Error getting archived leagues: {e}")
|
||
return []
|
||
|
||
def load_archive_file(filepath):
|
||
"""Load a specific archive file"""
|
||
try:
|
||
with open(filepath, 'r') as f:
|
||
return json.load(f)
|
||
except (json.JSONDecodeError, IOError) as e:
|
||
print(f"Error loading archive file {filepath}: {e}")
|
||
return None
|
||
|
||
def analyze_player_performance(player_id, archives_data):
|
||
"""Analyze performance of a specific player across all archives"""
|
||
player_stats = {
|
||
'total_tournaments': 0,
|
||
'total_leagues': 0,
|
||
'tournament_scores': [],
|
||
'league_scores': [],
|
||
'best_tournament_score': 0,
|
||
'worst_tournament_score': float('inf'),
|
||
'average_tournament_score': 0,
|
||
'total_shots_fired': 0,
|
||
'performance_trend': [],
|
||
'tournament_history': [],
|
||
'league_history': []
|
||
}
|
||
|
||
# Process tournament archives
|
||
for archive in archives_data.get('tournaments', []):
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if not data:
|
||
continue
|
||
|
||
results = data.get('results', {})
|
||
participants = results.get('participants', {})
|
||
|
||
if str(player_id) in participants:
|
||
participant = participants[str(player_id)]
|
||
score = participant.get('total_score', 0)
|
||
completed = participant.get('completed', False)
|
||
|
||
if completed:
|
||
player_stats['total_tournaments'] += 1
|
||
player_stats['tournament_scores'].append(score)
|
||
|
||
if score > player_stats['best_tournament_score']:
|
||
player_stats['best_tournament_score'] = score
|
||
if score < player_stats['worst_tournament_score']:
|
||
player_stats['worst_tournament_score'] = score
|
||
|
||
# Count shots fired
|
||
targets = participant.get('targets', {})
|
||
shots_in_tournament = 0
|
||
for target in targets.values():
|
||
if target.get('shot1') is not None:
|
||
shots_in_tournament += 1
|
||
if target.get('shot2') is not None:
|
||
shots_in_tournament += 1
|
||
|
||
player_stats['total_shots_fired'] += shots_in_tournament
|
||
|
||
# Add to history
|
||
player_stats['tournament_history'].append({
|
||
'date': archive['archived_at'],
|
||
'score': score,
|
||
'tournament_type': archive['tournament_type'],
|
||
'completed': completed,
|
||
'shots_fired': shots_in_tournament
|
||
})
|
||
except Exception as e:
|
||
print(f"Error analyzing tournament archive: {e}")
|
||
continue
|
||
|
||
# Process league archives
|
||
for archive in archives_data.get('leagues', []):
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if not data:
|
||
continue
|
||
|
||
league = data.get('league', {})
|
||
participants = league.get('participants', {})
|
||
|
||
if str(player_id) in participants:
|
||
participant = participants[str(player_id)]
|
||
final_score = participant.get('final_score', 0)
|
||
total_score = participant.get('total_score', 0)
|
||
tournaments_participated = participant.get('tournaments_participated', 0)
|
||
|
||
player_stats['total_leagues'] += 1
|
||
player_stats['league_scores'].append(final_score)
|
||
|
||
player_stats['league_history'].append({
|
||
'date': archive['archived_at'],
|
||
'final_score': final_score,
|
||
'total_score': total_score,
|
||
'tournaments_participated': tournaments_participated,
|
||
'joker_used': participant.get('joker_used', False),
|
||
'tournament_results': participant.get('tournament_results', [])
|
||
})
|
||
except Exception as e:
|
||
print(f"Error analyzing league archive: {e}")
|
||
continue
|
||
|
||
# Calculate averages and trends
|
||
if player_stats['tournament_scores']:
|
||
player_stats['average_tournament_score'] = sum(player_stats['tournament_scores']) / len(player_stats['tournament_scores'])
|
||
|
||
# Performance trend (simple moving average)
|
||
if len(player_stats['tournament_scores']) >= 3:
|
||
for i in range(2, len(player_stats['tournament_scores'])):
|
||
avg = sum(player_stats['tournament_scores'][i-2:i+1]) / 3
|
||
player_stats['performance_trend'].append(avg)
|
||
|
||
if player_stats['worst_tournament_score'] == float('inf'):
|
||
player_stats['worst_tournament_score'] = 0
|
||
|
||
# Sort histories by date (newest first)
|
||
player_stats['tournament_history'].sort(key=lambda x: x['date'], reverse=True)
|
||
player_stats['league_history'].sort(key=lambda x: x['date'], reverse=True)
|
||
|
||
return player_stats
|
||
|
||
def get_all_players_from_archives():
|
||
"""Get all players that appear in any archive"""
|
||
all_players = {}
|
||
|
||
# Get current players first
|
||
current_players = load_players()
|
||
for player in current_players['players']:
|
||
all_players[str(player['id'])] = {
|
||
'id': player['id'],
|
||
'name': player['name'],
|
||
'current_player': True
|
||
}
|
||
|
||
# Add players from tournament archives
|
||
tournaments = get_archived_tournaments()
|
||
for archive in tournaments:
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if data and 'results' in data:
|
||
participants = data['results'].get('participants', {})
|
||
for player_id, participant in participants.items():
|
||
if player_id not in all_players:
|
||
all_players[player_id] = {
|
||
'id': int(player_id),
|
||
'name': participant.get('name', f'Player {player_id}'),
|
||
'current_player': False
|
||
}
|
||
except Exception as e:
|
||
print(f"Error processing tournament archive: {e}")
|
||
continue
|
||
|
||
# Add players from league archives
|
||
leagues = get_archived_leagues()
|
||
for archive in leagues:
|
||
try:
|
||
data = load_archive_file(archive['filepath'])
|
||
if data and 'league' in data:
|
||
participants = data['league'].get('participants', {})
|
||
for player_id, participant in participants.items():
|
||
if player_id not in all_players:
|
||
all_players[player_id] = {
|
||
'id': int(player_id),
|
||
'name': participant.get('name', f'Player {player_id}'),
|
||
'current_player': False
|
||
}
|
||
except Exception as e:
|
||
print(f"Error processing league archive: {e}")
|
||
continue
|
||
|
||
# Convert to list and sort by name
|
||
players_list = list(all_players.values())
|
||
players_list.sort(key=lambda x: x['name'])
|
||
|
||
return players_list
|
||
|
||
|
||
|
||
# Add these routes after the existing routes in app.py
|
||
|
||
# Add this to your app.py file to integrate the modern archive system
|
||
|
||
# Replace your existing archive routes with these updated ones:
|
||
|
||
@app.route('/archive')
|
||
def archive_index():
|
||
"""Archive index page with updated styling"""
|
||
if is_mobile_device():
|
||
return redirect('/mobile/archive')
|
||
|
||
tournaments = get_archived_tournaments() # Now only returns standalone tournaments
|
||
leagues = get_archived_leagues()
|
||
|
||
# Calculate overview stats
|
||
total_tournaments = len(tournaments)
|
||
total_leagues = len(leagues)
|
||
|
||
# Get total players from current players list
|
||
players_data = load_players()
|
||
total_players = len([p for p in players_data['players'] if p['enabled']])
|
||
|
||
# Calculate total competitions (standalone tournaments + completed leagues)
|
||
total_matches = total_tournaments + total_leagues
|
||
|
||
stats = {
|
||
'total_tournaments': total_tournaments,
|
||
'total_leagues': total_leagues,
|
||
'total_players': total_players,
|
||
'total_matches': total_matches
|
||
}
|
||
|
||
# Use the new template
|
||
return render_template('modern_archive_index.html',
|
||
tournaments=tournaments,
|
||
leagues=leagues,
|
||
stats=stats)
|
||
|
||
@app.route('/archive/player-analysis')
|
||
def player_analysis():
|
||
"""Modern Player analysis page"""
|
||
if is_mobile_device():
|
||
return redirect('/mobile/archive/player-analysis')
|
||
|
||
all_players = get_all_players_from_archives()
|
||
|
||
# Calculate overview stats
|
||
total_players = len(all_players)
|
||
active_players = len([p for p in all_players if p['current_player']])
|
||
|
||
# Get average tournaments and top score from archives
|
||
avg_tournaments = 0
|
||
top_score = 0
|
||
|
||
if all_players:
|
||
total_tournament_count = 0
|
||
max_score = 0
|
||
|
||
for player in all_players:
|
||
# Get basic stats for each player
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
player_stats = analyze_player_performance(player['id'], archives_data)
|
||
total_tournament_count += player_stats['total_tournaments']
|
||
if player_stats['best_tournament_score'] > max_score:
|
||
max_score = player_stats['best_tournament_score']
|
||
|
||
avg_tournaments = total_tournament_count // total_players if total_players else 0
|
||
top_score = max_score
|
||
|
||
overview_stats = {
|
||
'total_players': total_players,
|
||
'active_players': active_players,
|
||
'avg_tournaments': avg_tournaments,
|
||
'top_score': top_score
|
||
}
|
||
|
||
return render_template('modern_player_analysis.html',
|
||
players=all_players,
|
||
overview_stats=overview_stats)
|
||
|
||
@app.route('/archive/player/<int:player_id>')
|
||
def view_player_stats(player_id):
|
||
"""Modern Player stats page"""
|
||
if is_mobile_device():
|
||
return redirect(f'/mobile/archive/player/{player_id}')
|
||
|
||
all_players = get_all_players_from_archives()
|
||
player_info = next((p for p in all_players if p['id'] == player_id), None)
|
||
|
||
if not player_info:
|
||
return redirect('/archive/player-analysis')
|
||
|
||
# Get archives data
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
|
||
# Analyze player performance
|
||
player_stats = analyze_player_performance(player_id, archives_data)
|
||
|
||
return render_template('modern_player_stats.html',
|
||
player=player_info,
|
||
stats=player_stats)
|
||
|
||
# Enhanced API endpoints for the modern archive system
|
||
|
||
@app.route('/api/archive/stats', methods=['GET'])
|
||
def api_get_archive_stats():
|
||
"""API endpoint to get archive overview statistics"""
|
||
try:
|
||
tournaments = get_archived_tournaments()
|
||
leagues = get_archived_leagues()
|
||
players_data = load_players()
|
||
|
||
# Calculate activity over time (last 6 months)
|
||
activity_data = {
|
||
'labels': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'],
|
||
'tournaments': [2, 4, 3, 5, 6, 4], # This should be calculated from actual data
|
||
'leagues': [1, 1, 2, 1, 2, 1]
|
||
}
|
||
|
||
# Calculate tournament type distribution - now includes 4_targets
|
||
type_distribution = {'20_targets': 0, '40_targets': 0, '4_targets': 0}
|
||
for tournament in tournaments:
|
||
tournament_type = tournament.get('tournament_type', '20_targets')
|
||
if tournament_type in type_distribution:
|
||
type_distribution[tournament_type] += 1
|
||
|
||
for league in leagues:
|
||
league_type = league.get('tournament_type', '20_targets')
|
||
if league_type in type_distribution:
|
||
type_distribution[league_type] += 1
|
||
|
||
stats = {
|
||
'overview': {
|
||
'total_tournaments': len(tournaments),
|
||
'total_leagues': len(leagues),
|
||
'total_players': len([p for p in players_data['players'] if p['enabled']]),
|
||
'total_matches': len(tournaments) + sum(l.get('completed_tournaments', 0) for l in leagues)
|
||
},
|
||
'activity_data': activity_data,
|
||
'type_distribution': {
|
||
'labels': ['20 Targets', '40 Targets', '4 Targets'],
|
||
'data': [type_distribution['20_targets'], type_distribution['40_targets'], type_distribution['4_targets']]
|
||
}
|
||
}
|
||
|
||
return jsonify({'status': 'success', 'stats': stats})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||
@app.route('/api/archive/player/<int:player_id>/performance', methods=['GET'])
|
||
def api_get_player_performance(player_id):
|
||
"""API endpoint to get player performance data for charts"""
|
||
try:
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
|
||
player_stats = analyze_player_performance(player_id, archives_data)
|
||
|
||
# Prepare chart data
|
||
performance_data = {
|
||
'trend': {
|
||
'labels': [f'T{i+1}' for i in range(len(player_stats['tournament_scores']))],
|
||
'data': player_stats['tournament_scores']
|
||
},
|
||
'distribution': {
|
||
'labels': ['0-50', '51-60', '61-70', '71-80', '81-90', '91-100'],
|
||
'data': [0, 0, 0, 0, 0, 0]
|
||
}
|
||
}
|
||
|
||
# Calculate score distribution
|
||
for score in player_stats['tournament_scores']:
|
||
if score <= 50:
|
||
performance_data['distribution']['data'][0] += 1
|
||
elif score <= 60:
|
||
performance_data['distribution']['data'][1] += 1
|
||
elif score <= 70:
|
||
performance_data['distribution']['data'][2] += 1
|
||
elif score <= 80:
|
||
performance_data['distribution']['data'][3] += 1
|
||
elif score <= 90:
|
||
performance_data['distribution']['data'][4] += 1
|
||
else:
|
||
performance_data['distribution']['data'][5] += 1
|
||
|
||
return jsonify({'status': 'success', 'performance': performance_data})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||
|
||
@app.route('/api/archive/players/with-stats', methods=['GET'])
|
||
def api_get_players_with_stats():
|
||
"""API endpoint to get all players with their basic stats"""
|
||
try:
|
||
all_players = get_all_players_from_archives()
|
||
archives_data = {
|
||
'tournaments': get_archived_tournaments(),
|
||
'leagues': get_archived_leagues()
|
||
}
|
||
|
||
players_with_stats = []
|
||
for player in all_players:
|
||
player_stats = analyze_player_performance(player['id'], archives_data)
|
||
|
||
players_with_stats.append({
|
||
'id': player['id'],
|
||
'name': player['name'],
|
||
'current_player': player['current_player'],
|
||
'stats': {
|
||
'total_tournaments': player_stats['total_tournaments'],
|
||
'total_leagues': player_stats['total_leagues'],
|
||
'best_tournament_score': player_stats['best_tournament_score'],
|
||
'average_tournament_score': player_stats['average_tournament_score'],
|
||
'total_shots_fired': player_stats['total_shots_fired'],
|
||
'performance_trend': player_stats['tournament_scores'][-8:] if len(player_stats['tournament_scores']) >= 8 else player_stats['tournament_scores'] # Last 8 tournaments for mini chart
|
||
}
|
||
})
|
||
|
||
return jsonify({'status': 'success', 'players': players_with_stats})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 500
|
||
|
||
# Add these routes to handle clicking on archived tournaments/leagues
|
||
# These will load the archive data and show it in your existing result page format
|
||
|
||
@app.route('/archive/tournament/<filename>')
|
||
def view_archived_tournament(filename):
|
||
"""View archived tournament in results format"""
|
||
if is_mobile_device():
|
||
return redirect(f'/mobile/archive/tournament/{filename}')
|
||
|
||
filepath = os.path.join(ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/archive')
|
||
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
# Process results for display
|
||
participants = []
|
||
for player_id, participant_data in results_data.get('participants', {}).items():
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': participant_data['name'],
|
||
'total_score': participant_data['total_score'],
|
||
'completed': participant_data['completed'],
|
||
'targets': participant_data.get('targets', {})
|
||
})
|
||
|
||
# Sort by score (descending)
|
||
participants.sort(key=lambda x: x['total_score'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
# Use the existing results display template but with archived data
|
||
return render_template('results_display.html',
|
||
results=results_data,
|
||
participants=participants,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at'),
|
||
'tournament_type': tournament_data.get('tournament_type', '20_targets')
|
||
})
|
||
|
||
@app.route('/archive/league/<filename>')
|
||
def view_archived_league(filename):
|
||
"""View archived league in results format"""
|
||
if is_mobile_device():
|
||
return redirect(f'/mobile/archive/league/{filename}')
|
||
|
||
filepath = os.path.join(LEAGUE_ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/archive')
|
||
|
||
league_data = data.get('league', {})
|
||
|
||
# Process league results using your existing function
|
||
calculate_league_final_scores(league_data)
|
||
participants = get_league_final_rankings(league_data)
|
||
|
||
# Use the existing league results display template but with archived data
|
||
return render_template('league_scoreboard_display.html',
|
||
league=league_data,
|
||
participants=participants,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at'),
|
||
'tournament_type': league_data.get('tournament_type', '20_targets')
|
||
})
|
||
|
||
# Mobile versions
|
||
@app.route('/mobile/archive/tournament/<filename>')
|
||
def mobile_view_archived_tournament(filename):
|
||
"""Mobile view of archived tournament"""
|
||
filepath = os.path.join(ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/mobile/archive')
|
||
|
||
tournament_data = data.get('tournament', {})
|
||
results_data = data.get('results', {})
|
||
|
||
# Process results for display
|
||
participants = []
|
||
for player_id, participant_data in results_data.get('participants', {}).items():
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': participant_data['name'],
|
||
'total_score': participant_data['total_score'],
|
||
'completed': participant_data['completed']
|
||
})
|
||
|
||
# Sort by score (descending)
|
||
participants.sort(key=lambda x: x['total_score'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return render_template('mobile_results.html',
|
||
results=results_data,
|
||
participants=participants,
|
||
show_league_results=False,
|
||
show_tournament_results=True,
|
||
tournament_active=False,
|
||
league=None,
|
||
tournament_type=results_data.get('tournament_type', '20_targets'),
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at')
|
||
})
|
||
|
||
@app.route('/mobile/archive/league/<filename>')
|
||
def mobile_view_archived_league(filename):
|
||
"""Mobile view of archived league"""
|
||
filepath = os.path.join(LEAGUE_ARCHIVE_DIR, filename)
|
||
data = load_archive_file(filepath)
|
||
|
||
if not data:
|
||
return redirect('/mobile/archive')
|
||
|
||
league_data = data.get('league', {})
|
||
|
||
# Process league results
|
||
calculate_league_final_scores(league_data)
|
||
participants = get_league_final_rankings(league_data)
|
||
|
||
return render_template('mobile_results.html',
|
||
league=league_data,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=False,
|
||
results=None,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': filename,
|
||
'archived_at': data.get('archived_at')
|
||
})
|
||
# MAIN ROUTES
|
||
@app.route('/')
|
||
def index():
|
||
# Redirect mobile users to mobile menu
|
||
if is_mobile_device():
|
||
return redirect('/mobile')
|
||
|
||
# Desktop users get the regular dashboard
|
||
settings = load_settings()
|
||
|
||
# Check if tournament is active and get current round players
|
||
current_round_data = get_current_round_data()
|
||
|
||
# If tournament is active, override camera titles with player names
|
||
if current_round_data:
|
||
tournament_titles = {}
|
||
players = current_round_data['players']
|
||
|
||
for i in range(6): # Always 6 positions
|
||
if i < len(players):
|
||
tournament_titles[str(i + 1)] = players[i]['name']
|
||
else:
|
||
tournament_titles[str(i + 1)] = 'Empty'
|
||
|
||
# Create a copy of settings with tournament titles
|
||
display_settings = settings.copy()
|
||
display_settings['camera_titles'] = tournament_titles
|
||
display_settings['tournament_active'] = True
|
||
|
||
tournament_state = load_tournament_state()
|
||
display_settings['current_round'] = tournament_state.get('current_round', 1)
|
||
display_settings['total_rounds'] = tournament_state.get('total_rounds', 1)
|
||
|
||
# Add league info if available
|
||
league_state = load_league_state()
|
||
if league_state:
|
||
display_settings['league_active'] = True
|
||
display_settings['league_tournament'] = league_state.get('current_tournament', 1)
|
||
display_settings['league_total'] = league_state.get('total_tournaments', 6)
|
||
else:
|
||
display_settings = settings.copy()
|
||
display_settings['tournament_active'] = False
|
||
display_settings['current_round'] = 1
|
||
display_settings['total_rounds'] = 1
|
||
display_settings['league_active'] = False
|
||
|
||
return render_template('index.html', streams=STREAMS, settings=display_settings)
|
||
|
||
# MOBILE ROUTES
|
||
@app.route('/mobile')
|
||
def mobile_menu():
|
||
"""Mobile main menu page"""
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
tournament_active = tournament_state is not None
|
||
league_active = league_state is not None and not league_state.get('league_finished', False)
|
||
results_available = results is not None and results.get('tournament_finished', False)
|
||
league_results_available = league_state is not None and league_state.get('league_finished', False)
|
||
|
||
return render_template('mobile_menu.html',
|
||
tournament_active=tournament_active,
|
||
league_active=league_active,
|
||
tournament_state=tournament_state,
|
||
league_state=league_state,
|
||
results_available=results_available,
|
||
league_results_available=league_results_available)
|
||
|
||
@app.route('/mobile/streams')
|
||
def mobile_streams():
|
||
"""Mobile streams page"""
|
||
settings = load_settings()
|
||
tournament_state = load_tournament_state()
|
||
|
||
# Check if tournament is active
|
||
tournament_active = tournament_state is not None
|
||
current_round_data = get_current_round_data() if tournament_active else None
|
||
|
||
return render_template('mobile_streams.html',
|
||
streams=STREAMS,
|
||
settings=settings,
|
||
tournament_active=tournament_active,
|
||
current_round_data=current_round_data,
|
||
tournament_state=tournament_state)
|
||
|
||
@app.route('/mobile/draft')
|
||
def mobile_draft():
|
||
"""Mobile tournament draft page"""
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
if not tournament_state:
|
||
return redirect('/mobile')
|
||
|
||
return render_template('mobile_draft.html',
|
||
tournament=tournament_state,
|
||
league=league_state)
|
||
|
||
|
||
@app.route('/mobile/results')
|
||
def mobile_results():
|
||
"""Mobile results page"""
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
# Priority 1: Show league results if there's an active or finished league
|
||
if league_state:
|
||
if league_state.get('league_finished', False):
|
||
# Show final league results
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_final_rankings(league_state)
|
||
|
||
return render_template('mobile_results.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=False,
|
||
results=None)
|
||
else:
|
||
# Show ongoing league scoreboard
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_current_standings(league_state)
|
||
|
||
tournament_state = load_tournament_state()
|
||
tournament_active = tournament_state is not None
|
||
current_tournament_results = results
|
||
|
||
return render_template('mobile_league_results.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=tournament_active,
|
||
tournament_state=tournament_state,
|
||
current_tournament_results=current_tournament_results)
|
||
|
||
# Priority 1.5: Check if current results are from a finished league
|
||
elif results and results.get('league_tournament_number'):
|
||
# This is a league tournament result, but league state was archived
|
||
league_archives = get_archived_leagues()
|
||
|
||
if league_archives:
|
||
latest_archive = league_archives[0]
|
||
archive_data = load_archive_file(latest_archive['filepath'])
|
||
|
||
if archive_data and 'league' in archive_data:
|
||
archived_league = archive_data['league']
|
||
calculate_league_final_scores(archived_league)
|
||
participants = get_league_final_rankings(archived_league)
|
||
|
||
return render_template('mobile_results.html',
|
||
league=archived_league,
|
||
participants=participants,
|
||
show_league_results=True,
|
||
show_tournament_results=False,
|
||
tournament_active=False,
|
||
results=None,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': latest_archive['filename'],
|
||
'archived_at': archive_data.get('archived_at')
|
||
})
|
||
|
||
return redirect('/mobile/archive')
|
||
|
||
# Priority 2: Show individual tournament results (standalone tournament only)
|
||
elif results and results.get('tournament_finished', False):
|
||
participants = []
|
||
for player_id, data in results['participants'].items():
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'total_score': data['total_score'],
|
||
'completed': data['completed']
|
||
})
|
||
|
||
# Sort by score (descending)
|
||
participants.sort(key=lambda x: x['total_score'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return render_template('mobile_results.html',
|
||
results=results,
|
||
participants=participants,
|
||
show_league_results=False,
|
||
show_tournament_results=True,
|
||
tournament_active=False,
|
||
league=None,
|
||
tournament_type=results.get('tournament_type', '20_targets'))
|
||
else:
|
||
return redirect('/mobile')
|
||
|
||
# DESKTOP ROUTES
|
||
@app.route('/fullscreen/<int:camera_id>')
|
||
def fullscreen(camera_id):
|
||
# Get the camera stream data
|
||
if 1 <= camera_id <= len(STREAMS):
|
||
settings = load_settings()
|
||
stream = STREAMS[camera_id - 1]
|
||
|
||
# Check if tournament is active for title
|
||
current_round_data = get_current_round_data()
|
||
if current_round_data and camera_id <= len(current_round_data['players']):
|
||
camera_title = current_round_data['players'][camera_id - 1]['name']
|
||
else:
|
||
camera_title = settings['camera_titles'].get(str(camera_id), f'Camera {camera_id}')
|
||
|
||
custom_title = request.args.get('title', camera_title)
|
||
|
||
return render_template('fullscreen.html',
|
||
stream=stream,
|
||
camera_id=camera_id,
|
||
title=custom_title,
|
||
settings=settings)
|
||
else:
|
||
return redirect('/')
|
||
|
||
@app.route('/tournament')
|
||
def tournament():
|
||
"""Tournament management page"""
|
||
# Check if mobile device
|
||
if is_mobile_device():
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
if tournament_state:
|
||
# Mobile users with active tournament go to draft view
|
||
return redirect('/mobile/draft')
|
||
else:
|
||
# Mobile users without tournament go to mobile menu
|
||
return redirect('/mobile')
|
||
|
||
# Desktop users get full tournament management
|
||
players_data = load_players()
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
|
||
return render_template('tournament.html',
|
||
players=players_data['players'],
|
||
tournament_state=tournament_state,
|
||
league_state=league_state)
|
||
|
||
@app.route('/tournament/draft')
|
||
def tournament_draft():
|
||
"""Tournament draft page"""
|
||
# Redirect mobile users to mobile draft
|
||
if is_mobile_device():
|
||
return redirect('/mobile/draft')
|
||
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
if not tournament_state:
|
||
return redirect('/tournament')
|
||
|
||
return render_template('draft.html',
|
||
tournament=tournament_state,
|
||
league=league_state)
|
||
|
||
@app.route('/results/calculator')
|
||
def results_calculator():
|
||
"""Results calculator page (desktop only)"""
|
||
if is_mobile_device():
|
||
return redirect('/mobile/streams')
|
||
|
||
tournament_state = load_tournament_state()
|
||
if not tournament_state:
|
||
return redirect('/tournament')
|
||
|
||
# Get or create results structure
|
||
results = load_results()
|
||
if not results:
|
||
results = create_results_structure(tournament_state)
|
||
save_results(results)
|
||
|
||
return render_template('results_calculator.html',
|
||
tournament=tournament_state,
|
||
results=results)
|
||
|
||
@app.route('/results')
|
||
def results_display():
|
||
"""Results display page"""
|
||
# Redirect mobile users to mobile results
|
||
if is_mobile_device():
|
||
return redirect('/mobile/results')
|
||
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
# Priority 1: Show league results if there's an active or finished league
|
||
if league_state:
|
||
if league_state.get('league_finished', False):
|
||
# Show final league results
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_final_rankings(league_state)
|
||
|
||
return render_template('league_scoreboard_display.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
results=None)
|
||
else:
|
||
# Show ongoing league scoreboard
|
||
calculate_league_final_scores(league_state)
|
||
participants = get_league_current_standings(league_state)
|
||
|
||
tournament_state = load_tournament_state()
|
||
current_tournament_results = results
|
||
|
||
return render_template('league_scoreboard_display.html',
|
||
league=league_state,
|
||
participants=participants,
|
||
tournament_state=tournament_state,
|
||
current_tournament_results=current_tournament_results,
|
||
results=None)
|
||
|
||
# Priority 1.5: Check if current results are from a finished league (even if league state was archived)
|
||
elif results and results.get('league_tournament_number'):
|
||
# This is a league tournament result, but league state was archived
|
||
# Try to find the archived league data
|
||
league_archives = get_archived_leagues()
|
||
|
||
# Find the most recent league archive (should be the one that just finished)
|
||
if league_archives:
|
||
latest_archive = league_archives[0] # Already sorted by date (newest first)
|
||
archive_data = load_archive_file(latest_archive['filepath'])
|
||
|
||
if archive_data and 'league' in archive_data:
|
||
archived_league = archive_data['league']
|
||
|
||
# Show final league results from archive
|
||
calculate_league_final_scores(archived_league)
|
||
participants = get_league_final_rankings(archived_league)
|
||
|
||
return render_template('league_scoreboard_display.html',
|
||
league=archived_league,
|
||
participants=participants,
|
||
results=None,
|
||
archived=True,
|
||
archive_info={
|
||
'filename': latest_archive['filename'],
|
||
'archived_at': archive_data.get('archived_at')
|
||
})
|
||
|
||
# If we can't find the archive, redirect to archive page
|
||
return redirect('/archive')
|
||
|
||
# Priority 2: Show individual tournament results (standalone tournament only)
|
||
elif results and results.get('tournament_finished', False):
|
||
participants = []
|
||
for player_id, data in results['participants'].items():
|
||
participants.append({
|
||
'id': player_id,
|
||
'name': data['name'],
|
||
'total_score': data['total_score'],
|
||
'completed': data['completed']
|
||
})
|
||
|
||
# Sort by score (descending)
|
||
participants.sort(key=lambda x: x['total_score'], reverse=True)
|
||
|
||
# Add rankings
|
||
for i, participant in enumerate(participants):
|
||
participant['rank'] = i + 1
|
||
|
||
return render_template('results_display.html',
|
||
results=results,
|
||
participants=participants,
|
||
league=None)
|
||
else:
|
||
return redirect('/tournament')
|
||
|
||
# API Routes
|
||
@app.route('/api/settings', methods=['GET'])
|
||
def get_settings():
|
||
"""API endpoint to get current settings"""
|
||
settings = load_settings()
|
||
return jsonify(settings)
|
||
|
||
@app.route('/api/settings', methods=['POST'])
|
||
def update_settings():
|
||
"""API endpoint to update settings"""
|
||
try:
|
||
new_settings = request.get_json()
|
||
|
||
# Load current settings
|
||
current_settings = load_settings()
|
||
|
||
# Update only provided fields
|
||
if 'camera_titles' in new_settings:
|
||
current_settings['camera_titles'].update(new_settings['camera_titles'])
|
||
|
||
if 'display_options' in new_settings:
|
||
current_settings['display_options'].update(new_settings['display_options'])
|
||
|
||
# Save updated settings
|
||
if save_settings(current_settings):
|
||
return jsonify({'status': 'success', 'settings': current_settings})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save settings'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players', methods=['GET'])
|
||
def get_players():
|
||
"""API endpoint to get all players"""
|
||
players_data = load_players()
|
||
return jsonify(players_data)
|
||
|
||
@app.route('/api/players', methods=['POST'])
|
||
def update_players():
|
||
"""API endpoint to update players"""
|
||
try:
|
||
players_data = request.get_json()
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success', 'players': players_data})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save players'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players/add', methods=['POST'])
|
||
def add_player():
|
||
"""API endpoint to add a new player"""
|
||
try:
|
||
data = request.get_json()
|
||
name = data.get('name', '').strip()
|
||
|
||
if not name:
|
||
return jsonify({'status': 'error', 'message': 'Player name is required'}), 400
|
||
|
||
players_data = load_players()
|
||
|
||
# Find next available ID
|
||
existing_ids = [p['id'] for p in players_data['players']]
|
||
new_id = max(existing_ids) + 1 if existing_ids else 1
|
||
|
||
# Add new player
|
||
new_player = {
|
||
'id': new_id,
|
||
'name': name,
|
||
'enabled': True
|
||
}
|
||
|
||
players_data['players'].append(new_player)
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success', 'player': new_player})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save player'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players/<int:player_id>/delete', methods=['POST'])
|
||
def delete_player(player_id):
|
||
"""API endpoint to delete a player"""
|
||
try:
|
||
players_data = load_players()
|
||
|
||
# Find and remove the player
|
||
players_data['players'] = [p for p in players_data['players'] if p['id'] != player_id]
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success'})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save players'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/players/<int:player_id>', methods=['POST'])
|
||
def update_player(player_id):
|
||
"""API endpoint to update a single player"""
|
||
try:
|
||
data = request.get_json()
|
||
players_data = load_players()
|
||
|
||
# Find and update the player
|
||
for player in players_data['players']:
|
||
if player['id'] == player_id:
|
||
if 'name' in data:
|
||
player['name'] = data['name']
|
||
if 'enabled' in data:
|
||
player['enabled'] = data['enabled']
|
||
break
|
||
|
||
if save_players(players_data):
|
||
return jsonify({'status': 'success', 'player': player})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save player'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
# LEAGUE API ROUTES
|
||
@app.route('/api/league/start', methods=['POST'])
|
||
def start_league():
|
||
"""API endpoint to start a new league"""
|
||
try:
|
||
data = request.get_json()
|
||
tournament_type = data.get('tournament_type', '20_targets')
|
||
if tournament_type not in ['20_targets', '40_targets', '4_targets']:
|
||
return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400
|
||
|
||
players_data = load_players()
|
||
enabled_players = [p for p in players_data['players'] if p['enabled']]
|
||
|
||
if len(enabled_players) < 1:
|
||
return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400
|
||
|
||
league_data = create_league(enabled_players, tournament_type)
|
||
|
||
if save_league_state(league_data):
|
||
return jsonify({'status': 'success', 'league': league_data})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save league'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/league/tournament/start', methods=['POST'])
|
||
def start_league_tournament():
|
||
"""API endpoint to start next tournament in league"""
|
||
try:
|
||
league_state = load_league_state()
|
||
if not league_state:
|
||
return jsonify({'status': 'error', 'message': 'No active league'}), 400
|
||
|
||
if league_state.get('league_finished', False):
|
||
return jsonify({'status': 'error', 'message': 'League already finished'}), 400
|
||
|
||
current_tournament = league_state.get('current_tournament', 0)
|
||
if current_tournament >= league_state['total_tournaments']:
|
||
return jsonify({'status': 'error', 'message': 'All tournaments completed'}), 400
|
||
|
||
# Get players for this tournament (excluding joker users for this round)
|
||
data = request.get_json() or {}
|
||
joker_players = data.get('joker_players', []) # List of player IDs using joker
|
||
|
||
# Update joker usage in league
|
||
for player_id in joker_players:
|
||
player_id_str = str(player_id)
|
||
if player_id_str in league_state['participants']:
|
||
if league_state['participants'][player_id_str]['joker_used']:
|
||
return jsonify({'status': 'error', 'message': f'Player {player_id} already used joker'}), 400
|
||
league_state['participants'][player_id_str]['joker_used'] = True
|
||
|
||
# Create list of participating players (not using joker)
|
||
participating_players = []
|
||
for player_id, participant in league_state['participants'].items():
|
||
if int(player_id) not in joker_players:
|
||
participating_players.append({
|
||
'id': int(player_id),
|
||
'name': participant['name']
|
||
})
|
||
|
||
if len(participating_players) < 1:
|
||
return jsonify({'status': 'error', 'message': 'Need at least 1 participating player'}), 400
|
||
|
||
# Start next tournament
|
||
league_state['current_tournament'] = current_tournament + 1
|
||
tournament_data = create_draft(
|
||
participating_players,
|
||
league_state['tournament_type'],
|
||
league_state['current_tournament']
|
||
)
|
||
|
||
# Save states
|
||
save_league_state(league_state)
|
||
|
||
if save_tournament_state(tournament_data):
|
||
# Create results structure
|
||
results = create_results_structure(tournament_data)
|
||
save_results(results)
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'tournament': tournament_data,
|
||
'league': league_state
|
||
})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/league/reset', methods=['POST'])
|
||
def reset_league():
|
||
"""API endpoint to reset/clear league"""
|
||
try:
|
||
# Archive current league if it exists
|
||
league_state = load_league_state()
|
||
if league_state:
|
||
archive_league(league_state)
|
||
|
||
# Remove league, tournament, and results files
|
||
for file_path in [LEAGUE_FILE, TOURNAMENT_FILE, RESULTS_FILE]:
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
|
||
return jsonify({'status': 'success'})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
# TOURNAMENT API ROUTES (Updated)
|
||
@app.route('/api/tournament/start', methods=['POST'])
|
||
def start_tournament():
|
||
"""API endpoint to start a standalone tournament"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
tournament_type = data.get('tournament_type', '20_targets')
|
||
|
||
if tournament_type not in ['20_targets', '40_targets', '4_targets']:
|
||
return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400
|
||
|
||
players_data = load_players()
|
||
enabled_players = [p for p in players_data['players'] if p['enabled']]
|
||
|
||
if len(enabled_players) < 1:
|
||
return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400
|
||
|
||
tournament_data = create_draft(enabled_players, tournament_type)
|
||
|
||
if save_tournament_state(tournament_data):
|
||
# Create results structure when tournament starts
|
||
results = create_results_structure(tournament_data)
|
||
save_results(results)
|
||
|
||
return jsonify({'status': 'success', 'tournament': tournament_data})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/tournament/round/<int:round_number>', methods=['POST'])
|
||
def change_round(round_number):
|
||
"""API endpoint to change current round"""
|
||
try:
|
||
tournament_state = load_tournament_state()
|
||
if not tournament_state:
|
||
return jsonify({'status': 'error', 'message': 'No active tournament'}), 400
|
||
|
||
if round_number < 1 or round_number > tournament_state['total_rounds']:
|
||
return jsonify({'status': 'error', 'message': 'Invalid round number'}), 400
|
||
|
||
tournament_state['current_round'] = round_number
|
||
|
||
# Update round statuses
|
||
for round_data in tournament_state['rounds']:
|
||
if round_data['round_number'] < round_number:
|
||
round_data['status'] = 'completed'
|
||
elif round_data['round_number'] == round_number:
|
||
round_data['status'] = 'pending'
|
||
else:
|
||
round_data['status'] = 'waiting'
|
||
|
||
if save_tournament_state(tournament_state):
|
||
return jsonify({'status': 'success', 'tournament': tournament_state})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/tournament/reset', methods=['POST'])
|
||
def reset_tournament():
|
||
"""API endpoint to reset/clear tournament"""
|
||
try:
|
||
if os.path.exists(TOURNAMENT_FILE):
|
||
os.remove(TOURNAMENT_FILE)
|
||
if os.path.exists(RESULTS_FILE):
|
||
os.remove(RESULTS_FILE)
|
||
return jsonify({'status': 'success'})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
# RESULTS API Routes (Updated)
|
||
@app.route('/api/results/participant/<int:player_id>', methods=['POST'])
|
||
def update_participant_scores(player_id):
|
||
"""API endpoint to update participant scores"""
|
||
try:
|
||
data = request.get_json()
|
||
results = load_results()
|
||
|
||
if not results:
|
||
return jsonify({'status': 'error', 'message': 'No results data found'}), 400
|
||
|
||
player_id_str = str(player_id)
|
||
if player_id_str not in results['participants']:
|
||
return jsonify({'status': 'error', 'message': 'Player not found'}), 400
|
||
|
||
# Update scores
|
||
if 'targets' in data:
|
||
results['participants'][player_id_str]['targets'].update(data['targets'])
|
||
|
||
# Recalculate total score
|
||
targets = results['participants'][player_id_str]['targets']
|
||
total_score = calculate_total_score(targets)
|
||
results['participants'][player_id_str]['total_score'] = total_score
|
||
|
||
# Check if all targets are completed (all shots entered, including 0s)
|
||
all_completed = is_participant_completed(targets)
|
||
results['participants'][player_id_str]['completed'] = all_completed
|
||
|
||
if save_results(results):
|
||
return jsonify({'status': 'success', 'participant': results['participants'][player_id_str]})
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save results'}), 500
|
||
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/results/finish', methods=['POST'])
|
||
def finish_tournament():
|
||
"""API endpoint to finish tournament"""
|
||
try:
|
||
results = load_results()
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
|
||
if not results:
|
||
return jsonify({'status': 'error', 'message': 'No results data found'}), 400
|
||
|
||
if not tournament_state:
|
||
return jsonify({'status': 'error', 'message': 'No active tournament found'}), 400
|
||
|
||
# Check if all participants are completed
|
||
all_completed = all(
|
||
participant['completed']
|
||
for participant in results['participants'].values()
|
||
)
|
||
|
||
if not all_completed:
|
||
return jsonify({'status': 'error', 'message': 'Not all participants have completed scores'}), 400
|
||
|
||
# Mark tournament as finished
|
||
results['tournament_finished'] = True
|
||
results['finished_at'] = datetime.now().isoformat()
|
||
|
||
# Define total shots per participant for each tournament type
|
||
total_shots_per_participant = {
|
||
'20_targets': 40, # 20 targets × 2 shots
|
||
'40_targets': 80, # 40 targets × 2 shots
|
||
'4_targets': 20 # 4 targets × 5 shots
|
||
}
|
||
|
||
league_finished = False # Track if league finished
|
||
|
||
# Update league state if this is a league tournament
|
||
if league_state and 'league_tournament_number' in results:
|
||
tournament_number = results['league_tournament_number']
|
||
|
||
# Update league with tournament results
|
||
for player_id, participant in results['participants'].items():
|
||
if player_id in league_state['participants']:
|
||
league_participant = league_state['participants'][player_id]
|
||
|
||
# Add tournament result
|
||
league_participant['tournament_results'].append({
|
||
'tournament': tournament_number,
|
||
'score': participant['total_score'],
|
||
'participated': True
|
||
})
|
||
|
||
# Update total score
|
||
league_participant['total_score'] += participant['total_score']
|
||
|
||
# Add results for players who used joker (didn't participate)
|
||
for player_id, participant in league_state['participants'].items():
|
||
if player_id not in results['participants']:
|
||
# This player used joker
|
||
participant['tournament_results'].append({
|
||
'tournament': tournament_number,
|
||
'score': 0,
|
||
'participated': False,
|
||
'joker': True
|
||
})
|
||
|
||
# Calculate total shots correctly for any tournament type
|
||
tournament_type = results.get('tournament_type', '20_targets')
|
||
shots_per_participant = total_shots_per_participant.get(tournament_type, 40)
|
||
total_shots_fired = len(results['participants']) * shots_per_participant
|
||
|
||
# Add to completed tournaments
|
||
league_state['completed_tournaments'].append({
|
||
'tournament_number': tournament_number,
|
||
'tournament_type': tournament_type,
|
||
'finished_at': datetime.now().isoformat(),
|
||
'results_summary': {
|
||
'participants': len(results['participants']),
|
||
'shots_per_participant': shots_per_participant,
|
||
'total_shots': total_shots_fired,
|
||
'format_description': get_tournament_format_description(tournament_type)
|
||
}
|
||
})
|
||
|
||
# Check if league is finished
|
||
if tournament_number >= league_state['total_tournaments']:
|
||
league_state['league_finished'] = True
|
||
league_state['finished_at'] = datetime.now().isoformat()
|
||
|
||
# Calculate final scores
|
||
calculate_league_final_scores(league_state)
|
||
|
||
league_finished = True
|
||
print(f"League finished! Final tournament was {tournament_type} format.")
|
||
|
||
save_league_state(league_state)
|
||
|
||
# Archive the tournament (only if it's NOT part of a league)
|
||
archive_success = False
|
||
if not league_state: # Only archive standalone tournaments
|
||
archive_success = archive_tournament(tournament_state, results)
|
||
tournament_type = results.get('tournament_type', '20_targets')
|
||
print(f"Standalone {tournament_type} tournament archived: {archive_success}")
|
||
else:
|
||
tournament_type = results.get('tournament_type', '20_targets')
|
||
print(f"League {tournament_type} tournament - not archiving individual tournament")
|
||
|
||
# Archive the league if it just finished
|
||
league_archive_success = False
|
||
if league_finished and league_state:
|
||
league_archive_success = archive_league(league_state)
|
||
print(f"League archived: {league_archive_success}")
|
||
|
||
# Save final results
|
||
if save_results(results):
|
||
# End the tournament by removing tournament state
|
||
if os.path.exists(TOURNAMENT_FILE):
|
||
os.remove(TOURNAMENT_FILE)
|
||
|
||
# If league finished, also remove league state file
|
||
if league_finished and os.path.exists(LEAGUE_FILE):
|
||
os.remove(LEAGUE_FILE)
|
||
|
||
response_data = {
|
||
'status': 'success',
|
||
'results': results,
|
||
'archived': archive_success,
|
||
'league_archived': league_archive_success if league_finished else None,
|
||
'tournament_type': results.get('tournament_type', '20_targets'),
|
||
'tournament_format': get_tournament_format_description(results.get('tournament_type', '20_targets'))
|
||
}
|
||
|
||
if league_state:
|
||
response_data['league'] = league_state
|
||
response_data['league_finished'] = league_state.get('league_finished', False)
|
||
|
||
return jsonify(response_data)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'Failed to save results'}), 500
|
||
|
||
except Exception as e:
|
||
print(f"Error finishing tournament: {e}")
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
|
||
def get_tournament_format_description(tournament_type):
|
||
"""Get human-readable description of tournament format"""
|
||
format_descriptions = {
|
||
'20_targets': '20 Targets (2 shots each)',
|
||
'40_targets': '40 Targets (2 shots each)',
|
||
'4_targets': '4 Targets (5 shots each)'
|
||
}
|
||
return format_descriptions.get(tournament_type, '20 Targets (2 shots each)')
|
||
@app.route('/api/results', methods=['GET'])
|
||
def get_results():
|
||
"""API endpoint to get current results"""
|
||
results = load_results()
|
||
if results:
|
||
return jsonify(results)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'No results found'}), 404
|
||
|
||
@app.route('/api/league', methods=['GET'])
|
||
def get_league():
|
||
"""API endpoint to get current league state"""
|
||
league_state = load_league_state()
|
||
if league_state:
|
||
return jsonify(league_state)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'No league found'}), 404
|
||
|
||
# Add this route to your Flask app (around line 850, with the other mobile routes)
|
||
|
||
@app.route('/mobile/remote')
|
||
def mobile_remote():
|
||
"""Mobile remote control page"""
|
||
# This page doesn't redirect mobile users - it's specifically for mobile control
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
return render_template('mobile_remote.html',
|
||
tournament_state=tournament_state,
|
||
league_state=league_state,
|
||
results=results)
|
||
|
||
# Add these API endpoints for the remote control functionality
|
||
|
||
@app.route('/api/tournament', methods=['GET'])
|
||
def get_tournament():
|
||
"""API endpoint to get current tournament state"""
|
||
tournament_state = load_tournament_state()
|
||
if tournament_state:
|
||
return jsonify(tournament_state)
|
||
else:
|
||
return jsonify({'status': 'error', 'message': 'No tournament found'}), 404
|
||
|
||
@app.route('/api/remote/refresh_dashboard', methods=['POST'])
|
||
def refresh_dashboard():
|
||
"""API endpoint to trigger dashboard refresh"""
|
||
try:
|
||
# You could add logic here to trigger refresh on connected clients
|
||
# For now, just return success
|
||
return jsonify({'status': 'success', 'message': 'Refresh signal sent'})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/remote/system_info', methods=['GET'])
|
||
def get_system_info():
|
||
"""API endpoint to get comprehensive system information"""
|
||
try:
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
players_data = load_players()
|
||
settings = load_settings()
|
||
|
||
system_info = {
|
||
'timestamp': datetime.now().isoformat(),
|
||
'tournament': {
|
||
'active': tournament_state is not None,
|
||
'data': tournament_state
|
||
},
|
||
'league': {
|
||
'active': league_state is not None and not league_state.get('league_finished', False),
|
||
'finished': league_state is not None and league_state.get('league_finished', False),
|
||
'data': league_state
|
||
},
|
||
'results': {
|
||
'available': results is not None,
|
||
'finished': results is not None and results.get('tournament_finished', False),
|
||
'data': results
|
||
},
|
||
'players': {
|
||
'total': len(players_data['players']) if players_data else 0,
|
||
'enabled': len([p for p in players_data['players'] if p['enabled']]) if players_data else 0
|
||
},
|
||
'settings': settings
|
||
}
|
||
|
||
return jsonify(system_info)
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/remote/emergency_reset', methods=['POST'])
|
||
def emergency_reset():
|
||
"""API endpoint for emergency system reset"""
|
||
try:
|
||
# Archive current states before reset
|
||
tournament_state = load_tournament_state()
|
||
league_state = load_league_state()
|
||
results = load_results()
|
||
|
||
if tournament_state and results:
|
||
archive_tournament(tournament_state, results)
|
||
|
||
if league_state:
|
||
archive_league(league_state)
|
||
|
||
# Remove all state files
|
||
for file_path in [LEAGUE_FILE, TOURNAMENT_FILE, RESULTS_FILE]:
|
||
if os.path.exists(file_path):
|
||
os.remove(file_path)
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'message': 'Emergency reset completed',
|
||
'archived': True
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
@app.route('/api/remote/camera_titles', methods=['GET'])
|
||
def get_camera_titles():
|
||
"""API endpoint to get camera titles"""
|
||
try:
|
||
settings = load_settings()
|
||
current_round_data = get_current_round_data()
|
||
|
||
titles = {}
|
||
|
||
if current_round_data:
|
||
# Tournament active - use player names
|
||
players = current_round_data['players']
|
||
for i in range(6):
|
||
if i < len(players):
|
||
titles[str(i + 1)] = players[i]['name']
|
||
else:
|
||
titles[str(i + 1)] = 'Empty'
|
||
else:
|
||
# No tournament - use configured titles
|
||
titles = settings['camera_titles']
|
||
|
||
return jsonify({
|
||
'status': 'success',
|
||
'titles': titles,
|
||
'tournament_active': current_round_data is not None
|
||
})
|
||
except Exception as e:
|
||
return jsonify({'status': 'error', 'message': str(e)}), 400
|
||
|
||
|
||
|
||
if __name__ == '__main__':
|
||
app.run(host='0.0.0.0', port=5000, debug=True) |