274 lines
10 KiB
Python
274 lines
10 KiB
Python
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('/<int:lineup_id>')
|
|
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
|