from flask import Flask, render_template, jsonify, request, session, redirect, url_for, send_file, make_response from datetime import datetime, timedelta import os import sys import traceback from services.sleeper_api import SleeperAPI from services.espn_api import ESPNAPI from config import Config from faker import Faker # Force unbuffered output for Docker logs os.environ['PYTHONUNBUFFERED'] = '1' print("=== RosterHash Starting ===", flush=True) print(f"=== Version: {Config.VERSION} ===", flush=True) app = Flask(__name__) app.config.from_object(Config) # Initialize API services sleeper_api = SleeperAPI() espn_api = ESPNAPI() # Simple memory cache for API responses api_cache = {} CACHE_DURATION = 300 # 5 minutes in seconds # Debug time override debug_time_override = None def get_debug_time(): """Get the current time, or debug override time if set""" global debug_time_override if debug_time_override: return debug_time_override return datetime.now() def get_cached_or_fetch(cache_key, fetch_function, *args): """Get data from cache or fetch it and cache the result""" now = get_debug_time() if cache_key in api_cache: cached_data, cached_time = api_cache[cache_key] if (now - cached_time).total_seconds() < CACHE_DURATION: print(f"DEBUG: Using cached data for {cache_key}", flush=True) return cached_data print(f"DEBUG: Fetching fresh data for {cache_key}", flush=True) try: data = fetch_function(*args) api_cache[cache_key] = (data, now) return data except Exception as e: print(f"ERROR: Failed to fetch {cache_key}: {str(e)}", flush=True) # Return cached data if available, even if stale if cache_key in api_cache: cached_data, _ = api_cache[cache_key] print(f"DEBUG: Using stale cached data for {cache_key}", flush=True) return cached_data raise def get_league_color(league_index): """Assign colors to leagues in order""" colors = app.config['LEAGUE_COLORS'] return colors[league_index % len(colors)] # Cycle through colors def normalize_team_abbreviation(espn_team): """Convert ESPN team abbreviation to Sleeper format""" team_map = app.config['TEAM_ABBREVIATION_MAP'] return team_map.get(espn_team, espn_team) @app.context_processor def inject_apis(): """Make API and version available to all templates""" global debug_time_override is_debug_mode = request.args.get('debug') == 'true' return dict( sleeper_api=sleeper_api, app_version=app.config['VERSION'], current_theme=session.get('theme', 'dark'), is_debug_mode=is_debug_mode, debug_time_override=debug_time_override ) @app.route('/') def index(): """Home page with username input""" print("DEBUG: Index route accessed", flush=True) # Get the last used username from cookie last_username = request.cookies.get('last_username', '') mySleeperUsername = Faker().user_name() print(f"DEBUG: Last username from cookie: '{last_username}'", flush=True) # Auto-redirect to dashboard if username cookie exists if last_username and last_username.strip(): print(f"DEBUG: Auto-redirecting to dashboard for user: '{last_username}'", flush=True) return redirect(url_for('dashboard_current', username=last_username.strip())) return render_template('index.html', last_username=last_username, mySleeperUsername=mySleeperUsername) @app.route('/dashboard') def dashboard_form(): """Handle form submission from homepage""" username = request.args.get('username') if username: return dashboard_current(username.strip()) else: return redirect(url_for('index')) # Add theme handling route @app.route('/toggle_theme', methods=['POST']) def toggle_theme(): """Toggle between light and dark theme""" current_theme = session.get('theme', 'dark') new_theme = 'dark' if current_theme == 'light' else 'light' session['theme'] = new_theme # Get return URL from form or default to index return_url = request.form.get('return_url', url_for('index')) return redirect(return_url) # Add timezone handling route @app.route('/set_timezone', methods=['POST']) def set_timezone(): """Set user's timezone preference""" timezone = request.form.get('timezone') if timezone: session['user_timezone'] = timezone print(f"DEBUG: Set user timezone to: {timezone}", flush=True) # Get return URL from form or default to index return_url = request.form.get('return_url', url_for('index')) return redirect(return_url) # Add debug time override route @app.route('/set_debug_time', methods=['POST']) def set_debug_time(): """Set debug time override (only in debug mode)""" global debug_time_override # Check if debug mode is enabled if not request.args.get('debug') == 'true': return jsonify({'error': 'Debug mode not enabled'}), 403 debug_datetime_str = request.form.get('debug_time') if debug_datetime_str: try: # Parse the datetime string from the picker (format: YYYY-MM-DDTHH:MM) debug_time_override = datetime.strptime(debug_datetime_str, '%Y-%m-%dT%H:%M') print(f"DEBUG: Set debug time override to: {debug_time_override}", flush=True) # Clear cache when time changes to get fresh data api_cache.clear() print("DEBUG: Cleared API cache due to time override", flush=True) except ValueError as e: print(f"DEBUG: Invalid debug time format: {debug_datetime_str}, error: {e}", flush=True) return jsonify({'error': 'Invalid datetime format'}), 400 else: # Clear the override debug_time_override = None api_cache.clear() print("DEBUG: Cleared debug time override", flush=True) # Get return URL from form or default to index return_url = request.form.get('return_url', url_for('index')) return redirect(return_url) @app.route('/change-username') def change_username(): """Clear username cookie and redirect to index""" response = make_response(redirect(url_for('index'))) response.set_cookie('last_username', '', expires=0) # Clear the cookie return response @app.route('/') def dashboard_current(username): """Dashboard for current NFL week""" print(f"DEBUG: Dashboard current route - username: '{username}'", flush=True) try: nfl_state = sleeper_api.get_nfl_state() current_week = nfl_state.get('week', 1) print(f"DEBUG: Current week: {current_week}", flush=True) response = make_response(dashboard(username, current_week)) # Set cookie to remember this username (expires in 30 days) response.set_cookie('last_username', username, max_age=30*24*60*60) return response except Exception as e: print(f"ERROR: dashboard_current exception - {str(e)}", flush=True) print(f"ERROR: Full traceback: {traceback.format_exc()}", flush=True) return render_template('error.html', message=f"Could not find user '{username}'. Please check the username and try again.") @app.route('//') def dashboard_week(username, week): """Dashboard for specific week""" print(f"DEBUG: Dashboard week - username: '{username}', week: {week}", flush=True) response = make_response(dashboard(username, week)) # Set cookie to remember this username (expires in 30 days) response.set_cookie('last_username', username, max_age=30*24*60*60) return response @app.route('///refresh', methods=['POST']) def refresh_scores(username, week): """Server-side refresh scores - redirect back to dashboard""" print(f"DEBUG: Refresh scores POST - username: '{username}', week: {week}", flush=True) return redirect(url_for('dashboard_week', username=username, week=week)) def dashboard(username, week): """Main dashboard logic - fetch and display user's fantasy data""" print(f"DEBUG: Dashboard function START - username: '{username}', week: {week}", flush=True) try: # Get user info from Sleeper (cached) user_cache_key = f"user_{username}" user = get_cached_or_fetch(user_cache_key, sleeper_api.get_user, username) print(f"DEBUG: get_user() returned: {user}", flush=True) if not user: print(f"WARNING: User lookup failed for username: '{username}'", flush=True) return render_template('error.html', message=f"Could not find user '{username}'. Please check the username and try again.") print(f"DEBUG: User found - ID: {user.get('user_id')}, Display: {user.get('display_name', 'Unknown')}", flush=True) # Get current NFL season (cached) nfl_cache_key = "nfl_state" nfl_state = get_cached_or_fetch(nfl_cache_key, sleeper_api.get_nfl_state) season = nfl_state.get('season', str(datetime.now().year)) print(f"DEBUG: Using season: {season}", flush=True) # Get user's fantasy leagues (cached) user_id = user['user_id'] leagues_cache_key = f"leagues_{user_id}_{season}" leagues = get_cached_or_fetch(leagues_cache_key, sleeper_api.get_user_leagues, user_id, season) print(f"DEBUG: Found {len(leagues) if leagues else 0} leagues", flush=True) if leagues: for i, league in enumerate(leagues): print(f"DEBUG: League {i+1}: ID={league.get('league_id')}, Name='{league.get('name')}'", flush=True) # Get NFL game schedule for the week with user's timezone user_timezone = session.get('user_timezone', 'America/Los_Angeles') # Default to PST print(f"DEBUG: Using timezone: {user_timezone}", flush=True) print(f"DEBUG: Calling get_week_schedule() for week {week}, season {season}", flush=True) try: schedule = espn_api.get_week_schedule(week, season, user_timezone, debug_time_override) print(f"DEBUG: Schedule retrieved successfully", flush=True) except Exception as e: print(f"ERROR: ESPN schedule fetch failed: {str(e)}", flush=True) schedule = {} # Process each league for matchup data league_data = [] for i, league in enumerate(leagues): print(f"DEBUG: ===== Processing league {i+1}/{len(leagues)}: '{league['name']}' =====", flush=True) league_id = league['league_id'] try: # Get matchups for the current week print(f"DEBUG: Calling get_matchups() for league {league_id}, week {week}", flush=True) matchups = sleeper_api.get_matchups(league_id, week) print(f"DEBUG: Found {len(matchups) if matchups else 0} matchups", flush=True) # Get rosters to find user's team print(f"DEBUG: Calling get_rosters() for league {league_id}", flush=True) rosters = sleeper_api.get_rosters(league_id) print(f"DEBUG: Found {len(rosters) if rosters else 0} rosters", flush=True) # Find user's roster in this league user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None) print(f"DEBUG: User roster found: {user_roster is not None}", flush=True) if user_roster: # Find user's matchup for this week (if matchups exist) user_matchup = None if matchups: user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None) print(f"DEBUG: User matchup found: {user_matchup is not None}", flush=True) # Find opponent's matchup and user info opponent_matchup = None opponent_user = None if user_matchup and matchups: # Find opponent in same matchup opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None) print(f"DEBUG: Opponent matchup found: {opponent_matchup is not None}", flush=True) if opponent_matchup: # Get opponent's roster to find owner opponent_roster = next((r for r in rosters if r['roster_id'] == opponent_matchup['roster_id']), None) if opponent_roster: opponent_owner_id = opponent_roster['owner_id'] # Fetch opponent's user profile opponent_user = sleeper_api.get_user_by_id(opponent_owner_id) print(f"DEBUG: Opponent user: {opponent_user.get('display_name') if opponent_user else 'None'}", flush=True) # Get all players on user's roster for calendar with starter info all_players = [] if user_roster and user_roster.get('players'): players_list = user_roster['players'] starters_list = user_matchup.get('starters') if user_matchup else None starters_list = starters_list or [] # Handle None case players_points = user_matchup.get('players_points') if user_matchup else None players_points = players_points or {} # Handle None case print(f"DEBUG: Processing {len(players_list)} total players, {len(starters_list)} starters", flush=True) for player_id in players_list: try: # Get player details from Sleeper API player_info = sleeper_api.get_player_info(player_id) if player_info: # Create a copy of the player object for this league to avoid shared references player = player_info.copy() # Add starter status to player data player['is_starter'] = player_id in starters_list # Add player points for this week (league-specific) player['points'] = players_points.get(player_id, 0.0) # Check multiple possible injury status fields injury_status = ( player.get('injury_status') or player.get('status') or player.get('injury_designation') or player.get('practice_participation') ) # Only show injury status if it's not "Active" or None if injury_status and injury_status.lower() != 'active': player['injury_status'] = injury_status else: player['injury_status'] = None # Print any player with injury status (only non-Active) if player['injury_status']: player_name = f"{player.get('first_name', '')} {player.get('last_name', '')}".strip() print(f"DEBUG: {player_name} has injury status: {player['injury_status']}", flush=True) all_players.append(player) except Exception as e: print(f"ERROR: Failed to get player info for {player_id}: {str(e)}", flush=True) # Calculate winning/losing status user_points = user_matchup['points'] if user_matchup else 0 opponent_points = opponent_matchup['points'] if opponent_matchup else 0 # Determine status if user_points > opponent_points: match_status = 'winning' elif user_points < opponent_points: match_status = 'losing' else: match_status = 'tied' # Store processed league data league_info = { 'league': league, 'league_color': get_league_color(i), # Assign color by order 'user_matchup': user_matchup, 'opponent_matchup': opponent_matchup, 'opponent_user': opponent_user, 'user_roster': user_roster, 'all_players': all_players, 'match_status': match_status, 'user_points': user_points, 'opponent_points': opponent_points } league_data.append(league_info) print(f"DEBUG: League '{league['name']}' processed successfully", flush=True) else: print(f"DEBUG: Skipping league '{league['name']}' - missing data", flush=True) except Exception as e: print(f"ERROR: Failed to process league '{league['name']}': {str(e)}", flush=True) print(f"ERROR: League processing traceback: {traceback.format_exc()}", flush=True) # Debug output before rendering template print(f"DEBUG: About to render template with {len(league_data)} leagues", flush=True) for league_info in league_data: print(f"DEBUG: League '{league_info['league']['name']}' has {len(league_info['all_players'])} players", flush=True) total_game_count = sum(len(games) for games in schedule.values()) print(f"DEBUG: Schedule has {total_game_count} total games", flush=True) try: # Render dashboard with all collected data result = render_template('dashboard.html', user=user, week=week, league_data=league_data, schedule=schedule, nfl_state=nfl_state) print(f"DEBUG: Template rendered successfully", flush=True) return result except Exception as e: print(f"ERROR: Template rendering failed: {str(e)}", flush=True) print(f"ERROR: Template rendering traceback: {traceback.format_exc()}", flush=True) return render_template('error.html', message=f"Template error: {str(e)}") except Exception as e: print(f"ERROR: Dashboard function exception: {str(e)}", flush=True) print(f"ERROR: Full dashboard traceback: {traceback.format_exc()}", flush=True) return render_template('error.html', message=f"Error loading data: {str(e)}") @app.route('/api/refresh//') def refresh_data(username, week): """API endpoint for live score updates""" print(f"DEBUG: Refresh data API - username: '{username}', week: {week}", flush=True) try: # Get user and current season user = sleeper_api.get_user(username) if not user: return jsonify({'error': 'User not found'}), 404 nfl_state = sleeper_api.get_nfl_state() season = nfl_state.get('season', str(datetime.now().year)) leagues = sleeper_api.get_user_leagues(user['user_id'], season) league_scores = [] # Get updated scores for each league for league in leagues: matchups = sleeper_api.get_matchups(league['league_id'], week) rosters = sleeper_api.get_rosters(league['league_id']) user_roster = next((r for r in rosters if r['owner_id'] == user['user_id']), None) if user_roster and matchups: # Find user and opponent matchups user_matchup = next((m for m in matchups if m['roster_id'] == user_roster['roster_id']), None) opponent_matchup = None if user_matchup: opponent_matchup = next((m for m in matchups if m['matchup_id'] == user_matchup['matchup_id'] and m['roster_id'] != user_roster['roster_id']), None) # Return current points league_scores.append({ 'league_id': league['league_id'], 'league_name': league['name'], 'user_points': user_matchup['points'] if user_matchup else 0, 'opponent_points': opponent_matchup['points'] if opponent_matchup else 0 }) return jsonify({'league_scores': league_scores}) except Exception as e: print(f"ERROR: Refresh data exception: {str(e)}", flush=True) return jsonify({'error': str(e)}), 500 # Static file routes @app.route('/rosterhash_logo.png') def logo(): """Serve the logo file""" return send_file('rosterhash_logo.png', mimetype='image/png') @app.route('/favicon.ico') def favicon(): """Serve the favicon""" return send_file('rosterhash_logo.png', mimetype='image/png') if __name__ == '__main__': print("DEBUG: Starting Flask app on 0.0.0.0:5000", flush=True) print(f"DEBUG: RosterHash {app.config['VERSION']} ready!", flush=True) app.run(host='0.0.0.0', port=5000, debug=True)