Files
Sdk_TV_app/app.py
T

2619 lines
100 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 - NOW WITH PROPER COUNTING FOR ALL FORMATS
tournament_type = archive.get('tournament_type', '20_targets')
shots_in_tournament = count_shots_in_tournament(participant, tournament_type)
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 # NOW CORRECTLY CALCULATED
})
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/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
@app.route('/api/archive/player/<int:player_id>/shot-accuracy')
def get_player_shot_accuracy(player_id):
"""
Get aggregated shot accuracy data for a player from tournament archive files
"""
try:
# Get player name from JSON file instead of database
players_data = load_players()
player = None
for p in players_data['players']:
if p['id'] == player_id:
player = p
break
if not player:
return jsonify({
'status': 'error',
'message': f'Player with ID {player_id} not found'
}), 404
player_name = player['name']
# Initialize shot counts by tournament type
shot_accuracy = {
'40 Targets': defaultdict(int),
'20 Targets': defaultdict(int),
'4 Targets': defaultdict(int)
}
# Path to tournament archives
tournament_archives_path = 'tournament_archives'
# Check if directory exists
if not os.path.exists(tournament_archives_path):
return jsonify({
'status': 'error',
'message': f'Tournament archives directory not found: {tournament_archives_path}'
}), 404
# Find all tournament archive files
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
if not archive_files:
return jsonify({
'status': 'success',
'data': {},
'player_name': player_name,
'message': 'No tournament archive files found'
})
# Process each tournament archive file
for file_path in archive_files:
try:
with open(file_path, 'r') as f:
tournament_data = json.load(f)
# Check if this tournament has our player
participants = tournament_data.get('results', {}).get('participants', {})
# Find player by ID or name
player_data = None
for participant_id, participant_info in participants.items():
if (str(participant_id) == str(player_id) or
participant_info.get('name') == player_name):
player_data = participant_info
break
if not player_data or not player_data.get('completed'):
continue
# Determine tournament type
tournament_type = determine_tournament_type_from_archive(tournament_data)
# Extract individual shots - NOW WITH PROPER SHOT COUNTING FOR ALL FORMATS
shots = extract_shots_from_player_data(player_data, tournament_type)
# Debug print to verify shot counts
print(f"Player {player_name}, Tournament type: {tournament_type}, Total shots extracted: {len(shots)}")
# Count shots by value
for shot_value in shots:
if shot_value == 10:
shot_accuracy[tournament_type]['tens'] += 1
elif shot_value == 9:
shot_accuracy[tournament_type]['nines'] += 1
elif shot_value == 8:
shot_accuracy[tournament_type]['eights'] += 1
elif shot_value == 7:
shot_accuracy[tournament_type]['sevens'] += 1
elif shot_value == 6:
shot_accuracy[tournament_type]['sixes'] += 1
elif shot_value == 5:
shot_accuracy[tournament_type]['fives'] += 1
elif shot_value == 4:
shot_accuracy[tournament_type]['fours'] += 1
elif shot_value == 3:
shot_accuracy[tournament_type]['threes'] += 1
elif shot_value == 2:
shot_accuracy[tournament_type]['twos'] += 1
elif shot_value == 1:
shot_accuracy[tournament_type]['ones'] += 1
elif shot_value == 0:
shot_accuracy[tournament_type]['zeros'] += 1
except Exception as e:
print(f"Error processing tournament file {file_path}: {e}")
continue
# Convert defaultdict to regular dict for JSON serialization
result = {}
for tournament_type, counts in shot_accuracy.items():
if any(counts.values()): # Only include types with data
result[tournament_type] = dict(counts)
return jsonify({
'status': 'success',
'data': result,
'player_name': player_name,
'files_processed': len(archive_files)
})
except Exception as e:
import traceback
return jsonify({
'status': 'error',
'message': str(e),
'traceback': traceback.format_exc()
}), 500
@app.route('/api/archive/tournament/<tournament_id>/shots')
def get_tournament_shots(tournament_id):
"""
Get individual shot data for a specific tournament from archive
"""
try:
# Tournament ID might be a database ID or the tournament filename/timestamp
tournament_archives_path = 'tournament_archives'
# Try to find tournament file by various methods
tournament_file = None
# Method 1: Direct filename match
potential_files = [
f"{tournament_archives_path}/tournament_{tournament_id}.json",
f"{tournament_archives_path}/{tournament_id}.json"
]
for file_path in potential_files:
if os.path.exists(file_path):
tournament_file = file_path
break
# Method 2: Search through all tournament files for matching ID
if not tournament_file:
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
for file_path in archive_files:
try:
with open(file_path, 'r') as f:
data = json.load(f)
# Check if tournament ID matches
tournament_data = data.get('tournament', {})
results_data = data.get('results', {})
if (tournament_data.get('created_at') == tournament_id or
results_data.get('tournament_id') == tournament_id or
str(tournament_id) in file_path):
tournament_file = file_path
break
except:
continue
if not tournament_file:
return jsonify({
'status': 'error',
'message': 'Tournament archive file not found'
}), 404
# Load tournament data
with open(tournament_file, 'r') as f:
tournament_data = json.load(f)
# Extract all players' shots
participants = tournament_data.get('results', {}).get('participants', {})
all_shots_data = {}
for participant_id, participant_info in participants.items():
if participant_info.get('completed'):
shots = extract_shots_from_player_data(participant_info)
all_shots_data[participant_info.get('name')] = shots
return jsonify({
'status': 'success',
'tournament_data': {
'tournament_type': determine_tournament_type_from_archive(tournament_data),
'participants': len(participants),
'file_path': tournament_file
},
'shots_by_player': all_shots_data
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
def extract_shots_from_player_data(player_data, tournament_type=None):
"""
Extract individual shot values from player data in your archive format
Now properly handles all tournament formats:
- 4 targets: 5 shots each (shot1-shot5) = 20 total shots
- 20 targets: 2 shots each (shot1-shot2) = 40 total shots
- 40 targets: 2 shots each (shot1-shot2) = 80 total shots
"""
shots = []
targets = player_data.get('targets', {})
if not targets:
return shots
# Sort targets by number to maintain order
target_numbers = sorted([int(k) for k in targets.keys() if k.isdigit()])
# Determine shots per target based on tournament format
if tournament_type and '4' in str(tournament_type):
shots_per_target = 5 # 4 targets format: 5 shots each
else:
# Auto-detect if tournament_type not provided
num_targets = len(target_numbers)
if num_targets <= 6: # Likely 4 targets format (4 targets + maybe some extras)
shots_per_target = 5
else: # 20 or 40 targets format
shots_per_target = 2
for target_num in target_numbers:
target_data = targets[str(target_num)]
# Extract shots for this target
for shot_num in range(1, shots_per_target + 1):
shot_key = f'shot{shot_num}'
shot_value = target_data.get(shot_key)
if shot_value is not None:
shots.append(int(shot_value))
return shots
def count_shots_in_tournament(participant_data, tournament_type=None):
"""
Count total shots fired by a participant in a tournament
Now properly handles all tournament formats:
- 4 targets: 5 shots each = 20 total shots
- 20 targets: 2 shots each = 40 total shots
- 40 targets: 2 shots each = 80 total shots
"""
targets = participant_data.get('targets', {})
shots_count = 0
if not targets:
return shots_count
# Determine shots per target based on tournament format
if tournament_type and '4' in str(tournament_type):
max_shots_per_target = 5 # 4 targets format: 5 shots each
else:
# Auto-detect if tournament_type not provided
target_count = len([k for k in targets.keys() if k.isdigit()])
if target_count <= 6: # Likely 4 targets format
max_shots_per_target = 5
else: # 20 or 40 targets format
max_shots_per_target = 2
for target in targets.values():
for shot_num in range(1, max_shots_per_target + 1):
shot_key = f'shot{shot_num}'
if target.get(shot_key) is not None:
shots_count += 1
return shots_count
def determine_tournament_type_from_archive(tournament_data):
"""
Determine tournament type from your archive data structure
"""
# First check the tournament_type field
tournament_info = tournament_data.get('tournament', {})
results_info = tournament_data.get('results', {})
tournament_type = (tournament_info.get('tournament_type') or
results_info.get('tournament_type'))
if tournament_type:
if '40' in tournament_type:
return '40 Targets'
elif '20' in tournament_type:
return '20 Targets'
elif '4' in tournament_type:
return '4 Targets'
# Fallback: count targets from first completed player
participants = results_info.get('participants', {})
for participant_info in participants.values():
if participant_info.get('completed'):
targets = participant_info.get('targets', {})
target_count = len([k for k in targets.keys() if k.isdigit()])
if target_count >= 30: # 40 targets
return '40 Targets'
elif target_count >= 10: # 20 targets
return '20 Targets'
elif target_count <= 6: # 4 targets (changed from <= 8 to <= 6)
return '4 Targets'
break
# Default fallback
return '20 Targets'
@app.route('/api/debug/player/<int:player_id>')
def debug_player_info(player_id):
"""
Debug endpoint to check player info and archive structure
"""
try:
# Check player exists
players_data = load_players()
player = None
for p in players_data['players']:
if p['id'] == player_id:
player = p
break
# Check archive directory
tournament_archives_path = 'tournament_archives'
archive_exists = os.path.exists(tournament_archives_path)
archive_files = []
if archive_exists:
archive_files = glob.glob(f"{tournament_archives_path}/tournament_*.json")
# Check current working directory
cwd = os.getcwd()
# List contents of current directory
current_dir_contents = os.listdir('.')
return jsonify({
'status': 'success',
'player_found': player is not None,
'player_info': player,
'current_working_directory': cwd,
'current_dir_contents': current_dir_contents,
'archive_directory_exists': archive_exists,
'archive_directory_path': tournament_archives_path,
'archive_files_found': len(archive_files),
'archive_files': [os.path.basename(f) for f in archive_files[:5]] # First 5 files
})
except Exception as e:
import traceback
return jsonify({
'status': 'error',
'message': str(e),
'traceback': traceback.format_exc()
}), 500
# Debug endpoint to see raw tournament data
@app.route('/api/archive/tournament-file/<path:filename>')
def get_tournament_file_debug(filename):
"""
Debug endpoint to see raw tournament file data
"""
try:
tournament_archives_path = 'tournament_archives'
file_path = os.path.join(tournament_archives_path, filename)
if not os.path.exists(file_path):
return jsonify({
'status': 'error',
'message': 'File not found'
}), 404
with open(file_path, 'r') as f:
data = json.load(f)
return jsonify({
'status': 'success',
'file_path': file_path,
'data': data
})
except Exception as e:
return jsonify({
'status': 'error',
'message': str(e)
}), 500
@app.route('/api/archive/tournament-leaders', methods=['GET'])
def api_get_tournament_leaders():
"""API endpoint to get overall tournament leaders by tournament type"""
try:
tournaments = get_archived_tournaments()
# Group data by tournament type
tournament_types = {
'20_targets': {
'name': '20 Targets',
'description': '20 Targets (2 shots each)',
'best_score': {'player_name': None, 'score': 0},
'most_tens': {'player_name': None, 'tens': 0},
'total_tournaments': 0
},
'40_targets': {
'name': '40 Targets',
'description': '40 Targets (2 shots each)',
'best_score': {'player_name': None, 'score': 0},
'most_tens': {'player_name': None, 'tens': 0},
'total_tournaments': 0
},
'4_targets': {
'name': '4 Targets',
'description': '4 Targets (5 shots each)',
'best_score': {'player_name': None, 'score': 0},
'most_tens': {'player_name': None, 'tens': 0},
'total_tournaments': 0
}
}
for tournament in tournaments:
try:
data = load_archive_file(tournament['filepath'])
if not data:
continue
results = data.get('results', {})
participants = results.get('participants', {})
tournament_type = tournament.get('tournament_type', '20_targets')
if tournament_type not in tournament_types or not participants:
continue
tournament_types[tournament_type]['total_tournaments'] += 1
for player_id, participant in participants.items():
if not participant.get('completed'):
continue
player_name = participant.get('name', f'Player {player_id}')
# Check best score for this tournament type
score = participant.get('total_score', 0)
if score > tournament_types[tournament_type]['best_score']['score']:
tournament_types[tournament_type]['best_score'] = {
'player_name': player_name,
'score': score
}
# Count 10s for this player in this tournament
targets = participant.get('targets', {})
tens_count = 0
for target in targets.values():
if target.get('shot1') == 10:
tens_count += 1
if target.get('shot2') == 10:
tens_count += 1
# For 4_targets format, check additional shots
for shot_num in range(3, 6): # shot3, shot4, shot5
if target.get(f'shot{shot_num}') == 10:
tens_count += 1
if tens_count > tournament_types[tournament_type]['most_tens']['tens']:
tournament_types[tournament_type]['most_tens'] = {
'player_name': player_name,
'tens': tens_count
}
except Exception as e:
print(f"Error processing tournament {tournament.get('filepath', 'unknown')}: {e}")
continue
# Convert to list format, only include types with data
tournament_leaders = []
for type_key, type_data in tournament_types.items():
if type_data['total_tournaments'] > 0 and type_data['best_score']['player_name']:
tournament_leaders.append({
'id': type_key,
'name': type_data['name'],
'description': type_data['description'],
'total_tournaments': type_data['total_tournaments'],
'best_score': type_data['best_score'],
'most_tens': type_data['most_tens']
})
return jsonify({'status': 'success', 'tournament_types': tournament_leaders})
except Exception as e:
return jsonify({'status': 'error', 'message': str(e)}), 500
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)