from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, session from web.services.web_service import WebService from web.services.stats_service import StatsService from web.services.feature_service import FeatureService from web.services.roster_version_service import RosterVersionService from web.services.team_performance_service import TeamPerformanceService from web.services.narrative_service import NarrativeService import json import logging bp = Blueprint('teams', __name__, url_prefix='/teams') logger = logging.getLogger(__name__) # --- API Endpoints --- @bp.route('/api/search') def api_search(): query = request.args.get('q', '').strip() # Strip whitespace if len(query) < 2: return jsonify([]) # Use L2 database for fuzzy search on username from web.services.stats_service import StatsService # Support sorting by matches for better "Find Player" experience sort_by = request.args.get('sort', 'matches') players, total = StatsService.get_players(page=1, per_page=50, search=query, sort_by=sort_by) # Format for frontend results = [] for p in players: # Convert sqlite3.Row to dict to avoid AttributeError p_dict = dict(p) # Fetch feature stats for better preview f = FeatureService.get_player_features(p_dict['steam_id_64']) # Manually attach match count if not present matches_played = p_dict.get('matches_played', 0) results.append({ 'steam_id': p_dict['steam_id_64'], 'name': p_dict['username'], 'avatar': p_dict['avatar_url'] or 'https://avatars.steamstatic.com/fef49e7fa7e1997310d705b2a6158ff8dc1cdfeb_full.jpg', 'rating': (f['core_avg_rating'] if f else 0.0), 'matches': matches_played }) # Python-side sort if DB sort didn't work for 'matches' (since dim_players doesn't have match_count) if sort_by == 'matches': # We need to fetch match counts to sort! # This is expensive for search results but necessary for "matches sample sort" # Let's batch fetch counts for these 50 players steam_ids = [r['steam_id'] for r in results] if steam_ids: from web.services.web_service import query_db placeholders = ','.join('?' for _ in steam_ids) sql = f"SELECT steam_id_64, COUNT(*) as cnt FROM fact_match_players WHERE steam_id_64 IN ({placeholders}) GROUP BY steam_id_64" counts = query_db('l2', sql, steam_ids) cnt_map = {r['steam_id_64']: r['cnt'] for r in counts} for r in results: r['matches'] = cnt_map.get(r['steam_id'], 0) results.sort(key=lambda x: x['matches'], reverse=True) return jsonify(results) @bp.route('/api/roster', methods=['GET', 'POST']) def api_roster(): target_team = WebService.get_active_lineup() if not target_team: WebService.save_lineup("My Team", "Default Roster", []) target_team = WebService.get_active_lineup() target_team = dict(target_team) if request.method == 'POST': # Admin Check if not session.get('is_admin'): return jsonify({'error': 'Unauthorized'}), 403 data = request.json action = data.get('action') steam_id = data.get('steam_id') current_ids = [] try: current_ids = json.loads(target_team['player_ids_json']) except: pass previous_ids = list(current_ids) if action == 'add': if steam_id not in current_ids: current_ids.append(steam_id) elif action == 'remove': if steam_id in current_ids: current_ids.remove(steam_id) else: return jsonify({'error': 'Unknown roster action'}), 400 WebService.save_lineup( target_team['name'], target_team['description'], current_ids, lineup_id=target_team['id'], ) try: version_id, created = RosterVersionService.snapshot_roster( current_ids, name=f"{target_team['name']} Roster", ) except Exception: WebService.save_lineup( target_team['name'], target_team['description'], previous_ids, lineup_id=target_team['id'], ) raise return jsonify({'status': 'success', 'roster': current_ids}) # GET: Return detailed player info try: p_ids_json = target_team.get('player_ids_json', '[]') p_ids = json.loads(p_ids_json) players = StatsService.get_players_by_ids(p_ids) # Add extra stats needed for cards enriched = [] if players: for p in players: try: # Convert sqlite3.Row to dict p_dict = dict(p) # print(f"DEBUG: Processing player {p_dict.get('steam_id_64')}") # Get features for Rating/KD display f = FeatureService.get_player_features(p_dict['steam_id_64']) # f might be a Row object, convert it p_dict['stats'] = dict(f) if f else {} # Fetch Metadata (Tags) meta = WebService.get_player_metadata(p_dict['steam_id_64']) p_dict['tags'] = meta.get('tags', []) enriched.append(p_dict) except Exception as inner_e: logger.exception( "Failed to enrich roster player %s", p.get('steam_id_64') if hasattr(p, 'get') else 'unknown', ) return jsonify({ 'status': 'success', 'team': dict(target_team), # Ensure target_team is dict too 'roster': enriched }) except Exception as e: logger.exception("Roster API failed") return jsonify({'error': str(e)}), 500 # --- Views --- @bp.route('/') def index(): # Directly render the Clubhouse SPA return render_template('teams/clubhouse.html') @bp.route('/performance') def performance(): return render_template( 'teams/performance.html', seasons=NarrativeService.get_seasons(), **TeamPerformanceService.get_dashboard(), ) @bp.route('/roster-role', methods=['POST']) def roster_role(): if not session.get('is_admin'): return "Unauthorized", 403 steam_id = request.form.get('steam_id') member_role = request.form.get('member_role') try: RosterVersionService.update_current_member_role( steam_id, member_role, ) flash('Roster role updated.', 'success') except ValueError as exc: flash(str(exc), 'error') return redirect(url_for('teams.performance')) # Deprecated routes (kept for compatibility if needed, but hidden) @bp.route('/list') def list_view(): lineups = WebService.get_lineups() # ... existing logic ... return render_template('teams/list.html', lineups=lineups) @bp.route('/') def detail(lineup_id): try: lineup = WebService.get_lineup(lineup_id) if not lineup: return "Lineup not found", 404 p_ids = json.loads(lineup['player_ids_json']) players = StatsService.get_players_by_ids(p_ids) # Shared Matches shared_matches = StatsService.get_shared_matches(p_ids) # Calculate Aggregate Stats agg_stats = { 'avg_rating': 0, 'avg_kd': 0, 'avg_kast': 0 } radar_data = { 'STA': 0, 'BAT': 0, 'HPS': 0, 'PTL': 0, 'SIDE': 0, 'UTIL': 0 } player_features = [] if players: count = len(players) total_rating = 0 total_kd = 0 total_kast = 0 # Radar totals r_totals = {k: 0 for k in radar_data} for p in players: # Fetch L3 features for each player f = FeatureService.get_player_features(p['steam_id_64']) if f: # Attach stats to player object for template p['rating'] = f.get('core_avg_rating') or 0 p['stats'] = f player_features.append(f) total_rating += f.get('core_avg_rating') or 0 total_kd += f.get('core_avg_kd') or 0 total_kast += f.get('core_avg_kast') or 0 # Radar accumulation (L3 Mapping) r_totals['STA'] += f.get('core_avg_rating') or 0 # Rating (Scale ~1.0) r_totals['BAT'] += (f.get('tac_opening_duel_winrate') or 0) * 2 # WinRate (0.5 -> 1.0) Scale to match Rating? r_totals['HPS'] += (f.get('tac_clutch_1v1_rate') or 0) * 2 # WinRate (0.5 -> 1.0) r_totals['PTL'] += ((f.get('score_pistol') or 0) / 50.0) # Score (0-100 -> 0-2.0) r_totals['SIDE'] += f.get('meta_side_ct_rating') or 0 # Rating (Scale ~1.0) r_totals['UTIL'] += f.get('tac_util_usage_rate') or 0 # Usage Rate (Count? or Rate?) else: player_features.append(None) p['rating'] = 0 if count > 0: agg_stats['avg_rating'] = total_rating / count agg_stats['avg_kd'] = total_kd / count agg_stats['avg_kast'] = total_kast / count for k in radar_data: radar_data[k] = r_totals[k] / count return render_template('teams/detail.html', lineup=lineup, players=players, agg_stats=agg_stats, shared_matches=shared_matches, radar_data=radar_data) except Exception as e: logger.exception("Lineup detail failed for lineup_id=%s", lineup_id) return "Unable to load lineup", 500