Files
Sdk_TV_app/TV_APP_V2/app.py
T
2025-07-30 17:53:24 +02:00

2058 lines
78 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')
num_targets = 40 if tournament_type == '40_targets' else 20
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'])
results['participants'][player_id] = {
'name': player['name'],
'targets': {str(i): {'shot1': None, 'shot2': None} for i in range(1, num_targets + 1)},
'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():
shot1 = target.get('shot1')
shot2 = target.get('shot2')
total += (shot1 if shot1 is not None else 0)
total += (shot2 if shot2 is not None else 0)
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():
shot1 = target.get('shot1')
shot2 = target.get('shot2')
if shot1 is None or shot2 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
type_distribution = {'20_targets': 0, '40_targets': 0}
for tournament in tournaments:
tournament_type = tournament.get('tournament_type', '20_targets')
type_distribution[tournament_type] += 1
for league in leagues:
league_type = league.get('tournament_type', '20_targets')
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'],
'data': [type_distribution['20_targets'], type_distribution['40_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) # No individual tournament results
else:
# Show ongoing league scoreboard
calculate_league_final_scores(league_state)
participants = get_league_current_standings(league_state)
# Check if there's a current tournament
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 2: Show individual tournament results (standalone tournament)
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, # ← FIXED: Pass as '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)
else:
# Show ongoing league scoreboard
calculate_league_final_scores(league_state) # Calculate current standings
participants = get_league_current_standings(league_state) # Use the helper function
# Add tournament information
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)
# Priority 2: Show individual tournament results (standalone tournament)
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)
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']:
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']:
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()
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
})
# Add to completed tournaments
league_state['completed_tournaments'].append({
'tournament_number': tournament_number,
'finished_at': datetime.now().isoformat(),
'results_summary': {
'participants': len(results['participants']),
'total_shots': len(results['participants']) * (40 if results.get('tournament_type') == '40_targets' else 20) * 2
}
})
# 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("League finished!")
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)
print(f"Standalone tournament archived: {archive_success}")
else:
print("League 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
}
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
@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)