from flask import Flask, render_template, request, redirect, jsonify, session import json import os import random import glob from collections import defaultdict from datetime import datetime import re app = Flask(__name__) app.secret_key = 'your-secret-key-for-sessions' # Change this to a random secret key # Language support SUPPORTED_LANGUAGES = ['sl', 'en'] DEFAULT_LANGUAGE = 'sl' def load_translations(language='sl'): """Load translations for the specified language""" try: locale_file = f'locales/{language}.json' if os.path.exists(locale_file): with open(locale_file, 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: print(f"Error loading translations for {language}: {e}") # Fallback to default language try: with open(f'locales/{DEFAULT_LANGUAGE}.json', 'r', encoding='utf-8') as f: return json.load(f) except Exception as e: print(f"Error loading default translations: {e}") return {} def get_current_language(): """Get current language from session or default""" return session.get('language', DEFAULT_LANGUAGE) def get_translations(): """Get translations for current language""" return load_translations(get_current_language()) 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) def calculate_tens_from_targets(targets): """Calculate the number of 10s from a targets dictionary""" tens_count = 0 if not targets: return 0 for target in targets.values(): if isinstance(target, dict): for shot_key, shot_value in target.items(): if shot_key.startswith('shot') and shot_value == 10: tens_count += 1 return tens_count # 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 5 tournaments (changed from 6)""" 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': 5, # Changed from 6 to 5 '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 4 tournaments (changed from 5) '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 4 tournaments (changed from best 5)""" 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 4 (changed from 5) tournament_scores.sort(reverse=True) best_scores = tournament_scores[:4] if len(tournament_scores) > 4 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(): # Calculate total 10s across all tournaments total_tens = sum(result.get('tens_count', 0) for result in data['tournament_results'] if result.get('participated', False)) 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'], 'total_tens': total_tens }) # Sort by final score (best 5 tournaments) descending, then by total 10s for tiebreaking participants.sort(key=lambda x: (x['final_score'], x['total_tens']), 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 total_tens = 0 for result in data['tournament_results']: if result['participated']: tournament_scores.append(result['score']) completed_tournaments += 1 total_tens += result.get('tens_count', 0) # 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'], 'total_tens': total_tens }) # Sort by current total score (descending), then by total 10s for tiebreaking participants.sort(key=lambda x: (x['current_total'], x['total_tens']), 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(): # Calculate total 10s from completed tournaments total_tens = sum(result.get('tens_count', 0) for result in data['tournament_results'] if result.get('participated', False)) 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'], 'total_tens': total_tens, '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, then by total 10s for tiebreaking participants.sort(key=lambda x: (x['final_score'], x['total_tens']), 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, translations=get_translations(), current_language=get_current_language()) @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, translations=get_translations(), current_language=get_current_language()) @app.route('/archive/player/') 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, translations=get_translations(), current_language=get_current_language()) # 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/') 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(): targets = participant_data.get('targets', {}) tens_count = calculate_tens_from_targets(targets) participants.append({ 'id': player_id, 'name': participant_data['name'], 'total_score': participant_data['total_score'], 'completed': participant_data['completed'], 'targets': targets, 'tens_count': tens_count }) # Sort by score (descending), then by tens (descending) as tiebreaker participants.sort(key=lambda x: (x['total_score'], x['tens_count']), 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/') 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/') 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(): targets = participant_data.get('targets', {}) tens_count = calculate_tens_from_targets(targets) participants.append({ 'id': player_id, 'name': participant_data['name'], 'total_score': participant_data['total_score'], 'completed': participant_data['completed'], 'tens_count': tens_count }) # Sort by score (descending), then by tens (descending) as tiebreaker participants.sort(key=lambda x: (x['total_score'], x['tens_count']), 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/') 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, translations=get_translations(), current_language=get_current_language()) # MOBILE ROUTES @app.route('/mobile') def mobile_menu(): """Mobile redirect to streams""" return redirect('/mobile/streams') @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, translations=get_translations(), current_language=get_current_language()) @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(): targets = data.get('targets', {}) tens_count = calculate_tens_from_targets(targets) participants.append({ 'id': player_id, 'name': data['name'], 'total_score': data['total_score'], 'completed': data['completed'], 'tens_count': tens_count }) # Sort by score (descending), then by tens (descending) as tiebreaker participants.sort(key=lambda x: (x['total_score'], x['tens_count']), 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/') 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, translations=get_translations(), current_language=get_current_language()) @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, translations=get_translations(), current_language=get_current_language()) @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, translations=get_translations(), current_language=get_current_language()) 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, translations=get_translations(), current_language=get_current_language()) # 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(): targets = data.get('targets', {}) tens_count = calculate_tens_from_targets(targets) participants.append({ 'id': player_id, 'name': data['name'], 'total_score': data['total_score'], 'completed': data['completed'], 'tens_count': tens_count }) # Sort by score (descending), then by tens (descending) as tiebreaker participants.sort(key=lambda x: (x['total_score'], x['tens_count']), 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, translations=get_translations(), current_language=get_current_language()) 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//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/', 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/', 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/', 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] # Calculate 10s using existing logic tens_count = 0 targets = participant.get('targets', {}) for target in targets.values(): for shot_key, shot_value in target.items(): if shot_key.startswith('shot') and shot_value == 10: tens_count += 1 # Add tournament result with 10s count league_participant['tournament_results'].append({ 'tournament': tournament_number, 'score': participant['total_score'], 'tens_count': tens_count, '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, 'tens_count': 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//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//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/') 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/') 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 # Language API endpoints @app.route('/api/language', methods=['GET']) def get_language(): """Get current language""" return jsonify({ 'current_language': get_current_language(), 'supported_languages': SUPPORTED_LANGUAGES, 'translations': get_translations() }) @app.route('/api/language/', methods=['POST']) def set_language(language): """Set current language""" if language in SUPPORTED_LANGUAGES: session['language'] = language return jsonify({ 'status': 'success', 'language': language, 'translations': get_translations() }) else: return jsonify({ 'status': 'error', 'message': f'Unsupported language: {language}' }), 400 if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True)