commit 75ac46c23cbc7652ca7ab4e2f57fc10a8a9d865f Author: Bl3kiiie <96978644+Bl3kiiie@users.noreply.github.com> Date: Wed Jul 30 17:53:24 2025 +0200 Add files via upload diff --git a/TV_APP_V2/app.py b/TV_APP_V2/app.py new file mode 100644 index 0000000..11f67ac --- /dev/null +++ b/TV_APP_V2/app.py @@ -0,0 +1,2058 @@ +from flask import Flask, render_template, request, redirect, jsonify +import json +import os +import random +import glob +from collections import defaultdict +from datetime import datetime +import re + +app = Flask(__name__) + +def is_mobile_device(): + """Check if the request is coming from a mobile device""" + user_agent = request.headers.get('User-Agent', '').lower() + mobile_patterns = [ + r'android', r'iphone', r'ipad', r'ipod', r'blackberry', + r'iemobile', r'opera mini', r'mobile', r'tablet' + ] + return any(re.search(pattern, user_agent) for pattern in mobile_patterns) + +# Define streams globally so both routes can access them +STREAMS = [ + {'name': 'Target1', 'url': 'http://192.168.0.134:9081'}, + {'name': 'Target2', 'url': 'http://192.168.0.134:9082'}, + {'name': 'Target3', 'url': 'http://192.168.0.134:9083'}, + {'name': 'Target4', 'url': 'http://192.168.0.134:9084'}, + {'name': 'Target5', 'url': 'http://192.168.0.134:9085'}, + {'name': 'Target6', 'url': 'http://192.168.0.134:9086'}, +] + +# Settings file paths +SETTINGS_FILE = 'camera_settings.json' +PLAYERS_FILE = 'players.json' +TOURNAMENT_FILE = 'tournament_state.json' +RESULTS_FILE = 'tournament_results.json' +LEAGUE_FILE = 'league_state.json' +ARCHIVE_DIR = 'tournament_archives' +LEAGUE_ARCHIVE_DIR = 'league_archives' + +# Default settings +DEFAULT_SETTINGS = { + 'camera_titles': { + '1': 'Camera 1', + '2': 'Camera 2', + '3': 'Camera 3', + '4': 'Camera 4', + '5': 'Camera 5', + '6': 'Camera 6' + }, + 'display_options': { + 'show_titles': True, + 'title_size': 1.1 + } +} + +# Default players structure +DEFAULT_PLAYERS = { + 'players': [ + {'id': 1, 'name': 'Player 1', 'enabled': True}, + {'id': 2, 'name': 'Player 2', 'enabled': True}, + {'id': 3, 'name': 'Player 3', 'enabled': True}, + {'id': 4, 'name': 'Player 4', 'enabled': True}, + {'id': 5, 'name': 'Player 5', 'enabled': True}, + {'id': 6, 'name': 'Player 6', 'enabled': True}, + ] +} + +def load_settings(): + """Load settings from JSON file, create with defaults if not exists""" + try: + if os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE, 'r') as f: + settings = json.load(f) + # Ensure all required keys exist (for backwards compatibility) + for key in DEFAULT_SETTINGS: + if key not in settings: + settings[key] = DEFAULT_SETTINGS[key] + for camera_id in DEFAULT_SETTINGS['camera_titles']: + if camera_id not in settings['camera_titles']: + settings['camera_titles'][camera_id] = DEFAULT_SETTINGS['camera_titles'][camera_id] + return settings + else: + return DEFAULT_SETTINGS.copy() + except (json.JSONDecodeError, IOError): + return DEFAULT_SETTINGS.copy() + +def save_settings(settings): + """Save settings to JSON file""" + try: + with open(SETTINGS_FILE, 'w') as f: + json.dump(settings, f, indent=2) + return True + except IOError: + return False + +def load_players(): + """Load players from JSON file, create with defaults if not exists""" + try: + if os.path.exists(PLAYERS_FILE): + with open(PLAYERS_FILE, 'r') as f: + return json.load(f) + else: + save_players(DEFAULT_PLAYERS) + return DEFAULT_PLAYERS.copy() + except (json.JSONDecodeError, IOError): + return DEFAULT_PLAYERS.copy() + +def save_players(players_data): + """Save players to JSON file""" + try: + with open(PLAYERS_FILE, 'w') as f: + json.dump(players_data, f, indent=2) + return True + except IOError: + return False + +def load_tournament_state(): + """Load tournament state from JSON file""" + try: + if os.path.exists(TOURNAMENT_FILE): + with open(TOURNAMENT_FILE, 'r') as f: + return json.load(f) + else: + return None + except (json.JSONDecodeError, IOError): + return None + +def save_tournament_state(tournament_data): + """Save tournament state to JSON file""" + try: + with open(TOURNAMENT_FILE, 'w') as f: + json.dump(tournament_data, f, indent=2) + return True + except IOError: + return False + +def load_league_state(): + """Load league state from JSON file""" + try: + if os.path.exists(LEAGUE_FILE): + with open(LEAGUE_FILE, 'r') as f: + return json.load(f) + else: + return None + except (json.JSONDecodeError, IOError): + return None + +def save_league_state(league_data): + """Save league state to JSON file""" + try: + with open(LEAGUE_FILE, 'w') as f: + json.dump(league_data, f, indent=2) + return True + except IOError: + return False + +def load_results(): + """Load results from JSON file""" + try: + if os.path.exists(RESULTS_FILE): + with open(RESULTS_FILE, 'r') as f: + return json.load(f) + else: + return None + except (json.JSONDecodeError, IOError): + return None + +def save_results(results_data): + """Save results to JSON file""" + try: + with open(RESULTS_FILE, 'w') as f: + json.dump(results_data, f, indent=2) + return True + except IOError: + return False + +def archive_tournament(tournament_data, results_data): + """Archive completed tournament data""" + try: + # Create archive directory if it doesn't exist + if not os.path.exists(ARCHIVE_DIR): + os.makedirs(ARCHIVE_DIR) + + # Create filename with timestamp + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_filename = f"tournament_{timestamp}.json" + archive_path = os.path.join(ARCHIVE_DIR, archive_filename) + + # Combine tournament and results data + archive_data = { + 'tournament': tournament_data, + 'results': results_data, + 'archived_at': datetime.now().isoformat() + } + + # Save to archive + with open(archive_path, 'w') as f: + json.dump(archive_data, f, indent=2) + + print(f"Tournament archived to: {archive_path}") + return True + except Exception as e: + print(f"Error archiving tournament: {e}") + return False + +def archive_league(league_data): + """Archive completed league data""" + try: + if not os.path.exists(LEAGUE_ARCHIVE_DIR): + os.makedirs(LEAGUE_ARCHIVE_DIR) + + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + archive_filename = f"league_{timestamp}.json" + archive_path = os.path.join(LEAGUE_ARCHIVE_DIR, archive_filename) + + archive_data = { + 'league': league_data, + 'archived_at': datetime.now().isoformat() + } + + with open(archive_path, 'w') as f: + json.dump(archive_data, f, indent=2) + + print(f"League archived to: {archive_path}") + return True + except Exception as e: + print(f"Error archiving league: {e}") + return False + +def create_league(enabled_players, tournament_type): + """Create a new league with 6 tournaments""" + league_id = f"league_{datetime.now().strftime('%Y%m%d_%H%M%S')}" + + league_data = { + 'league_id': league_id, + 'created_at': datetime.now().isoformat(), + 'tournament_type': tournament_type, + 'total_tournaments': 6, + 'current_tournament': 0, # Will be 1 when first tournament starts + 'participants': {}, + 'completed_tournaments': [], + 'league_finished': False + } + + # Initialize participants + for player in enabled_players: + league_data['participants'][str(player['id'])] = { + 'name': player['name'], + 'joker_used': False, + 'tournament_results': [], + 'total_score': 0, + 'final_score': 0, # Best 5 tournaments + 'tournaments_participated': 0 + } + + return league_data + +def create_draft(enabled_players, tournament_type='20_targets', league_tournament_number=None): + """Create draft groups of 6 players and organize rounds""" + if len(enabled_players) < 1: + return None + + # Shuffle players for random grouping + players_copy = enabled_players.copy() + random.shuffle(players_copy) + + # Create groups of up to 6 + groups = [] + for i in range(0, len(players_copy), 6): + group = players_copy[i:i+6] + groups.append(group) + + # Create rounds + rounds = [] + for i, group in enumerate(groups): + rounds.append({ + 'round_number': i + 1, + 'players': group, + 'status': 'pending' if i == 0 else 'waiting' + }) + + tournament_data = { + 'rounds': rounds, + 'created_at': datetime.now().isoformat(), + 'total_players': len(enabled_players), + 'total_rounds': len(rounds), + 'current_round': 1, + 'tournament_type': tournament_type + } + + if league_tournament_number: + tournament_data['league_tournament_number'] = league_tournament_number + + return tournament_data + +def create_results_structure(tournament_data): + """Create results structure for a tournament""" + if not tournament_data: + return None + + tournament_type = tournament_data.get('tournament_type', '20_targets') + num_targets = 40 if tournament_type == '40_targets' else 20 + + results = { + 'tournament_id': tournament_data.get('created_at', datetime.now().isoformat()), + 'tournament_type': tournament_type, + 'participants': {}, + 'tournament_finished': False, + 'created_at': datetime.now().isoformat() + } + + # Add league info if present + if 'league_tournament_number' in tournament_data: + results['league_tournament_number'] = tournament_data['league_tournament_number'] + + # Create structure for each participant + all_players = [] + for round_data in tournament_data['rounds']: + all_players.extend(round_data['players']) + + for player in all_players: + player_id = str(player['id']) + results['participants'][player_id] = { + 'name': player['name'], + 'targets': {str(i): {'shot1': None, 'shot2': None} for i in range(1, num_targets + 1)}, + 'total_score': 0, + 'completed': False + } + + return results + +def calculate_total_score(targets): + """Calculate total score from targets, treating None as 0""" + total = 0 + for target in targets.values(): + shot1 = target.get('shot1') + shot2 = target.get('shot2') + total += (shot1 if shot1 is not None else 0) + total += (shot2 if shot2 is not None else 0) + return total + +def is_participant_completed(targets): + """Check if a participant has completed all targets (all shots entered, including 0s)""" + for target in targets.values(): + shot1 = target.get('shot1') + shot2 = target.get('shot2') + if shot1 is None or shot2 is None: + return False + return True + +def calculate_league_final_scores(league_data): + """Calculate final league scores using best 5 tournaments (joker system)""" + for participant_id, participant in league_data['participants'].items(): + tournament_scores = [] + + # Get all tournament scores where player participated + for result in participant['tournament_results']: + if result['participated']: + tournament_scores.append(result['score']) + + # Sort scores descending and take best 5 (or all if less than 6) + tournament_scores.sort(reverse=True) + best_scores = tournament_scores[:5] if len(tournament_scores) > 5 else tournament_scores + + participant['final_score'] = sum(best_scores) + participant['tournaments_participated'] = len(tournament_scores) + +def get_league_final_rankings(league_data): + """Get final league rankings sorted by final score""" + participants = [] + for player_id, data in league_data['participants'].items(): + participants.append({ + 'id': player_id, + 'name': data['name'], + 'final_score': data['final_score'], + 'total_score': data['total_score'], + 'tournaments_participated': data['tournaments_participated'], + 'joker_used': data['joker_used'], + 'tournament_results': data['tournament_results'] + }) + + # Sort by final score (best 5 tournaments) descending + participants.sort(key=lambda x: x['final_score'], reverse=True) + + # Add rankings + for i, participant in enumerate(participants): + participant['rank'] = i + 1 + + return participants + +def get_current_round_data(): + """Get current round data from tournament""" + tournament_state = load_tournament_state() + if not tournament_state: + return None + + current_round_num = tournament_state.get('current_round', 1) + + # Find the current round + for round_data in tournament_state['rounds']: + if round_data['round_number'] == current_round_num: + return round_data + + return None + +def calculate_current_league_standings(league_data): + """Calculate current league standings during active league""" + participants = [] + for player_id, data in league_data['participants'].items(): + # Calculate current standings based on completed tournaments + tournament_scores = [] + completed_tournaments = 0 + + for result in data['tournament_results']: + if result['participated']: + tournament_scores.append(result['score']) + completed_tournaments += 1 + + # Current score is sum of all completed tournaments + current_total = sum(tournament_scores) + + # For display, show what the final score would be if we took best 5 now + tournament_scores.sort(reverse=True) + projected_final = sum(tournament_scores[:5]) if len(tournament_scores) >= 5 else sum(tournament_scores) + + participants.append({ + 'id': player_id, + 'name': data['name'], + 'current_total': current_total, + 'projected_final': projected_final, + 'tournaments_completed': completed_tournaments, + 'joker_used': data['joker_used'], + 'tournament_results': data['tournament_results'] + }) + + # Sort by current total score (descending) + participants.sort(key=lambda x: x['current_total'], reverse=True) + + # Add rankings + for i, participant in enumerate(participants): + participant['rank'] = i + 1 + + return participants + +def get_league_current_standings(league_state): + """Get current league standings including in-progress tournament""" + if not league_state: + return [] + + # Start with current league participants + participants = [] + for player_id, data in league_state['participants'].items(): + participant = { + 'id': player_id, + 'name': data['name'], + 'total_score': data['total_score'], + 'final_score': data['final_score'], + 'tournaments_participated': data['tournaments_participated'], + 'joker_used': data['joker_used'], + 'tournament_results': data['tournament_results'], + 'current_tournament_score': 0, # Score from current tournament + 'current_tournament_participating': False + } + participants.append(participant) + + # Add current tournament scores if available + current_results = load_results() + if current_results and not current_results.get('tournament_finished', False): + for player_id, result_data in current_results['participants'].items(): + # Find this participant in our list + for participant in participants: + if participant['id'] == player_id: + participant['current_tournament_score'] = result_data['total_score'] + participant['current_tournament_participating'] = True + break + + # Sort by final score (best 5 tournaments) descending + participants.sort(key=lambda x: x['final_score'], reverse=True) + + # Add rankings + for i, participant in enumerate(participants): + participant['rank'] = i + 1 + + return participants + +# Add these functions after the existing helper functions in app.py + +def get_archived_tournaments(): + """Get list of standalone archived tournaments (excluding league tournaments)""" + try: + if not os.path.exists(ARCHIVE_DIR): + return [] + + archives = [] + for file_path in glob.glob(os.path.join(ARCHIVE_DIR, "tournament_*.json")): + try: + with open(file_path, 'r') as f: + data = json.load(f) + + # Extract metadata + filename = os.path.basename(file_path) + archived_at = data.get('archived_at', 'Unknown') + tournament_data = data.get('tournament', {}) + results_data = data.get('results', {}) + + # Skip tournaments that are part of a league + if tournament_data.get('league_tournament_number') or results_data.get('league_tournament_number'): + continue + + archive_info = { + 'filename': filename, + 'filepath': file_path, + 'archived_at': archived_at, + 'created_at': tournament_data.get('created_at', 'Unknown'), + 'tournament_type': tournament_data.get('tournament_type', '20_targets'), + 'total_players': tournament_data.get('total_players', 0), + 'total_rounds': tournament_data.get('total_rounds', 0), + 'tournament_finished': results_data.get('tournament_finished', False), + 'participants_count': len(results_data.get('participants', {})) + } + + archives.append(archive_info) + except (json.JSONDecodeError, IOError) as e: + print(f"Error reading archive {file_path}: {e}") + continue + + # Sort by archived date (newest first) + archives.sort(key=lambda x: x['archived_at'], reverse=True) + return archives + except Exception as e: + print(f"Error getting archived tournaments: {e}") + return [] + +def get_archived_leagues(): + """Get list of all archived leagues""" + try: + if not os.path.exists(LEAGUE_ARCHIVE_DIR): + return [] + + archives = [] + for file_path in glob.glob(os.path.join(LEAGUE_ARCHIVE_DIR, "league_*.json")): + try: + with open(file_path, 'r') as f: + data = json.load(f) + + filename = os.path.basename(file_path) + archived_at = data.get('archived_at', 'Unknown') + league_data = data.get('league', {}) + + archive_info = { + 'filename': filename, + 'filepath': file_path, + 'archived_at': archived_at, + 'created_at': league_data.get('created_at', 'Unknown'), + 'league_id': league_data.get('league_id', 'Unknown'), + 'tournament_type': league_data.get('tournament_type', '20_targets'), + 'total_tournaments': league_data.get('total_tournaments', 6), + 'participants_count': len(league_data.get('participants', {})), + 'league_finished': league_data.get('league_finished', False), + 'completed_tournaments': len(league_data.get('completed_tournaments', [])) + } + + archives.append(archive_info) + except (json.JSONDecodeError, IOError) as e: + print(f"Error reading league archive {file_path}: {e}") + continue + + # Sort by archived date (newest first) + archives.sort(key=lambda x: x['archived_at'], reverse=True) + return archives + except Exception as e: + print(f"Error getting archived leagues: {e}") + return [] + +def load_archive_file(filepath): + """Load a specific archive file""" + try: + with open(filepath, 'r') as f: + return json.load(f) + except (json.JSONDecodeError, IOError) as e: + print(f"Error loading archive file {filepath}: {e}") + return None + +def analyze_player_performance(player_id, archives_data): + """Analyze performance of a specific player across all archives""" + player_stats = { + 'total_tournaments': 0, + 'total_leagues': 0, + 'tournament_scores': [], + 'league_scores': [], + 'best_tournament_score': 0, + 'worst_tournament_score': float('inf'), + 'average_tournament_score': 0, + 'total_shots_fired': 0, + 'performance_trend': [], + 'tournament_history': [], + 'league_history': [] + } + + # Process tournament archives + for archive in archives_data.get('tournaments', []): + try: + data = load_archive_file(archive['filepath']) + if not data: + continue + + results = data.get('results', {}) + participants = results.get('participants', {}) + + if str(player_id) in participants: + participant = participants[str(player_id)] + score = participant.get('total_score', 0) + completed = participant.get('completed', False) + + if completed: + player_stats['total_tournaments'] += 1 + player_stats['tournament_scores'].append(score) + + if score > player_stats['best_tournament_score']: + player_stats['best_tournament_score'] = score + if score < player_stats['worst_tournament_score']: + player_stats['worst_tournament_score'] = score + + # Count shots fired + targets = participant.get('targets', {}) + shots_in_tournament = 0 + for target in targets.values(): + if target.get('shot1') is not None: + shots_in_tournament += 1 + if target.get('shot2') is not None: + shots_in_tournament += 1 + + player_stats['total_shots_fired'] += shots_in_tournament + + # Add to history + player_stats['tournament_history'].append({ + 'date': archive['archived_at'], + 'score': score, + 'tournament_type': archive['tournament_type'], + 'completed': completed, + 'shots_fired': shots_in_tournament + }) + except Exception as e: + print(f"Error analyzing tournament archive: {e}") + continue + + # Process league archives + for archive in archives_data.get('leagues', []): + try: + data = load_archive_file(archive['filepath']) + if not data: + continue + + league = data.get('league', {}) + participants = league.get('participants', {}) + + if str(player_id) in participants: + participant = participants[str(player_id)] + final_score = participant.get('final_score', 0) + total_score = participant.get('total_score', 0) + tournaments_participated = participant.get('tournaments_participated', 0) + + player_stats['total_leagues'] += 1 + player_stats['league_scores'].append(final_score) + + player_stats['league_history'].append({ + 'date': archive['archived_at'], + 'final_score': final_score, + 'total_score': total_score, + 'tournaments_participated': tournaments_participated, + 'joker_used': participant.get('joker_used', False), + 'tournament_results': participant.get('tournament_results', []) + }) + except Exception as e: + print(f"Error analyzing league archive: {e}") + continue + + # Calculate averages and trends + if player_stats['tournament_scores']: + player_stats['average_tournament_score'] = sum(player_stats['tournament_scores']) / len(player_stats['tournament_scores']) + + # Performance trend (simple moving average) + if len(player_stats['tournament_scores']) >= 3: + for i in range(2, len(player_stats['tournament_scores'])): + avg = sum(player_stats['tournament_scores'][i-2:i+1]) / 3 + player_stats['performance_trend'].append(avg) + + if player_stats['worst_tournament_score'] == float('inf'): + player_stats['worst_tournament_score'] = 0 + + # Sort histories by date (newest first) + player_stats['tournament_history'].sort(key=lambda x: x['date'], reverse=True) + player_stats['league_history'].sort(key=lambda x: x['date'], reverse=True) + + return player_stats + +def get_all_players_from_archives(): + """Get all players that appear in any archive""" + all_players = {} + + # Get current players first + current_players = load_players() + for player in current_players['players']: + all_players[str(player['id'])] = { + 'id': player['id'], + 'name': player['name'], + 'current_player': True + } + + # Add players from tournament archives + tournaments = get_archived_tournaments() + for archive in tournaments: + try: + data = load_archive_file(archive['filepath']) + if data and 'results' in data: + participants = data['results'].get('participants', {}) + for player_id, participant in participants.items(): + if player_id not in all_players: + all_players[player_id] = { + 'id': int(player_id), + 'name': participant.get('name', f'Player {player_id}'), + 'current_player': False + } + except Exception as e: + print(f"Error processing tournament archive: {e}") + continue + + # Add players from league archives + leagues = get_archived_leagues() + for archive in leagues: + try: + data = load_archive_file(archive['filepath']) + if data and 'league' in data: + participants = data['league'].get('participants', {}) + for player_id, participant in participants.items(): + if player_id not in all_players: + all_players[player_id] = { + 'id': int(player_id), + 'name': participant.get('name', f'Player {player_id}'), + 'current_player': False + } + except Exception as e: + print(f"Error processing league archive: {e}") + continue + + # Convert to list and sort by name + players_list = list(all_players.values()) + players_list.sort(key=lambda x: x['name']) + + return players_list + +# Add these routes after the existing routes in app.py + +# Add this to your app.py file to integrate the modern archive system + +# Replace your existing archive routes with these updated ones: + +@app.route('/archive') +def archive_index(): + """Archive index page with updated styling""" + if is_mobile_device(): + return redirect('/mobile/archive') + + tournaments = get_archived_tournaments() # Now only returns standalone tournaments + leagues = get_archived_leagues() + + # Calculate overview stats + total_tournaments = len(tournaments) + total_leagues = len(leagues) + + # Get total players from current players list + players_data = load_players() + total_players = len([p for p in players_data['players'] if p['enabled']]) + + # Calculate total competitions (standalone tournaments + completed leagues) + total_matches = total_tournaments + total_leagues + + stats = { + 'total_tournaments': total_tournaments, + 'total_leagues': total_leagues, + 'total_players': total_players, + 'total_matches': total_matches + } + + # Use the new template + return render_template('modern_archive_index.html', + tournaments=tournaments, + leagues=leagues, + stats=stats) + +@app.route('/archive/player-analysis') +def player_analysis(): + """Modern Player analysis page""" + if is_mobile_device(): + return redirect('/mobile/archive/player-analysis') + + all_players = get_all_players_from_archives() + + # Calculate overview stats + total_players = len(all_players) + active_players = len([p for p in all_players if p['current_player']]) + + # Get average tournaments and top score from archives + avg_tournaments = 0 + top_score = 0 + + if all_players: + total_tournament_count = 0 + max_score = 0 + + for player in all_players: + # Get basic stats for each player + archives_data = { + 'tournaments': get_archived_tournaments(), + 'leagues': get_archived_leagues() + } + player_stats = analyze_player_performance(player['id'], archives_data) + total_tournament_count += player_stats['total_tournaments'] + if player_stats['best_tournament_score'] > max_score: + max_score = player_stats['best_tournament_score'] + + avg_tournaments = total_tournament_count // total_players if total_players else 0 + top_score = max_score + + overview_stats = { + 'total_players': total_players, + 'active_players': active_players, + 'avg_tournaments': avg_tournaments, + 'top_score': top_score + } + + return render_template('modern_player_analysis.html', + players=all_players, + overview_stats=overview_stats) + +@app.route('/archive/player/') +def view_player_stats(player_id): + """Modern Player stats page""" + if is_mobile_device(): + return redirect(f'/mobile/archive/player/{player_id}') + + all_players = get_all_players_from_archives() + player_info = next((p for p in all_players if p['id'] == player_id), None) + + if not player_info: + return redirect('/archive/player-analysis') + + # Get archives data + archives_data = { + 'tournaments': get_archived_tournaments(), + 'leagues': get_archived_leagues() + } + + # Analyze player performance + player_stats = analyze_player_performance(player_id, archives_data) + + return render_template('modern_player_stats.html', + player=player_info, + stats=player_stats) + +# Enhanced API endpoints for the modern archive system + +@app.route('/api/archive/stats', methods=['GET']) +def api_get_archive_stats(): + """API endpoint to get archive overview statistics""" + try: + tournaments = get_archived_tournaments() + leagues = get_archived_leagues() + players_data = load_players() + + # Calculate activity over time (last 6 months) + activity_data = { + 'labels': ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'], + 'tournaments': [2, 4, 3, 5, 6, 4], # This should be calculated from actual data + 'leagues': [1, 1, 2, 1, 2, 1] + } + + # Calculate tournament type distribution + type_distribution = {'20_targets': 0, '40_targets': 0} + for tournament in tournaments: + tournament_type = tournament.get('tournament_type', '20_targets') + type_distribution[tournament_type] += 1 + + for league in leagues: + league_type = league.get('tournament_type', '20_targets') + type_distribution[league_type] += 1 + + stats = { + 'overview': { + 'total_tournaments': len(tournaments), + 'total_leagues': len(leagues), + 'total_players': len([p for p in players_data['players'] if p['enabled']]), + 'total_matches': len(tournaments) + sum(l.get('completed_tournaments', 0) for l in leagues) + }, + 'activity_data': activity_data, + 'type_distribution': { + 'labels': ['20 Targets', '40 Targets'], + 'data': [type_distribution['20_targets'], type_distribution['40_targets']] + } + } + + return jsonify({'status': 'success', 'stats': stats}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + +@app.route('/api/archive/player//performance', methods=['GET']) +def api_get_player_performance(player_id): + """API endpoint to get player performance data for charts""" + try: + archives_data = { + 'tournaments': get_archived_tournaments(), + 'leagues': get_archived_leagues() + } + + player_stats = analyze_player_performance(player_id, archives_data) + + # Prepare chart data + performance_data = { + 'trend': { + 'labels': [f'T{i+1}' for i in range(len(player_stats['tournament_scores']))], + 'data': player_stats['tournament_scores'] + }, + 'distribution': { + 'labels': ['0-50', '51-60', '61-70', '71-80', '81-90', '91-100'], + 'data': [0, 0, 0, 0, 0, 0] + } + } + + # Calculate score distribution + for score in player_stats['tournament_scores']: + if score <= 50: + performance_data['distribution']['data'][0] += 1 + elif score <= 60: + performance_data['distribution']['data'][1] += 1 + elif score <= 70: + performance_data['distribution']['data'][2] += 1 + elif score <= 80: + performance_data['distribution']['data'][3] += 1 + elif score <= 90: + performance_data['distribution']['data'][4] += 1 + else: + performance_data['distribution']['data'][5] += 1 + + return jsonify({'status': 'success', 'performance': performance_data}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + +@app.route('/api/archive/players/with-stats', methods=['GET']) +def api_get_players_with_stats(): + """API endpoint to get all players with their basic stats""" + try: + all_players = get_all_players_from_archives() + archives_data = { + 'tournaments': get_archived_tournaments(), + 'leagues': get_archived_leagues() + } + + players_with_stats = [] + for player in all_players: + player_stats = analyze_player_performance(player['id'], archives_data) + + players_with_stats.append({ + 'id': player['id'], + 'name': player['name'], + 'current_player': player['current_player'], + 'stats': { + 'total_tournaments': player_stats['total_tournaments'], + 'total_leagues': player_stats['total_leagues'], + 'best_tournament_score': player_stats['best_tournament_score'], + 'average_tournament_score': player_stats['average_tournament_score'], + 'total_shots_fired': player_stats['total_shots_fired'], + 'performance_trend': player_stats['tournament_scores'][-8:] if len(player_stats['tournament_scores']) >= 8 else player_stats['tournament_scores'] # Last 8 tournaments for mini chart + } + }) + + return jsonify({'status': 'success', 'players': players_with_stats}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 500 + +# Add these routes to handle clicking on archived tournaments/leagues +# These will load the archive data and show it in your existing result page format + +@app.route('/archive/tournament/') +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/') +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(): + 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/') +def mobile_view_archived_league(filename): + """Mobile view of archived league""" + filepath = os.path.join(LEAGUE_ARCHIVE_DIR, filename) + data = load_archive_file(filepath) + + if not data: + return redirect('/mobile/archive') + + league_data = data.get('league', {}) + + # Process league results + calculate_league_final_scores(league_data) + participants = get_league_final_rankings(league_data) + + return render_template('mobile_results.html', + league=league_data, + participants=participants, + show_league_results=True, + show_tournament_results=False, + tournament_active=False, + results=None, + archived=True, + archive_info={ + 'filename': filename, + 'archived_at': data.get('archived_at') + }) +# MAIN ROUTES +@app.route('/') +def index(): + # Redirect mobile users to mobile menu + if is_mobile_device(): + return redirect('/mobile') + + # Desktop users get the regular dashboard + settings = load_settings() + + # Check if tournament is active and get current round players + current_round_data = get_current_round_data() + + # If tournament is active, override camera titles with player names + if current_round_data: + tournament_titles = {} + players = current_round_data['players'] + + for i in range(6): # Always 6 positions + if i < len(players): + tournament_titles[str(i + 1)] = players[i]['name'] + else: + tournament_titles[str(i + 1)] = 'Empty' + + # Create a copy of settings with tournament titles + display_settings = settings.copy() + display_settings['camera_titles'] = tournament_titles + display_settings['tournament_active'] = True + + tournament_state = load_tournament_state() + display_settings['current_round'] = tournament_state.get('current_round', 1) + display_settings['total_rounds'] = tournament_state.get('total_rounds', 1) + + # Add league info if available + league_state = load_league_state() + if league_state: + display_settings['league_active'] = True + display_settings['league_tournament'] = league_state.get('current_tournament', 1) + display_settings['league_total'] = league_state.get('total_tournaments', 6) + else: + display_settings = settings.copy() + display_settings['tournament_active'] = False + display_settings['current_round'] = 1 + display_settings['total_rounds'] = 1 + display_settings['league_active'] = False + + return render_template('index.html', streams=STREAMS, settings=display_settings) + +# MOBILE ROUTES +@app.route('/mobile') +def mobile_menu(): + """Mobile main menu page""" + tournament_state = load_tournament_state() + league_state = load_league_state() + results = load_results() + + tournament_active = tournament_state is not None + league_active = league_state is not None and not league_state.get('league_finished', False) + results_available = results is not None and results.get('tournament_finished', False) + league_results_available = league_state is not None and league_state.get('league_finished', False) + + return render_template('mobile_menu.html', + tournament_active=tournament_active, + league_active=league_active, + tournament_state=tournament_state, + league_state=league_state, + results_available=results_available, + league_results_available=league_results_available) + +@app.route('/mobile/streams') +def mobile_streams(): + """Mobile streams page""" + settings = load_settings() + tournament_state = load_tournament_state() + + # Check if tournament is active + tournament_active = tournament_state is not None + current_round_data = get_current_round_data() if tournament_active else None + + return render_template('mobile_streams.html', + streams=STREAMS, + settings=settings, + tournament_active=tournament_active, + current_round_data=current_round_data, + tournament_state=tournament_state) + +@app.route('/mobile/draft') +def mobile_draft(): + """Mobile tournament draft page""" + tournament_state = load_tournament_state() + league_state = load_league_state() + if not tournament_state: + return redirect('/mobile') + + return render_template('mobile_draft.html', + tournament=tournament_state, + league=league_state) + + +@app.route('/mobile/results') +def mobile_results(): + """Mobile results page""" + league_state = load_league_state() + results = load_results() + + # Priority 1: Show league results if there's an active or finished league + if league_state: + if league_state.get('league_finished', False): + # Show final league results + calculate_league_final_scores(league_state) + participants = get_league_final_rankings(league_state) + + return render_template('mobile_results.html', + league=league_state, + participants=participants, + show_league_results=True, + show_tournament_results=False, + tournament_active=False, + results=None) # No individual tournament results + else: + # Show ongoing league scoreboard + calculate_league_final_scores(league_state) + participants = get_league_current_standings(league_state) + + # Check if there's a current tournament + tournament_state = load_tournament_state() + tournament_active = tournament_state is not None + current_tournament_results = results + + return render_template('mobile_league_results.html', + league=league_state, + participants=participants, + show_league_results=True, + show_tournament_results=False, + tournament_active=tournament_active, + tournament_state=tournament_state, + current_tournament_results=current_tournament_results) + + # Priority 2: Show individual tournament results (standalone tournament) + elif results and results.get('tournament_finished', False): + participants = [] + for player_id, data in results['participants'].items(): + participants.append({ + 'id': player_id, + 'name': data['name'], + 'total_score': data['total_score'], + 'completed': data['completed'] + }) + + # Sort by score (descending) + participants.sort(key=lambda x: x['total_score'], reverse=True) + + # Add rankings + for i, participant in enumerate(participants): + participant['rank'] = i + 1 + + return render_template('mobile_results.html', + results=results, # ← FIXED: Pass as 'results' + participants=participants, + show_league_results=False, + show_tournament_results=True, + tournament_active=False, + league=None, + tournament_type=results.get('tournament_type', '20_targets')) + else: + return redirect('/mobile') + +# DESKTOP ROUTES +@app.route('/fullscreen/') +def fullscreen(camera_id): + # Get the camera stream data + if 1 <= camera_id <= len(STREAMS): + settings = load_settings() + stream = STREAMS[camera_id - 1] + + # Check if tournament is active for title + current_round_data = get_current_round_data() + if current_round_data and camera_id <= len(current_round_data['players']): + camera_title = current_round_data['players'][camera_id - 1]['name'] + else: + camera_title = settings['camera_titles'].get(str(camera_id), f'Camera {camera_id}') + + custom_title = request.args.get('title', camera_title) + + return render_template('fullscreen.html', + stream=stream, + camera_id=camera_id, + title=custom_title, + settings=settings) + else: + return redirect('/') + +@app.route('/tournament') +def tournament(): + """Tournament management page""" + # Check if mobile device + if is_mobile_device(): + tournament_state = load_tournament_state() + league_state = load_league_state() + if tournament_state: + # Mobile users with active tournament go to draft view + return redirect('/mobile/draft') + else: + # Mobile users without tournament go to mobile menu + return redirect('/mobile') + + # Desktop users get full tournament management + players_data = load_players() + tournament_state = load_tournament_state() + league_state = load_league_state() + + return render_template('tournament.html', + players=players_data['players'], + tournament_state=tournament_state, + league_state=league_state) + +@app.route('/tournament/draft') +def tournament_draft(): + """Tournament draft page""" + # Redirect mobile users to mobile draft + if is_mobile_device(): + return redirect('/mobile/draft') + + tournament_state = load_tournament_state() + league_state = load_league_state() + if not tournament_state: + return redirect('/tournament') + + return render_template('draft.html', + tournament=tournament_state, + league=league_state) + +@app.route('/results/calculator') +def results_calculator(): + """Results calculator page (desktop only)""" + if is_mobile_device(): + return redirect('/mobile/streams') + + tournament_state = load_tournament_state() + if not tournament_state: + return redirect('/tournament') + + # Get or create results structure + results = load_results() + if not results: + results = create_results_structure(tournament_state) + save_results(results) + + return render_template('results_calculator.html', + tournament=tournament_state, + results=results) + +@app.route('/results') +def results_display(): + """Results display page""" + # Redirect mobile users to mobile results + if is_mobile_device(): + return redirect('/mobile/results') + + league_state = load_league_state() + results = load_results() + + # Priority 1: Show league results if there's an active or finished league + if league_state: + if league_state.get('league_finished', False): + # Show final league results + calculate_league_final_scores(league_state) + participants = get_league_final_rankings(league_state) + + return render_template('league_scoreboard_display.html', + league=league_state, + participants=participants) + else: + # Show ongoing league scoreboard + calculate_league_final_scores(league_state) # Calculate current standings + participants = get_league_current_standings(league_state) # Use the helper function + + # Add tournament information + tournament_state = load_tournament_state() + current_tournament_results = results + + return render_template('league_scoreboard_display.html', + league=league_state, + participants=participants, + tournament_state=tournament_state, + current_tournament_results=current_tournament_results) + + # Priority 2: Show individual tournament results (standalone tournament) + elif results and results.get('tournament_finished', False): + participants = [] + for player_id, data in results['participants'].items(): + participants.append({ + 'id': player_id, + 'name': data['name'], + 'total_score': data['total_score'], + 'completed': data['completed'] + }) + + # Sort by score (descending) + participants.sort(key=lambda x: x['total_score'], reverse=True) + + # Add rankings + for i, participant in enumerate(participants): + participant['rank'] = i + 1 + + return render_template('results_display.html', + results=results, + participants=participants) + else: + return redirect('/tournament') + +# API Routes +@app.route('/api/settings', methods=['GET']) +def get_settings(): + """API endpoint to get current settings""" + settings = load_settings() + return jsonify(settings) + +@app.route('/api/settings', methods=['POST']) +def update_settings(): + """API endpoint to update settings""" + try: + new_settings = request.get_json() + + # Load current settings + current_settings = load_settings() + + # Update only provided fields + if 'camera_titles' in new_settings: + current_settings['camera_titles'].update(new_settings['camera_titles']) + + if 'display_options' in new_settings: + current_settings['display_options'].update(new_settings['display_options']) + + # Save updated settings + if save_settings(current_settings): + return jsonify({'status': 'success', 'settings': current_settings}) + else: + return jsonify({'status': 'error', 'message': 'Failed to save settings'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/players', methods=['GET']) +def get_players(): + """API endpoint to get all players""" + players_data = load_players() + return jsonify(players_data) + +@app.route('/api/players', methods=['POST']) +def update_players(): + """API endpoint to update players""" + try: + players_data = request.get_json() + + if save_players(players_data): + return jsonify({'status': 'success', 'players': players_data}) + else: + return jsonify({'status': 'error', 'message': 'Failed to save players'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/players/add', methods=['POST']) +def add_player(): + """API endpoint to add a new player""" + try: + data = request.get_json() + name = data.get('name', '').strip() + + if not name: + return jsonify({'status': 'error', 'message': 'Player name is required'}), 400 + + players_data = load_players() + + # Find next available ID + existing_ids = [p['id'] for p in players_data['players']] + new_id = max(existing_ids) + 1 if existing_ids else 1 + + # Add new player + new_player = { + 'id': new_id, + 'name': name, + 'enabled': True + } + + players_data['players'].append(new_player) + + if save_players(players_data): + return jsonify({'status': 'success', 'player': new_player}) + else: + return jsonify({'status': 'error', 'message': 'Failed to save player'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/players//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']: + return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400 + + players_data = load_players() + enabled_players = [p for p in players_data['players'] if p['enabled']] + + if len(enabled_players) < 1: + return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400 + + league_data = create_league(enabled_players, tournament_type) + + if save_league_state(league_data): + return jsonify({'status': 'success', 'league': league_data}) + else: + return jsonify({'status': 'error', 'message': 'Failed to save league'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/league/tournament/start', methods=['POST']) +def start_league_tournament(): + """API endpoint to start next tournament in league""" + try: + league_state = load_league_state() + if not league_state: + return jsonify({'status': 'error', 'message': 'No active league'}), 400 + + if league_state.get('league_finished', False): + return jsonify({'status': 'error', 'message': 'League already finished'}), 400 + + current_tournament = league_state.get('current_tournament', 0) + if current_tournament >= league_state['total_tournaments']: + return jsonify({'status': 'error', 'message': 'All tournaments completed'}), 400 + + # Get players for this tournament (excluding joker users for this round) + data = request.get_json() or {} + joker_players = data.get('joker_players', []) # List of player IDs using joker + + # Update joker usage in league + for player_id in joker_players: + player_id_str = str(player_id) + if player_id_str in league_state['participants']: + if league_state['participants'][player_id_str]['joker_used']: + return jsonify({'status': 'error', 'message': f'Player {player_id} already used joker'}), 400 + league_state['participants'][player_id_str]['joker_used'] = True + + # Create list of participating players (not using joker) + participating_players = [] + for player_id, participant in league_state['participants'].items(): + if int(player_id) not in joker_players: + participating_players.append({ + 'id': int(player_id), + 'name': participant['name'] + }) + + if len(participating_players) < 1: + return jsonify({'status': 'error', 'message': 'Need at least 1 participating player'}), 400 + + # Start next tournament + league_state['current_tournament'] = current_tournament + 1 + tournament_data = create_draft( + participating_players, + league_state['tournament_type'], + league_state['current_tournament'] + ) + + # Save states + save_league_state(league_state) + + if save_tournament_state(tournament_data): + # Create results structure + results = create_results_structure(tournament_data) + save_results(results) + + return jsonify({ + 'status': 'success', + 'tournament': tournament_data, + 'league': league_state + }) + else: + return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/league/reset', methods=['POST']) +def reset_league(): + """API endpoint to reset/clear league""" + try: + # Archive current league if it exists + league_state = load_league_state() + if league_state: + archive_league(league_state) + + # Remove league, tournament, and results files + for file_path in [LEAGUE_FILE, TOURNAMENT_FILE, RESULTS_FILE]: + if os.path.exists(file_path): + os.remove(file_path) + + return jsonify({'status': 'success'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +# TOURNAMENT API ROUTES (Updated) +@app.route('/api/tournament/start', methods=['POST']) +def start_tournament(): + """API endpoint to start a standalone tournament""" + try: + data = request.get_json() or {} + tournament_type = data.get('tournament_type', '20_targets') + + if tournament_type not in ['20_targets', '40_targets']: + return jsonify({'status': 'error', 'message': 'Invalid tournament type'}), 400 + + players_data = load_players() + enabled_players = [p for p in players_data['players'] if p['enabled']] + + if len(enabled_players) < 1: + return jsonify({'status': 'error', 'message': 'Need at least 1 enabled player'}), 400 + + tournament_data = create_draft(enabled_players, tournament_type) + + if save_tournament_state(tournament_data): + # Create results structure when tournament starts + results = create_results_structure(tournament_data) + save_results(results) + + return jsonify({'status': 'success', 'tournament': tournament_data}) + else: + return jsonify({'status': 'error', 'message': 'Failed to save tournament'}), 500 + + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/tournament/round/', 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() + + league_finished = False # Track if league finished + + # Update league state if this is a league tournament + if league_state and 'league_tournament_number' in results: + tournament_number = results['league_tournament_number'] + + # Update league with tournament results + for player_id, participant in results['participants'].items(): + if player_id in league_state['participants']: + league_participant = league_state['participants'][player_id] + + # Add tournament result + league_participant['tournament_results'].append({ + 'tournament': tournament_number, + 'score': participant['total_score'], + 'participated': True + }) + + # Update total score + league_participant['total_score'] += participant['total_score'] + + # Add results for players who used joker (didn't participate) + for player_id, participant in league_state['participants'].items(): + if player_id not in results['participants']: + # This player used joker + participant['tournament_results'].append({ + 'tournament': tournament_number, + 'score': 0, + 'participated': False, + 'joker': True + }) + + # Add to completed tournaments + league_state['completed_tournaments'].append({ + 'tournament_number': tournament_number, + 'finished_at': datetime.now().isoformat(), + 'results_summary': { + 'participants': len(results['participants']), + 'total_shots': len(results['participants']) * (40 if results.get('tournament_type') == '40_targets' else 20) * 2 + } + }) + + # Check if league is finished + if tournament_number >= league_state['total_tournaments']: + league_state['league_finished'] = True + league_state['finished_at'] = datetime.now().isoformat() + + # Calculate final scores + calculate_league_final_scores(league_state) + + league_finished = True + print("League finished!") + + save_league_state(league_state) + + # Archive the tournament (only if it's NOT part of a league) + archive_success = False + if not league_state: # Only archive standalone tournaments + archive_success = archive_tournament(tournament_state, results) + print(f"Standalone tournament archived: {archive_success}") + else: + print("League tournament - not archiving individual tournament") + + # Archive the league if it just finished + league_archive_success = False + if league_finished and league_state: + league_archive_success = archive_league(league_state) + print(f"League archived: {league_archive_success}") + + # Save final results + if save_results(results): + # End the tournament by removing tournament state + if os.path.exists(TOURNAMENT_FILE): + os.remove(TOURNAMENT_FILE) + + # If league finished, also remove league state file + if league_finished and os.path.exists(LEAGUE_FILE): + os.remove(LEAGUE_FILE) + + response_data = { + 'status': 'success', + 'results': results, + 'archived': archive_success, + 'league_archived': league_archive_success if league_finished else None + } + + if league_state: + response_data['league'] = league_state + response_data['league_finished'] = league_state.get('league_finished', False) + + return jsonify(response_data) + else: + return jsonify({'status': 'error', 'message': 'Failed to save results'}), 500 + + except Exception as e: + print(f"Error finishing tournament: {e}") + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/results', methods=['GET']) +def get_results(): + """API endpoint to get current results""" + results = load_results() + if results: + return jsonify(results) + else: + return jsonify({'status': 'error', 'message': 'No results found'}), 404 + +@app.route('/api/league', methods=['GET']) +def get_league(): + """API endpoint to get current league state""" + league_state = load_league_state() + if league_state: + return jsonify(league_state) + else: + return jsonify({'status': 'error', 'message': 'No league found'}), 404 + +# Add this route to your Flask app (around line 850, with the other mobile routes) + +@app.route('/mobile/remote') +def mobile_remote(): + """Mobile remote control page""" + # This page doesn't redirect mobile users - it's specifically for mobile control + tournament_state = load_tournament_state() + league_state = load_league_state() + results = load_results() + + return render_template('mobile_remote.html', + tournament_state=tournament_state, + league_state=league_state, + results=results) + +# Add these API endpoints for the remote control functionality + +@app.route('/api/tournament', methods=['GET']) +def get_tournament(): + """API endpoint to get current tournament state""" + tournament_state = load_tournament_state() + if tournament_state: + return jsonify(tournament_state) + else: + return jsonify({'status': 'error', 'message': 'No tournament found'}), 404 + +@app.route('/api/remote/refresh_dashboard', methods=['POST']) +def refresh_dashboard(): + """API endpoint to trigger dashboard refresh""" + try: + # You could add logic here to trigger refresh on connected clients + # For now, just return success + return jsonify({'status': 'success', 'message': 'Refresh signal sent'}) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/remote/system_info', methods=['GET']) +def get_system_info(): + """API endpoint to get comprehensive system information""" + try: + tournament_state = load_tournament_state() + league_state = load_league_state() + results = load_results() + players_data = load_players() + settings = load_settings() + + system_info = { + 'timestamp': datetime.now().isoformat(), + 'tournament': { + 'active': tournament_state is not None, + 'data': tournament_state + }, + 'league': { + 'active': league_state is not None and not league_state.get('league_finished', False), + 'finished': league_state is not None and league_state.get('league_finished', False), + 'data': league_state + }, + 'results': { + 'available': results is not None, + 'finished': results is not None and results.get('tournament_finished', False), + 'data': results + }, + 'players': { + 'total': len(players_data['players']) if players_data else 0, + 'enabled': len([p for p in players_data['players'] if p['enabled']]) if players_data else 0 + }, + 'settings': settings + } + + return jsonify(system_info) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/remote/emergency_reset', methods=['POST']) +def emergency_reset(): + """API endpoint for emergency system reset""" + try: + # Archive current states before reset + tournament_state = load_tournament_state() + league_state = load_league_state() + results = load_results() + + if tournament_state and results: + archive_tournament(tournament_state, results) + + if league_state: + archive_league(league_state) + + # Remove all state files + for file_path in [LEAGUE_FILE, TOURNAMENT_FILE, RESULTS_FILE]: + if os.path.exists(file_path): + os.remove(file_path) + + return jsonify({ + 'status': 'success', + 'message': 'Emergency reset completed', + 'archived': True + }) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +@app.route('/api/remote/camera_titles', methods=['GET']) +def get_camera_titles(): + """API endpoint to get camera titles""" + try: + settings = load_settings() + current_round_data = get_current_round_data() + + titles = {} + + if current_round_data: + # Tournament active - use player names + players = current_round_data['players'] + for i in range(6): + if i < len(players): + titles[str(i + 1)] = players[i]['name'] + else: + titles[str(i + 1)] = 'Empty' + else: + # No tournament - use configured titles + titles = settings['camera_titles'] + + return jsonify({ + 'status': 'success', + 'titles': titles, + 'tournament_active': current_round_data is not None + }) + except Exception as e: + return jsonify({'status': 'error', 'message': str(e)}), 400 + +if __name__ == '__main__': + app.run(host='0.0.0.0', port=5000, debug=True) \ No newline at end of file diff --git a/TV_APP_V2/camera_settings.json b/TV_APP_V2/camera_settings.json new file mode 100644 index 0000000..77f9c86 --- /dev/null +++ b/TV_APP_V2/camera_settings.json @@ -0,0 +1,14 @@ +{ + "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": 0.8 + } +} \ No newline at end of file diff --git a/TV_APP_V2/players.json b/TV_APP_V2/players.json new file mode 100644 index 0000000..4ba1638 --- /dev/null +++ b/TV_APP_V2/players.json @@ -0,0 +1,224 @@ +{ + "players": [ + { + "id": 1, + "name": "Domen Pleterski", + "enabled": true + }, + { + "id": 2, + "name": "Nik Pleterski", + "enabled": true + }, + { + "id": 3, + "name": "Ivan Tandler", + "enabled": true + }, + { + "id": 4, + "name": "Mateja Pleterski", + "enabled": true + }, + { + "id": 5, + "name": "Jo\u017ee Verhnjak", + "enabled": false + }, + { + "id": 6, + "name": "Mateja Senica", + "enabled": false + }, + { + "id": 7, + "name": "Branko Poker\u017enik", + "enabled": false + }, + { + "id": 8, + "name": "Franc \u017digart", + "enabled": false + }, + { + "id": 9, + "name": "Janez Bo\u017ei\u010d", + "enabled": false + }, + { + "id": 10, + "name": "Mitja \u010ceh", + "enabled": false + }, + { + "id": 11, + "name": "Rado Kefer", + "enabled": false + }, + { + "id": 12, + "name": "Matej Kvasnik", + "enabled": false + }, + { + "id": 13, + "name": "Angelca Mrak", + "enabled": false + }, + { + "id": 14, + "name": "Karli Proje", + "enabled": false + }, + { + "id": 15, + "name": "Jan Pleterski", + "enabled": false + }, + { + "id": 16, + "name": "Silvo Poro\u010dnik", + "enabled": false + }, + { + "id": 17, + "name": "Du\u0161an Onuk", + "enabled": false + }, + { + "id": 18, + "name": "Matja\u017e Pleterski", + "enabled": false + }, + { + "id": 19, + "name": "Franc Rizmal", + "enabled": false + }, + { + "id": 20, + "name": "Jo\u017ee Preglav", + "enabled": false + }, + { + "id": 21, + "name": "Marko Blimen", + "enabled": false + }, + { + "id": 22, + "name": "Doris Fesel", + "enabled": false + }, + { + "id": 23, + "name": "Robi Krautberger", + "enabled": false + }, + { + "id": 24, + "name": "Jo\u017ee Verdinek", + "enabled": false + }, + { + "id": 25, + "name": "Andrej Herman", + "enabled": false + }, + { + "id": 26, + "name": "Jakob Herman", + "enabled": false + }, + { + "id": 27, + "name": "Janez Mrak", + "enabled": false + }, + { + "id": 28, + "name": "An\u017ee Kolar", + "enabled": false + }, + { + "id": 29, + "name": "Alen Kolar", + "enabled": false + }, + { + "id": 30, + "name": "Maja Hirtl", + "enabled": false + }, + { + "id": 31, + "name": "Dejan Ku\u010dnik", + "enabled": false + }, + { + "id": 32, + "name": "David Strni\u0161a", + "enabled": false + }, + { + "id": 33, + "name": "Namir Uzunovi\u0107", + "enabled": false + }, + { + "id": 34, + "name": "Jo\u017ee Planin\u0161ec", + "enabled": false + }, + { + "id": 35, + "name": "Vanja Kolar", + "enabled": false + }, + { + "id": 36, + "name": "Klara Wankmuller", + "enabled": false + }, + { + "id": 37, + "name": "Milan Stramec", + "enabled": false + }, + { + "id": 38, + "name": "Bojan Sudar", + "enabled": false + }, + { + "id": 39, + "name": "Tia Sudar", + "enabled": false + }, + { + "id": 40, + "name": "Jaka Cvar", + "enabled": false + }, + { + "id": 41, + "name": "Tadej \u0160truc", + "enabled": false + }, + { + "id": 42, + "name": "Jure Glaser", + "enabled": false + }, + { + "id": 43, + "name": "Marko Pokr\u017enik", + "enabled": false + }, + { + "id": 44, + "name": "test", + "enabled": false + } + ] +} \ No newline at end of file diff --git a/TV_APP_V2/static/logo.png b/TV_APP_V2/static/logo.png new file mode 100644 index 0000000..da243c2 Binary files /dev/null and b/TV_APP_V2/static/logo.png differ diff --git a/TV_APP_V2/static/setting.png b/TV_APP_V2/static/setting.png new file mode 100644 index 0000000..bfbf326 Binary files /dev/null and b/TV_APP_V2/static/setting.png differ diff --git a/TV_APP_V2/templates/draft.html b/TV_APP_V2/templates/draft.html new file mode 100644 index 0000000..8d30a46 --- /dev/null +++ b/TV_APP_V2/templates/draft.html @@ -0,0 +1,547 @@ + + + + + Tournament Draft + + + + + + +
+ {% if tournament %} +
+
🎯 Shooting Tournament
+
+ {{ tournament.total_players }} players • {{ tournament.total_rounds }} rounds + {% if tournament.current_round %} + • Currently on Round {{ tournament.current_round }} + {% endif %} +
+ + {% if tournament.current_round %} +
+ + Round {{ tournament.current_round }} of {{ tournament.total_rounds }} + +
+ {% endif %} +
+ +
+ {% for round in tournament.rounds %} + {% set is_current = tournament.current_round == round.round_number %} + {% set is_completed = tournament.current_round > round.round_number %} +
+
+
Round {{ round.round_number }}
+ {% if is_current %} +
Current
+ {% elif is_completed %} +
Done
+ {% else %} +
Wait
+ {% endif %} +
+ +
+ {% for position in range(1, 7) %} + {% set player = round.players[position-1] if position <= round.players|length else none %} +
+
{{ position }}
+ {% if player %} +
{{ player.name }}
+
ID: {{ player.id }}
+ {% else %} +
Empty
+ {% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
+ + {% else %} +
+

No Active Tournament

+

Go to Tournament Management to set up players and start a tournament.

+ 🏆 Set Up Tournament +
+ {% endif %} +
+ + +
+ 🔄 Updating... +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/fullscreen.html b/TV_APP_V2/templates/fullscreen.html new file mode 100644 index 0000000..4afde6c --- /dev/null +++ b/TV_APP_V2/templates/fullscreen.html @@ -0,0 +1,766 @@ + + + + + {{ title }} - Fullscreen + + + + +
+ + +
+ +
+
Loading stream...
+
+ {{ title }} +
+ +
+ +
1.0x
+ + +
+
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/index.html b/TV_APP_V2/templates/index.html new file mode 100644 index 0000000..ef1e364 --- /dev/null +++ b/TV_APP_V2/templates/index.html @@ -0,0 +1,875 @@ + + + + + Camera Dashboard + + + + + + +
+ +
+
+

Camera Settings

+ +
+ +
+ {% if settings.tournament_active %} +
+

🏆 Active Tournament

+ +
+

Current Round: {{ settings.current_round }} of {{ settings.total_rounds }}

+

Camera cards show current round players

+
+ + +
+ {% endif %} + +
+ {% if not settings.tournament_active %} +

Navigation

+ 🏆 Tournament Mode +

Data & Analysis

+ 👤 Player Analysis + 📚 View Archive + {% endif %} +
+ + {% if not settings.tournament_active %} +
+

Display Options

+ +
+ +
+ +
+ + +
+
+
+

Camera Titles

+ {% for i in range(1, 7) %} +
+ + +
+ {% endfor %} +
+ {% endif %} +
+
+ +
+ {% for i in range(1, 7) %} +
+
{{ settings.camera_titles[i|string] }}
+
+ Camera Stream {{ i }} +
+
+ {% endfor %} +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/league_results_display.html b/TV_APP_V2/templates/league_results_display.html new file mode 100644 index 0000000..3c9cda2 --- /dev/null +++ b/TV_APP_V2/templates/league_results_display.html @@ -0,0 +1,1112 @@ + + + + + {% if league.league_finished %}League Final Results{% else %}League Scoreboard{% endif %} + + + + + + +
+ +
+ +
+ +
+ {% if league.league_finished %}League Championship{% else %}League Scoreboard{% endif %} +
+
+ {% if league.league_finished %} + Final Rankings - Best 5 of 6 Tournaments + {% else %} + Tournament {{ league.current_tournament }} of {{ league.total_tournaments }} - Live Updates + {% endif %} +
+
+
+ {{ participants|length }} + Participants +
+
+ {{ league.current_tournament if not league.league_finished else league.total_tournaments }} + {% if league.league_finished %}Total Tournaments{% else %}Current Tournament{% endif %} +
+
+ + {% if league.tournament_type == '40_targets' %}40{% else %}20{% endif %} + + Targets +
+
+ {% if participants and participants|length > 0 %}{{ participants[0].final_score if league.league_finished else participants[0].current_total }}{% else %}0{% endif %} + {% if league.league_finished %}Final Score{% else %}Leading Score{% endif %} +
+
+
+ + {% if participants and participants|length >= 3 %} + +
+
+ {% if league.league_finished %}🏆 Final Champions{% else %}🥇 Current Leaders{% endif %} +
+
+ {% for i in range(3) %} + {% set participant = participants[i] %} +
+
+
{{ participant.rank }}
+
+ {% if participant.rank == 1 %}st + {% elif participant.rank == 2 %}nd + {% elif participant.rank == 3 %}rd + {% else %}th + {% endif %} +
+
+ {% if participant.rank == 1 %}🥇 + {% elif participant.rank == 2 %}🥈 + {% elif participant.rank == 3 %}🥉 + {% endif %} +
+
+ +
+
{{ participant.name }}
+
+
ID: {{ participant.id }}
+ {% if participant.joker_used %} +
🃏 Joker Used
+ {% endif %} + {% if not league.league_finished and participant.current_tournament_participating %} +
🎯 Playing Now
+ {% endif %} +
+
+ +
+
{{ participant.final_score if league.league_finished else participant.current_total }}
+ {% if not league.league_finished %} +
+ {% if participant.current_tournament_participating %} + Current: +{{ participant.current_tournament_score }} + {% else %} + Final: {{ participant.final_score }} + {% endif %} +
+ {% else %} +
Total: {{ participant.total_score }}
+ {% endif %} +
{% if league.league_finished %}Final Score{% else %}Current Score{% endif %}
+
+
+ {% endfor %} +
+
+ {% endif %} +
+ + +
+
+

+ {% if league.league_finished %} + 📊 Complete Tournament Breakdown & Final Calculation + {% else %} + 📊 League Standings - Tournament {{ league.current_tournament }} of {{ league.total_tournaments }} + {% endif %} +

+
+ +
+ + + + + + + + {% if not league.league_finished %} + + {% endif %} + + + + + + + + + + + + {% for participant in participants %} + + + + + + {% for tournament_num in range(1, 7) %} + + {% endfor %} + + + + {% if not league.league_finished %} + + {% endif %} + + {% endfor %} + +
RankParticipantIndividual Tournament Scores + {% if league.league_finished %} + Final Score
(Best 5) + {% else %} + Current
(Best 5) + {% endif %} +
Live
T{{ league.current_tournament }}
T1T2T3T4T5T6
+ {{ participant.rank }} + {% if participant.rank == 1 %}🏆 + {% elif participant.rank == 2 %}🥈 + {% elif participant.rank == 3 %}🥉 + {% endif %} + +
{{ participant.name }}
+
{{ participant.id }}
+
+ {% set found_result = false %} + {% for result in participant.tournament_results %} + {% if result.tournament == tournament_num %} + {% set found_result = true %} + {% if result.get('joker') or not result.participated %} + 🃏 + {% else %} + {% set all_scores = [] %} + {% for r in participant.tournament_results if r.participated %} + {% set _ = all_scores.append(r.score) %} + {% endfor %} + {% set sorted_scores = all_scores|sort(reverse=true) %} + {% set excluded_score = sorted_scores[-1] if sorted_scores|length > 5 else none %} + + {% if all_scores|length > 5 and result.score == excluded_score %} + {{ result.score }} + {% else %} + {{ result.score }} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {% if not found_result %} + - + {% endif %} + + {% if league.league_finished %} + {{ participant.final_score }} + {% else %} + {{ participant.current_total }} + {% endif %} + + {% if participant.current_tournament_participating %} + {{ participant.current_tournament_score }} + {% else %} + - + {% endif %} +
+
+ +
+
📖 Scoring Legend:
+
+
+ + Counted Score +
+ {% if league.league_finished %} +
+ + Excluded (Worst) +
+ {% endif %} +
+ + Joker Used +
+
+ + {% if league.league_finished %}Final Score (Best 5){% else %}Current Score{% endif %} +
+ {% if not league.league_finished %} +
+ + Live Tournament +
+ {% endif %} +
+
+ + +
+
+ + +
+ 🔄 Updating... +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/league_scoreboard_display.html b/TV_APP_V2/templates/league_scoreboard_display.html new file mode 100644 index 0000000..57d61f4 --- /dev/null +++ b/TV_APP_V2/templates/league_scoreboard_display.html @@ -0,0 +1,1156 @@ + + + + + League Results + + + + + + +
+ +
+ +
+ +
League Championship
+
Final Rankings - Best 5 of 6 Tournaments
+
+
+ {{ participants|length }} + Participants +
+
+ {{ league.total_tournaments }} + Tournaments +
+
+ + {% if league.tournament_type == '40_targets' %}40{% else %}20{% endif %} + + Targets +
+
+ {% if participants and participants|length > 0 %}{{ participants[0].final_score }}{% else %}0{% endif %} + Highest Score +
+
+
+ + {% if participants and participants|length >= 3 %} + +
+
🏆 League Champions
+
+ {% for i in range(3) %} + {% set participant = participants[i] %} +
+
+
{{ participant.rank }}
+
+ {% if participant.rank == 1 %}st + {% elif participant.rank == 2 %}nd + {% elif participant.rank == 3 %}rd + {% else %}th + {% endif %} +
+
+ {% if participant.rank == 1 %}🥇 + {% elif participant.rank == 2 %}🥈 + {% elif participant.rank == 3 %}🥉 + {% endif %} +
+
+ +
+
{{ participant.name }}
+
+
ID: {{ participant.id }}
+ {% if participant.joker_used %} +
🃏 Joker Used
+ {% endif %} +
+
+ +
+
{{ participant.final_score }}
+
Total: {{ participant.total_score }}
+
Final Score
+
+
+ {% endfor %} +
+
+ {% endif %} +
+ + +
+
+

📊 Complete Tournament Breakdown & Final Calculation

+
+ +
+ + + + + + + + + + + + + + + + + + + {% for participant in participants %} + + + + + + {% for tournament_num in range(1, 7) %} + + {% endfor %} + + + + {% endfor %} + +
RankParticipantIndividual Tournament ScoresFinal Score
(Best 5)
T1T2T3T4T5T6
+ {{ participant.rank }} + {% if participant.rank == 1 %}🏆 + {% elif participant.rank == 2 %}🥈 + {% elif participant.rank == 3 %}🥉 + {% endif %} + +
{{ participant.name }}
+
+ {% set found_result = false %} + {% for result in participant.tournament_results %} + {% if result.tournament == tournament_num %} + {% set found_result = true %} + {% if result.get('joker') or not result.participated %} + 🃏 + {% else %} + {% set all_scores = [] %} + {% for r in participant.tournament_results if r.participated %} + {% set _ = all_scores.append(r.score) %} + {% endfor %} + {% set sorted_scores = all_scores|sort(reverse=true) %} + {% set excluded_score = sorted_scores[-1] if sorted_scores|length > 5 else none %} + + {% if all_scores|length > 5 and result.score == excluded_score %} + {{ result.score }} + {% else %} + {{ result.score }} + {% endif %} + {% endif %} + {% endif %} + {% endfor %} + {{ participant.final_score }}
+
+ +
+
📖 Scoring Legend:
+
+
+ + Counted Score +
+
+ + Excluded (Worst) +
+
+ + Joker Used +
+
+ + Final Score (Best 5) +
+
+
+ + +
+
+ + +
+ 🔄 Updating... +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/mobile_draft.html b/TV_APP_V2/templates/mobile_draft.html new file mode 100644 index 0000000..663d0dc --- /dev/null +++ b/TV_APP_V2/templates/mobile_draft.html @@ -0,0 +1,769 @@ + + + + + 📱 Tournament Draft + + + + +
+ + +
+ +
+ {% if tournament %} +
+
🎯 Shooting Tournament
+
+ {{ tournament.total_players }} players • {{ tournament.total_rounds }} rounds + {% if tournament.current_round %} + • Currently on Round {{ tournament.current_round }} + {% endif %} +
+
+ +
+ {% for round in tournament.rounds %} + {% set is_current = tournament.current_round == round.round_number %} + {% set is_completed = tournament.current_round > round.round_number %} +
+
+
Round {{ round.round_number }}
+ {% if is_current %} +
Current
+ {% elif is_completed %} +
Completed
+ {% else %} +
Waiting
+ {% endif %} +
+ +
+ {% for position in range(1, 7) %} + {% set player = round.players[position-1] if position <= round.players|length else none %} +
+
{{ position }}
+ {% if player %} +
{{ player.name }}
+
ID: {{ player.id }}
+ {% else %} +
Empty
+ {% endif %} +
+ {% endfor %} +
+
+ {% endfor %} +
+ + +
+
📊 Tournament Summary
+
+
+ Total Players: + {{ tournament.total_players }} +
+
+ Total Rounds: + {{ tournament.total_rounds }} +
+
+ Current Round: + {{ tournament.current_round or 'Not set' }} +
+
+ Updates: + On Round Change +
+
+ Created: + {{ tournament.created_at[:10] if tournament.created_at else 'Unknown' }} +
+
+ Status: + Active +
+
+
+ {% else %} + +
+

📋 No Active Tournament

+

No tournament is currently running. Check the Results tab for completed tournaments or use the dashboard to start a new tournament.

+
+ {% endif %} +
+ + +
+ 🆕 New Round Starting... +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/mobile_league_results.html b/TV_APP_V2/templates/mobile_league_results.html new file mode 100644 index 0000000..199e1c8 --- /dev/null +++ b/TV_APP_V2/templates/mobile_league_results.html @@ -0,0 +1,934 @@ + + + + + 📱 League Results + + + + +
+ + +
+ +
+
+
🏆 League Championship
+
+ Final Results - Best 5 of 6 Tournaments +
+
+ {% if league.tournament_type == '40_targets' %} + 40 Targets Format + {% else %} + 20 Targets Format + {% endif %} +
+
+ +
+
+
{{ participants|length if participants else 0 }}
+
Participants
+
+
+
{{ league.total_tournaments }}
+
Tournaments
+
+
+
{% if participants and participants|length > 0 %}{{ participants[0].final_score }}{% else %}0{% endif %}
+
Highest Score
+
+
+
{{ league.finished_at[:10] if league.finished_at else 'Today' }}
+
Completed
+
+
+ + {% if participants and participants|length > 0 %} +
+
🎉 League Complete!
+
+ Congratulations to {{ participants[0].name }} for winning the league! +
+
+ +
+ {% for participant in participants %} +
+
+
{{ participant.rank }}
+
+ {% if participant.rank == 1 %}st + {% elif participant.rank == 2 %}nd + {% elif participant.rank == 3 %}rd + {% else %}th + {% endif %} +
+ {% if participant.rank == 1 %} +
🥇
+ {% elif participant.rank == 2 %} +
🥈
+ {% elif participant.rank == 3 %} +
🥉
+ {% endif %} +
+ +
+
{{ participant.name }}
+
+
ID: {{ participant.id }}
+
+
+ +
+
{{ participant.final_score }}
+
Total: {{ participant.total_score }}
+
Final Score
+
+
+ {% endfor %} +
+ + + + {% set max_breakdowns = 3 %} + {% if participants|length < 3 %} + {% set max_breakdowns = participants|length %} + {% endif %} + + {% for i in range(max_breakdowns) %} + {% set participant = participants[i] %} +
+
+ {% if participant.rank == 1 %}🥇{% elif participant.rank == 2 %}🥈{% elif participant.rank == 3 %}🥉{% endif %} + {{ participant.name }}'s Tournament History +
+
+ {% for tournament_num in range(1, 7) %} + {% set result = participant.tournament_results + | selectattr("tournament", "equalto", tournament_num) + | list + | first %} + + {% if result %} + {% if (result.joker is defined and result.joker) or (result.participated is defined and not result.participated) %} +
+
T{{ tournament_num }}
+
🃏 Joker
+
+ {% else %} + {% set all_participated_scores = participant.tournament_results + | selectattr("participated", "defined") + | selectattr("participated") + | selectattr("score", "defined") + | map(attribute="score") + | list %} + {% set sorted_scores = all_participated_scores | sort(reverse=true) %} + {% set is_excluded = sorted_scores | length > 5 and result.score == sorted_scores[-1] %} + + {% if is_excluded %} +
+
T{{ tournament_num }}
+
{{ result.score }} ❌
+
+ {% else %} +
+
T{{ tournament_num }}
+
{{ result.score }} ✅
+
+ {% endif %} + {% endif %} + {% else %} +
+
T{{ tournament_num }}
+
-
+
+ {% endif %} + {% endfor %} + +
+
+ {% endfor %} + + +
+
📖 How Final Scores Are Calculated
+
+
+ 🏆 League Rule: Best 5 out of 6 tournaments count toward final ranking +
+
+ 🃏 Joker System: Each player can skip 1 tournament without penalty +
+
+
+ + Counted toward final score +
+
+ + Excluded (worst score) +
+
+ 🃏 + Joker used (skipped) +
+
+
+
+ {% else %} +
+
+

No League Results Available

+

League results will appear here when the league is complete.

+
+
+ {% endif %} +
+ + +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/mobile_menu.html b/TV_APP_V2/templates/mobile_menu.html new file mode 100644 index 0000000..acf3382 --- /dev/null +++ b/TV_APP_V2/templates/mobile_menu.html @@ -0,0 +1,382 @@ + + + + + 📱 Camera Dashboard + + + + +
+ +
Camera Dashboard
+
Loading mobile interface...
+ +
+
Detecting tournament state...
+ + + + + + +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/mobile_results.html b/TV_APP_V2/templates/mobile_results.html new file mode 100644 index 0000000..96d0b85 --- /dev/null +++ b/TV_APP_V2/templates/mobile_results.html @@ -0,0 +1,696 @@ + + + + + 📱 Tournament Results + + + + +
+ + +
+ +
+ + {% if participants %} + {% if participants|length > 0 %} +
+
🎉 Tournament Complete!
+
+ Congratulations to {{ participants[0].name }} for winning! +
+
+ {% endif %} + +
+ {% for participant in participants %} +
+
+
{{ participant.rank }}
+
+ {% if participant.rank == 1 %}st + {% elif participant.rank == 2 %}nd + {% elif participant.rank == 3 %}rd + {% else %}th + {% endif %} +
+ {% if participant.rank == 1 %} +
🥇
+ {% elif participant.rank == 2 %} +
🥈
+ {% elif participant.rank == 3 %} +
🥉
+ {% endif %} +
+ +
+
{{ participant.name }}
+
ID: {{ participant.id }}
+
+ +
+
{{ participant.total_score }}
+
Points
+
+
+ {% endfor %} +
+ {% else %} +
+
+

No Results Available

+

Tournament results will appear here when scoring is complete.

+
+
+ {% endif %} + + +
+
📊 Tournament Information
+
+
+ Total Participants: + {{ participants|length if participants else 0 }} +
+
+ Tournament ID: + {{ results.tournament_id[:10] if results.tournament_id else 'Unknown' }}... +
+
+ Created: + {{ results.created_at[:10] if results.created_at else 'Unknown' }} +
+
+ Status: + Completed +
+ {% if participants and participants|length > 0 %} +
+ Highest Score: + {{ participants[0].total_score }} points +
+
+ Winner: + {{ participants[0].name }} +
+ {% endif %} +
+
+
+ + +
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/mobile_streams.html b/TV_APP_V2/templates/mobile_streams.html new file mode 100644 index 0000000..6da9457 --- /dev/null +++ b/TV_APP_V2/templates/mobile_streams.html @@ -0,0 +1,670 @@ + + + + + 📱 Camera Streams + + + + +
+ + +
+ +
+
+ {% for i in range(1, 7) %} + {% set camera_id = i %} +
+
+
Loading...
+ Camera {{ camera_id }} +
+
+
+ {% if tournament_active and current_round_data and (i-1) < current_round_data.players|length %} + {{ current_round_data.players[i-1].name }} + {% else %} + {{ settings.camera_titles[camera_id|string] }} + {% endif %} +
+
+
+ {% endfor %} +
+
+ + + + + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/modern_archive_index.html b/TV_APP_V2/templates/modern_archive_index.html new file mode 100644 index 0000000..dd05a5e --- /dev/null +++ b/TV_APP_V2/templates/modern_archive_index.html @@ -0,0 +1,701 @@ + + + + + Tournament Archive + + + + + + +
+ +
+
+ 🏆 +
{{ stats.total_tournaments }}
+
Standalone Tournaments
+
+
+ 🎖️ +
{{ stats.total_leagues }}
+
Completed Leagues
+
+
+ 👥 +
{{ stats.total_players }}
+
Active Players
+
+
+ 📊 +
{{ stats.total_matches }}
+
Total Competitions
+
+
+ + + {% if leagues %} +
+

🎖️ League Championships

+
+ {% for league in leagues %} +
+
+
+
League Championship
+
{{ league.created_at[:10] if league.created_at != 'Unknown' else 'Unknown Date' }}
+
+ League +
+
+
+
+ 👥 + {{ league.participants_count }} players +
+
+ 🎯 + {{ league.tournament_type.replace('_', ' ')|title }} +
+
+ 📅 + {{ league.archived_at[:10] if league.archived_at != 'Unknown' else 'Unknown Date' }} +
+
+ + {{ league.completed_tournaments }}/{{ league.total_tournaments }} tournaments +
+
+
+ 🏆 View Results + +
+
+
+ {% endfor %} +
+
+ {% endif %} + + + {% if tournaments %} +
+

🏆 Standalone Tournaments

+
+ {% for tournament in tournaments %} +
+
+
+
Single Tournament
+
{{ tournament.created_at[:10] if tournament.created_at != 'Unknown' else 'Unknown Date' }}
+
+ Tournament +
+
+
+
+ 👥 + {{ tournament.participants_count }} players +
+
+ 🎯 + {{ tournament.tournament_type.replace('_', ' ')|title }} +
+
+ 📅 + {{ tournament.archived_at[:10] if tournament.archived_at != 'Unknown' else 'Unknown Date' }} +
+
+ 🏁 + {{ 'Finished' if tournament.tournament_finished else 'Incomplete' }} +
+
+
+ 📊 View Results + +
+
+
+ {% endfor %} +
+
+ {% endif %} + + + {% if not leagues and not tournaments %} +
+
+
📚
+

No Archives Found

+

Complete some tournaments or leagues to see them archived here

+ 🏆 Start Tournament +
+
+ {% endif %} +
+ + + + + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/modern_player_analysis.html b/TV_APP_V2/templates/modern_player_analysis.html new file mode 100644 index 0000000..c8a8643 --- /dev/null +++ b/TV_APP_V2/templates/modern_player_analysis.html @@ -0,0 +1,776 @@ + + + + + + Player Analysis - Camera Dashboard + + + + + + + +
+ +
+
+ 👥 +
{{ players|length }}
+
Total Players
+
+
+ +
{{ players|selectattr('current_player')|list|length }}
+
Active Players
+
+
+ 📊 +
{{ overview_stats.avg_tournaments if overview_stats else 0 }}
+
Avg Tournaments
+
+
+ 🏆 +
{{ overview_stats.top_score if overview_stats else 0 }}
+
Highest Score
+
+
+ +
+
Select a Player to Analyze
+ + +
+ + + + +
+ + + +
+
+ + +
+
+
+

Loading players...

+
+
+
+
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/modern_player_stats.html b/TV_APP_V2/templates/modern_player_stats.html new file mode 100644 index 0000000..8913b92 --- /dev/null +++ b/TV_APP_V2/templates/modern_player_stats.html @@ -0,0 +1,627 @@ + + + + + + {{ player.name }} - Player Stats + + + + + + + +
+ +
+ +
+
📊 Statistics
+
+
+
{{ stats.total_tournaments }}
+
Tournaments
+
+
+
{{ stats.total_leagues }}
+
Leagues
+
+
+
{{ stats.best_tournament_score }}
+
Best Score
+
+
+
{{ stats.average_tournament_score|round|int if stats.average_tournament_score > 0 else 0 }}
+
Average
+
+
+
{{ stats.total_shots_fired|default(0) }}
+
Total Shots
+
+
+
{{ stats.worst_tournament_score if stats.worst_tournament_score > 0 else 0 }}
+
Worst Score
+
+
+
+ + +
+
📈 Performance Trend
+
+ +
+
+
+ + +
+ +
+
🎯 Tournament History
+ {% if stats.tournament_history %} +
+ {% for tournament in stats.tournament_history[:10] %} +
+
+
{{ tournament.date[:10] if tournament.date != 'Unknown' else 'Unknown Date' }}
+
{{ tournament.tournament_type.replace('_', ' ')|title }} • {{ tournament.shots_fired }} shots
+
+
{{ tournament.score }}
+
+ {% endfor %} + {% if stats.tournament_history|length > 10 %} +
+ ... and {{ stats.tournament_history|length - 10 }} more +
+ {% endif %} +
+ {% else %} +
+
🎯
+
No tournament history
+
+ {% endif %} +
+ + +
+
🏆 League History
+ {% if stats.league_history %} +
+ {% for league in stats.league_history %} +
+
+
+
{{ league.date[:10] if league.date != 'Unknown' else 'Unknown Date' }}
+
+ League Championship • {{ league.tournaments_participated }}/6 tournaments + {% if league.joker_used %} • 🃏 Joker Used{% endif %} +
+
+
{{ league.final_score }}
+
+ +
+
+ Final Score: {{ league.final_score }} + Total: {{ league.total_score }} +
+
+ {% for result in league.tournament_results %} + + T{{ result.tournament }}: {{ result.score if result.participated else 'Joker' }} + + {% endfor %} +
+
+
+ {% endfor %} +
+ {% else %} +
+
🏆
+
No league history
+
+ {% endif %} +
+
+
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/results_calculator.html b/TV_APP_V2/templates/results_calculator.html new file mode 100644 index 0000000..647ccb5 --- /dev/null +++ b/TV_APP_V2/templates/results_calculator.html @@ -0,0 +1,1074 @@ + + + + + Results Calculator + + + + + + +
+
+
🎯 Tournament Scoring
+
Enter scores for each participant (20 targets, 2 shots each). Score 0 = miss.
+ +
+
+
0
+
Completed
+
+
+
{{ results.participants|length }}
+
Total
+
+
+
0
+
Total Shots
+
+
+
+ +
+ {% for player_id, participant in results.participants.items() %} +
+
+
+
{{ participant.name }}
+
ID: {{ player_id }}
+
+
+
+
{{ participant.total_score }}
+
Points
+
+
+ {% if participant.completed %} + Completed + {% else %} + Not Started + {% endif %} +
+
+
+
+ +
+
+
+ {% for i in range(1, 21) %} +
+
{{ i }}
+
+ + +
+
+ {% endfor %} +
+ +
+ + + +
+
+
+
+ {% endfor %} +
+ +
+
🏁 Finish Tournament
+
+ Complete scoring for all participants and finalize tournament results. +
+ + +
+
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/results_display.html b/TV_APP_V2/templates/results_display.html new file mode 100644 index 0000000..15d0799 --- /dev/null +++ b/TV_APP_V2/templates/results_display.html @@ -0,0 +1,872 @@ + + + + + Tournament Results + + + + + + +
+ +
+ +
+ +
Tournament Results
+
Final Rankings & Scores
+
+
+ {{ participants|length }} + Participants +
+
+ {{ (participants|length * 40) if participants else 0 }} + Total Shots +
+
+ {% if participants and participants|length > 0 %}{{ participants[0].total_score }}{% else %}0{% endif %} + Highest Score +
+
+
+ + {% if participants and participants|length >= 3 %} + +
+
🏆 Top 3 Winners
+
+ {% for i in range(3) %} + {% set participant = participants[i] %} +
+
+
{{ participant.rank }}
+
+ {% if participant.rank == 1 %}st + {% elif participant.rank == 2 %}nd + {% elif participant.rank == 3 %}rd + {% else %}th + {% endif %} +
+
+ {% if participant.rank == 1 %}🥇 + {% elif participant.rank == 2 %}🥈 + {% elif participant.rank == 3 %}🥉 + {% endif %} +
+
+ +
+
{{ participant.name }}
+
ID: {{ participant.id }}
+
+ +
+
{{ participant.total_score }}
+
Points
+
+
+ {% endfor %} +
+
+ {% endif %} +
+ + +
+
+

📊 Complete Rankings

+
+ +
+ + + + + + + + + + + + {% for participant in participants %} + + + + + + + + {% endfor %} + +
RankParticipantIDScoreStatus
+ {{ participant.rank }} + {% if participant.rank == 1 %}🥇 + {% elif participant.rank == 2 %}🥈 + {% elif participant.rank == 3 %}🥉 + {% endif %} + {{ participant.name }}{{ participant.id }}{{ participant.total_score }} + + ✓ + +
+
+ + +
+
+ + + + \ No newline at end of file diff --git a/TV_APP_V2/templates/tournament.html b/TV_APP_V2/templates/tournament.html new file mode 100644 index 0000000..4fc5b9d --- /dev/null +++ b/TV_APP_V2/templates/tournament.html @@ -0,0 +1,1565 @@ + + + + + Tournament Management + + + + + + +
+ + +
+ {% if league_state and not league_state.league_finished %} +

🏆 League Management

+
+
🏆 League Active
+
+
+
Tournament Type
+
+ {% if league_state.tournament_type == '40_targets' %} + 40 Targets + {% else %} + 20 Targets + {% endif %} +
+
+
+
Current Tournament
+
{{ league_state.current_tournament }} / {{ league_state.total_tournaments }}
+
+
+
Participants
+
{{ league_state.participants|length }}
+
+
+
Completed
+
{{ league_state.completed_tournaments|length }}
+
+
+
Created
+
{{ league_state.created_at[:10] }}
+
+
+ + {% if tournament_state %} + {% else %} + {% if league_state.current_tournament < league_state.total_tournaments %} + +
+
🃏 Joker Selection for Tournament {{ league_state.current_tournament + 1 }}
+

Select players who will use their joker (skip this tournament). Each player can only use their joker once per league.

+ +
+ {% for player_id, participant in league_state.participants.items() %} +
+ {{ participant.name }} + +
+ {% endfor %} +
+
+ +
+ +
+ {% else %} +
+ League Complete! All tournaments scheduled. Finish current one to see final results. +
+ {% endif %} + {% endif %} +
+ + {% elif league_state and league_state.league_finished %} +

🏁 League Completed

+
+
🏆 League Completed!
+
+
+
Tournament Type
+
{{ league_state.tournament_type == '40_targets' and '40 Targets' or '20 Targets' }}
+
+
+
Participants
+
{{ league_state.participants|length }}
+
+
+
Tournaments
+
{{ league_state.total_tournaments }}
+
+
+
Finished
+
{{ league_state.finished_at[:10] if league_state.finished_at else 'Today' }}
+
+
+ +
+ 🏆 View League Results + +
+
+ + {% elif not league_state and tournament_state %} +

🎯 Single Tournament Management

+
+
🎯 Tournament Active
+
+
+
Tournament Type
+
{{ tournament_state.tournament_type == '40_targets' and '40 Targets' or '20 Targets' }}
+
+
+
Players
+
{{ tournament_state.total_players }}
+
+
+
Rounds
+
{{ tournament_state.total_rounds }}
+
+
+
Current Round
+
{{ tournament_state.current_round }}
+
+
+
Created
+
{{ tournament_state.created_at[:10] if tournament_state.created_at else 'Today' }}
+
+
+ +
+ 📋 View Draft + 🎯 Score Tournament + 📺 Dashboard + +
+
+ + {% else %} +

🏁 Setup

+
No Active League or Tournament
+ + +
+
🎯 Select Tournament Type
+
+
+ +
20 Targets
+
Standard format with 20 targets, 2 shots each (40 shots total)
+
+
+ +
40 Targets
+
Extended format with 40 targets, 2 shots each (80 shots total)
+
+
+
+ +
+ 0 players enabled +
+ +
+ + +
+ + + {% endif %} +
+ + + + {% if tournament_state and league_state %} +
+

📋 Current Tournament

+ +
+
🎯 Tournament Active
+
+
+
Tournament Type
+
+ {% if tournament_state.tournament_type == '40_targets' %} + 40 Targets + {% else %} + 20 Targets + {% endif %} +
+
+
+
Total Players
+
{{ tournament_state.total_players }}
+
+
+
Total Rounds
+
{{ tournament_state.total_rounds }}
+
+
+
Current Round
+
{{ tournament_state.current_round }}
+
+ {% if league_state %} +
+
League Tournament
+
{{ tournament_state.league_tournament_number or 'N/A' }}
+
+ {% endif %} +
+ +
+ 📋 View Draft + 🎯 Score Tournament + 📺 Dashboard + +
+
+
+ {% endif %} + + + {% if not league_state and not tournament_state %} +
+

👥 Player Management

+ + +
+ + + +
+ + +
+ +
+
+ {% endif %} +
+ + + + + + + \ No newline at end of file diff --git a/TV_APP_V2/tournament_results.json b/TV_APP_V2/tournament_results.json new file mode 100644 index 0000000..63e99e4 --- /dev/null +++ b/TV_APP_V2/tournament_results.json @@ -0,0 +1,357 @@ +{ + "tournament_id": "2025-07-29T20:39:47.326034", + "tournament_type": "20_targets", + "participants": { + "4": { + "name": "Mateja Pleterski", + "targets": { + "1": { + "shot1": 0, + "shot2": 6 + }, + "2": { + "shot1": 7, + "shot2": 0 + }, + "3": { + "shot1": 3, + "shot2": 1 + }, + "4": { + "shot1": 1, + "shot2": 0 + }, + "5": { + "shot1": 10, + "shot2": 2 + }, + "6": { + "shot1": 10, + "shot2": 5 + }, + "7": { + "shot1": 2, + "shot2": 2 + }, + "8": { + "shot1": 7, + "shot2": 10 + }, + "9": { + "shot1": 1, + "shot2": 5 + }, + "10": { + "shot1": 5, + "shot2": 10 + }, + "11": { + "shot1": 1, + "shot2": 1 + }, + "12": { + "shot1": 2, + "shot2": 7 + }, + "13": { + "shot1": 7, + "shot2": 4 + }, + "14": { + "shot1": 9, + "shot2": 5 + }, + "15": { + "shot1": 0, + "shot2": 10 + }, + "16": { + "shot1": 8, + "shot2": 8 + }, + "17": { + "shot1": 2, + "shot2": 8 + }, + "18": { + "shot1": 10, + "shot2": 8 + }, + "19": { + "shot1": 6, + "shot2": 6 + }, + "20": { + "shot1": 4, + "shot2": 6 + } + }, + "total_score": 199, + "completed": true + }, + "2": { + "name": "Nik Pleterski", + "targets": { + "1": { + "shot1": 2, + "shot2": 5 + }, + "2": { + "shot1": 3, + "shot2": 7 + }, + "3": { + "shot1": 7, + "shot2": 7 + }, + "4": { + "shot1": 7, + "shot2": 2 + }, + "5": { + "shot1": 2, + "shot2": 4 + }, + "6": { + "shot1": 2, + "shot2": 6 + }, + "7": { + "shot1": 0, + "shot2": 5 + }, + "8": { + "shot1": 5, + "shot2": 4 + }, + "9": { + "shot1": 9, + "shot2": 7 + }, + "10": { + "shot1": 5, + "shot2": 8 + }, + "11": { + "shot1": 5, + "shot2": 8 + }, + "12": { + "shot1": 5, + "shot2": 4 + }, + "13": { + "shot1": 4, + "shot2": 7 + }, + "14": { + "shot1": 1, + "shot2": 10 + }, + "15": { + "shot1": 5, + "shot2": 3 + }, + "16": { + "shot1": 8, + "shot2": 5 + }, + "17": { + "shot1": 4, + "shot2": 9 + }, + "18": { + "shot1": 3, + "shot2": 1 + }, + "19": { + "shot1": 10, + "shot2": 10 + }, + "20": { + "shot1": 7, + "shot2": 0 + } + }, + "total_score": 206, + "completed": true + }, + "1": { + "name": "Domen Pleterski", + "targets": { + "1": { + "shot1": 6, + "shot2": 1 + }, + "2": { + "shot1": 6, + "shot2": 5 + }, + "3": { + "shot1": 7, + "shot2": 5 + }, + "4": { + "shot1": 7, + "shot2": 8 + }, + "5": { + "shot1": 10, + "shot2": 6 + }, + "6": { + "shot1": 7, + "shot2": 0 + }, + "7": { + "shot1": 1, + "shot2": 8 + }, + "8": { + "shot1": 3, + "shot2": 1 + }, + "9": { + "shot1": 6, + "shot2": 8 + }, + "10": { + "shot1": 3, + "shot2": 6 + }, + "11": { + "shot1": 1, + "shot2": 8 + }, + "12": { + "shot1": 0, + "shot2": 2 + }, + "13": { + "shot1": 1, + "shot2": 8 + }, + "14": { + "shot1": 5, + "shot2": 4 + }, + "15": { + "shot1": 1, + "shot2": 3 + }, + "16": { + "shot1": 1, + "shot2": 10 + }, + "17": { + "shot1": 0, + "shot2": 6 + }, + "18": { + "shot1": 6, + "shot2": 5 + }, + "19": { + "shot1": 8, + "shot2": 6 + }, + "20": { + "shot1": 4, + "shot2": 3 + } + }, + "total_score": 186, + "completed": true + }, + "3": { + "name": "Ivan Tandler", + "targets": { + "1": { + "shot1": 5, + "shot2": 8 + }, + "2": { + "shot1": 9, + "shot2": 3 + }, + "3": { + "shot1": 3, + "shot2": 1 + }, + "4": { + "shot1": 4, + "shot2": 8 + }, + "5": { + "shot1": 3, + "shot2": 1 + }, + "6": { + "shot1": 9, + "shot2": 5 + }, + "7": { + "shot1": 8, + "shot2": 0 + }, + "8": { + "shot1": 0, + "shot2": 2 + }, + "9": { + "shot1": 10, + "shot2": 6 + }, + "10": { + "shot1": 1, + "shot2": 3 + }, + "11": { + "shot1": 8, + "shot2": 3 + }, + "12": { + "shot1": 8, + "shot2": 8 + }, + "13": { + "shot1": 6, + "shot2": 3 + }, + "14": { + "shot1": 10, + "shot2": 2 + }, + "15": { + "shot1": 10, + "shot2": 2 + }, + "16": { + "shot1": 9, + "shot2": 2 + }, + "17": { + "shot1": 9, + "shot2": 4 + }, + "18": { + "shot1": 2, + "shot2": 10 + }, + "19": { + "shot1": 5, + "shot2": 9 + }, + "20": { + "shot1": 9, + "shot2": 5 + } + }, + "total_score": 213, + "completed": true + } + }, + "tournament_finished": true, + "created_at": "2025-07-29T20:39:47.326244", + "finished_at": "2025-07-29T20:39:58.449723" +} \ No newline at end of file