1.0.0 : Web Implemented.
This commit is contained in:
75
web/routes/admin.py
Normal file
75
web/routes/admin.py
Normal file
@@ -0,0 +1,75 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
||||
from web.config import Config
|
||||
from web.auth import admin_required
|
||||
from web.database import query_db
|
||||
import os
|
||||
|
||||
bp = Blueprint('admin', __name__, url_prefix='/admin')
|
||||
|
||||
@bp.route('/login', methods=['GET', 'POST'])
|
||||
def login():
|
||||
if request.method == 'POST':
|
||||
token = request.form.get('token')
|
||||
if token == Config.ADMIN_TOKEN:
|
||||
session['is_admin'] = True
|
||||
return redirect(url_for('admin.dashboard'))
|
||||
else:
|
||||
flash('Invalid Token', 'error')
|
||||
return render_template('admin/login.html')
|
||||
|
||||
@bp.route('/logout')
|
||||
def logout():
|
||||
session.pop('is_admin', None)
|
||||
return redirect(url_for('main.index'))
|
||||
|
||||
@bp.route('/')
|
||||
@admin_required
|
||||
def dashboard():
|
||||
return render_template('admin/dashboard.html')
|
||||
|
||||
from web.services.etl_service import EtlService
|
||||
|
||||
@bp.route('/trigger_etl', methods=['POST'])
|
||||
@admin_required
|
||||
def trigger_etl():
|
||||
script_name = request.form.get('script')
|
||||
allowed = ['L1A.py', 'L2_Builder.py', 'L3_Builder.py']
|
||||
if script_name not in allowed:
|
||||
return "Invalid script", 400
|
||||
|
||||
success, message = EtlService.run_script(script_name)
|
||||
status_code = 200 if success else 500
|
||||
return message, status_code
|
||||
|
||||
@bp.route('/sql', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def sql_runner():
|
||||
result = None
|
||||
error = None
|
||||
query = ""
|
||||
db_name = "l2"
|
||||
|
||||
if request.method == 'POST':
|
||||
query = request.form.get('query')
|
||||
db_name = request.form.get('db_name', 'l2')
|
||||
|
||||
# Safety check
|
||||
forbidden = ['DELETE', 'DROP', 'UPDATE', 'INSERT', 'ALTER', 'GRANT', 'REVOKE']
|
||||
if any(x in query.upper() for x in forbidden):
|
||||
error = "Only SELECT queries allowed in Web Runner."
|
||||
else:
|
||||
try:
|
||||
# Enforce limit if not present
|
||||
if 'LIMIT' not in query.upper():
|
||||
query += " LIMIT 50"
|
||||
|
||||
rows = query_db(db_name, query)
|
||||
if rows:
|
||||
columns = rows[0].keys()
|
||||
result = {'columns': columns, 'rows': rows}
|
||||
else:
|
||||
result = {'columns': [], 'rows': []}
|
||||
except Exception as e:
|
||||
error = str(e)
|
||||
|
||||
return render_template('admin/sql.html', result=result, error=error, query=query, db_name=db_name)
|
||||
35
web/routes/main.py
Normal file
35
web/routes/main.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from web.services.stats_service import StatsService
|
||||
import time
|
||||
|
||||
bp = Blueprint('main', __name__)
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
recent_matches = StatsService.get_recent_matches(limit=5)
|
||||
daily_counts = StatsService.get_daily_match_counts()
|
||||
live_matches = StatsService.get_live_matches()
|
||||
|
||||
# Convert rows to dict for easier JS usage
|
||||
heatmap_data = {}
|
||||
if daily_counts:
|
||||
for row in daily_counts:
|
||||
heatmap_data[row['day']] = row['count']
|
||||
|
||||
return render_template('home/index.html', recent_matches=recent_matches, heatmap_data=heatmap_data, live_matches=live_matches)
|
||||
|
||||
from web.services.etl_service import EtlService
|
||||
|
||||
@bp.route('/parse_match', methods=['POST'])
|
||||
def parse_match():
|
||||
url = request.form.get('url')
|
||||
if not url or '5eplay.com' not in url:
|
||||
return jsonify({'success': False, 'message': 'Invalid 5EPlay URL'})
|
||||
|
||||
# Trigger L1A.py with URL argument
|
||||
success, msg = EtlService.run_script('L1A.py', args=[url])
|
||||
|
||||
if success:
|
||||
return jsonify({'success': True, 'message': 'Match parsing completed successfully!'})
|
||||
else:
|
||||
return jsonify({'success': False, 'message': f'Error: {msg}'})
|
||||
48
web/routes/matches.py
Normal file
48
web/routes/matches.py
Normal file
@@ -0,0 +1,48 @@
|
||||
from flask import Blueprint, render_template, request, Response
|
||||
from web.services.stats_service import StatsService
|
||||
from web.config import Config
|
||||
import json
|
||||
|
||||
bp = Blueprint('matches', __name__, url_prefix='/matches')
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
map_name = request.args.get('map')
|
||||
date_from = request.args.get('date_from')
|
||||
|
||||
matches, total = StatsService.get_matches(page, Config.ITEMS_PER_PAGE, map_name, date_from)
|
||||
total_pages = (total + Config.ITEMS_PER_PAGE - 1) // Config.ITEMS_PER_PAGE
|
||||
|
||||
return render_template('matches/list.html', matches=matches, total=total, page=page, total_pages=total_pages)
|
||||
|
||||
@bp.route('/<match_id>')
|
||||
def detail(match_id):
|
||||
match = StatsService.get_match_detail(match_id)
|
||||
if not match:
|
||||
return "Match not found", 404
|
||||
|
||||
players = StatsService.get_match_players(match_id)
|
||||
rounds = StatsService.get_match_rounds(match_id)
|
||||
|
||||
# Organize players by team
|
||||
team1_players = [p for p in players if p['team_id'] == 1]
|
||||
team2_players = [p for p in players if p['team_id'] == 2]
|
||||
|
||||
return render_template('matches/detail.html', match=match,
|
||||
team1_players=team1_players, team2_players=team2_players,
|
||||
rounds=rounds)
|
||||
|
||||
@bp.route('/<match_id>/raw')
|
||||
def raw_json(match_id):
|
||||
match = StatsService.get_match_detail(match_id)
|
||||
if not match:
|
||||
return "Match not found", 404
|
||||
|
||||
# Construct a raw object from available raw fields
|
||||
data = {
|
||||
'round_list': json.loads(match['round_list_raw']) if match['round_list_raw'] else None,
|
||||
'leetify_data': json.loads(match['leetify_data_raw']) if match['leetify_data_raw'] else None
|
||||
}
|
||||
|
||||
return Response(json.dumps(data, indent=2, ensure_ascii=False), mimetype='application/json')
|
||||
198
web/routes/players.py
Normal file
198
web/routes/players.py
Normal file
@@ -0,0 +1,198 @@
|
||||
from flask import Blueprint, render_template, request, jsonify, redirect, url_for, flash, current_app, session
|
||||
from web.services.stats_service import StatsService
|
||||
from web.services.feature_service import FeatureService
|
||||
from web.services.web_service import WebService
|
||||
from web.database import execute_db
|
||||
from web.config import Config
|
||||
from datetime import datetime
|
||||
import os
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
bp = Blueprint('players', __name__, url_prefix='/players')
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
page = request.args.get('page', 1, type=int)
|
||||
search = request.args.get('search')
|
||||
# Default sort by 'matches' as requested
|
||||
sort_by = request.args.get('sort', 'matches')
|
||||
|
||||
players, total = FeatureService.get_players_list(page, Config.ITEMS_PER_PAGE, sort_by, search)
|
||||
total_pages = (total + Config.ITEMS_PER_PAGE - 1) // Config.ITEMS_PER_PAGE
|
||||
|
||||
return render_template('players/list.html', players=players, total=total, page=page, total_pages=total_pages, sort_by=sort_by)
|
||||
|
||||
@bp.route('/<steam_id>', methods=['GET', 'POST'])
|
||||
def detail(steam_id):
|
||||
if request.method == 'POST':
|
||||
# Check if admin action
|
||||
if 'admin_action' in request.form and session.get('is_admin'):
|
||||
action = request.form.get('admin_action')
|
||||
|
||||
if action == 'update_profile':
|
||||
notes = request.form.get('notes')
|
||||
|
||||
# Handle Avatar Upload
|
||||
if 'avatar' in request.files:
|
||||
file = request.files['avatar']
|
||||
if file and file.filename:
|
||||
try:
|
||||
# Use steam_id as filename to ensure uniqueness per player
|
||||
# Preserve extension
|
||||
ext = os.path.splitext(file.filename)[1].lower()
|
||||
if not ext: ext = '.jpg'
|
||||
|
||||
filename = f"{steam_id}{ext}"
|
||||
upload_folder = os.path.join(current_app.root_path, 'static', 'avatars')
|
||||
os.makedirs(upload_folder, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(upload_folder, filename)
|
||||
file.save(file_path)
|
||||
|
||||
# Generate URL (relative to web root)
|
||||
avatar_url = url_for('static', filename=f'avatars/{filename}')
|
||||
|
||||
# Update L2 DB directly (Immediate effect)
|
||||
execute_db('l2', "UPDATE dim_players SET avatar_url = ? WHERE steam_id_64 = ?", [avatar_url, steam_id])
|
||||
|
||||
flash('Avatar updated successfully.', 'success')
|
||||
except Exception as e:
|
||||
print(f"Avatar upload error: {e}")
|
||||
flash('Error uploading avatar.', 'error')
|
||||
|
||||
WebService.update_player_metadata(steam_id, notes=notes)
|
||||
flash('Profile updated.', 'success')
|
||||
|
||||
elif action == 'add_tag':
|
||||
tag = request.form.get('tag')
|
||||
if tag:
|
||||
meta = WebService.get_player_metadata(steam_id)
|
||||
tags = meta.get('tags', [])
|
||||
if tag not in tags:
|
||||
tags.append(tag)
|
||||
WebService.update_player_metadata(steam_id, tags=tags)
|
||||
flash('Tag added.', 'success')
|
||||
|
||||
elif action == 'remove_tag':
|
||||
tag = request.form.get('tag')
|
||||
if tag:
|
||||
meta = WebService.get_player_metadata(steam_id)
|
||||
tags = meta.get('tags', [])
|
||||
if tag in tags:
|
||||
tags.remove(tag)
|
||||
WebService.update_player_metadata(steam_id, tags=tags)
|
||||
flash('Tag removed.', 'success')
|
||||
|
||||
return redirect(url_for('players.detail', steam_id=steam_id))
|
||||
|
||||
# Add Comment
|
||||
username = request.form.get('username', 'Anonymous')
|
||||
content = request.form.get('content')
|
||||
if content:
|
||||
WebService.add_comment(None, username, 'player', steam_id, content)
|
||||
flash('Comment added!', 'success')
|
||||
return redirect(url_for('players.detail', steam_id=steam_id))
|
||||
|
||||
player = StatsService.get_player_info(steam_id)
|
||||
if not player:
|
||||
return "Player not found", 404
|
||||
|
||||
features = FeatureService.get_player_features(steam_id)
|
||||
# Ensure basic stats fallback if features missing or incomplete
|
||||
basic = StatsService.get_player_basic_stats(steam_id)
|
||||
|
||||
if not features:
|
||||
if basic:
|
||||
features = {
|
||||
'basic_avg_rating': basic.get('rating', 0),
|
||||
'basic_avg_kd': basic.get('kd', 0),
|
||||
'basic_avg_kast': basic.get('kast', 0),
|
||||
'basic_avg_adr': basic.get('adr', 0), # Pass ADR
|
||||
}
|
||||
else:
|
||||
# If features exist but ADR is missing (not in L3), try to patch it from basic
|
||||
if 'basic_avg_adr' not in features:
|
||||
features = dict(features) # Convert to dict if row
|
||||
features['basic_avg_adr'] = basic.get('adr', 0) if basic else 0
|
||||
|
||||
comments = WebService.get_comments('player', steam_id)
|
||||
metadata = WebService.get_player_metadata(steam_id)
|
||||
|
||||
# History for table (L2 Source) - Fetch ALL for history table/chart
|
||||
history_asc = StatsService.get_player_trend(steam_id, limit=1000)
|
||||
history = history_asc[::-1] if history_asc else []
|
||||
|
||||
return render_template('players/profile.html', player=player, features=features, comments=comments, metadata=metadata, history=history)
|
||||
|
||||
@bp.route('/comment/<int:comment_id>/like', methods=['POST'])
|
||||
def like_comment(comment_id):
|
||||
WebService.like_comment(comment_id)
|
||||
return jsonify({'success': True})
|
||||
|
||||
@bp.route('/<steam_id>/charts_data')
|
||||
def charts_data(steam_id):
|
||||
# ... (existing code) ...
|
||||
# Trend Data
|
||||
trends = StatsService.get_player_trend(steam_id, limit=1000)
|
||||
|
||||
# Radar Data (Construct from features)
|
||||
features = FeatureService.get_player_features(steam_id)
|
||||
radar_data = {}
|
||||
if features:
|
||||
# Dimensions: STA, BAT, HPS, PTL, T/CT, UTIL
|
||||
radar_data = {
|
||||
'STA': features['basic_avg_rating'] or 0,
|
||||
'BAT': features['bat_avg_duel_win_rate'] or 0,
|
||||
'HPS': features['hps_clutch_win_rate_1v1'] or 0,
|
||||
'PTL': features['ptl_pistol_win_rate'] or 0,
|
||||
'SIDE': features['side_rating_ct'] or 0,
|
||||
'UTIL': features['util_usage_rate'] or 0
|
||||
}
|
||||
|
||||
trend_labels = []
|
||||
trend_values = []
|
||||
for t in trends:
|
||||
dt = datetime.fromtimestamp(t['start_time']) if t['start_time'] else datetime.now()
|
||||
trend_labels.append(dt.strftime('%Y-%m-%d'))
|
||||
trend_values.append(t['rating'])
|
||||
|
||||
return jsonify({
|
||||
'trend': {'labels': trend_labels, 'values': trend_values},
|
||||
'radar': radar_data
|
||||
})
|
||||
|
||||
# --- API for Comparison ---
|
||||
@bp.route('/api/search')
|
||||
def api_search():
|
||||
query = request.args.get('q', '')
|
||||
if len(query) < 2:
|
||||
return jsonify([])
|
||||
|
||||
players, _ = FeatureService.get_players_list(page=1, per_page=10, search=query)
|
||||
# Return minimal data
|
||||
results = [{'steam_id': p['steam_id_64'], 'username': p['username'], 'avatar_url': p['avatar_url']} for p in players]
|
||||
return jsonify(results)
|
||||
|
||||
@bp.route('/api/batch_stats')
|
||||
def api_batch_stats():
|
||||
steam_ids = request.args.get('ids', '').split(',')
|
||||
stats = []
|
||||
for sid in steam_ids:
|
||||
if not sid: continue
|
||||
f = FeatureService.get_player_features(sid)
|
||||
p = StatsService.get_player_info(sid)
|
||||
|
||||
if f and p:
|
||||
stats.append({
|
||||
'username': p['username'],
|
||||
'steam_id': sid,
|
||||
'radar': {
|
||||
'STA': f['basic_avg_rating'] or 0,
|
||||
'BAT': f['bat_avg_duel_win_rate'] or 0,
|
||||
'HPS': f['hps_clutch_win_rate_1v1'] or 0,
|
||||
'PTL': f['ptl_pistol_win_rate'] or 0,
|
||||
'SIDE': f['side_rating_ct'] or 0,
|
||||
'UTIL': f['util_usage_rate'] or 0
|
||||
}
|
||||
})
|
||||
return jsonify(stats)
|
||||
73
web/routes/tactics.py
Normal file
73
web/routes/tactics.py
Normal file
@@ -0,0 +1,73 @@
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from web.services.web_service import WebService
|
||||
from web.services.stats_service import StatsService
|
||||
from web.services.feature_service import FeatureService
|
||||
import json
|
||||
|
||||
bp = Blueprint('tactics', __name__, url_prefix='/tactics')
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
return render_template('tactics/index.html')
|
||||
|
||||
# API: Analyze Lineup
|
||||
@bp.route('/api/analyze', methods=['POST'])
|
||||
def api_analyze():
|
||||
data = request.json
|
||||
steam_ids = data.get('steam_ids', [])
|
||||
|
||||
if not steam_ids:
|
||||
return jsonify({'error': 'No players selected'}), 400
|
||||
|
||||
# 1. Get Basic Info & Stats
|
||||
players = StatsService.get_players_by_ids(steam_ids)
|
||||
player_data = []
|
||||
|
||||
total_rating = 0
|
||||
total_kd = 0
|
||||
total_adr = 0
|
||||
count = 0
|
||||
|
||||
for p in players:
|
||||
p_dict = dict(p)
|
||||
# Fetch L3 features
|
||||
f = FeatureService.get_player_features(p_dict['steam_id_64'])
|
||||
stats = dict(f) if f else {}
|
||||
p_dict['stats'] = stats
|
||||
player_data.append(p_dict)
|
||||
|
||||
if stats:
|
||||
total_rating += stats.get('basic_avg_rating', 0) or 0
|
||||
total_kd += stats.get('basic_avg_kd', 0) or 0
|
||||
total_adr += stats.get('basic_avg_adr', 0) or 0
|
||||
count += 1
|
||||
|
||||
# 2. Shared Matches
|
||||
shared_matches = StatsService.get_shared_matches(steam_ids)
|
||||
|
||||
# 3. Aggregates
|
||||
avg_stats = {
|
||||
'rating': total_rating / count if count else 0,
|
||||
'kd': total_kd / count if count else 0,
|
||||
'adr': total_adr / count if count else 0
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
'players': player_data,
|
||||
'shared_matches': [dict(m) for m in shared_matches],
|
||||
'avg_stats': avg_stats
|
||||
})
|
||||
|
||||
# API: Save Board
|
||||
@bp.route('/save_board', methods=['POST'])
|
||||
def save_board():
|
||||
data = request.json
|
||||
title = data.get('title', 'Untitled Strategy')
|
||||
map_name = data.get('map_name', 'de_mirage')
|
||||
markers = data.get('markers')
|
||||
|
||||
if not markers:
|
||||
return jsonify({'success': False, 'message': 'No markers to save'})
|
||||
|
||||
WebService.save_strategy_board(title, map_name, json.dumps(markers), 'Anonymous')
|
||||
return jsonify({'success': True, 'message': 'Board saved successfully'})
|
||||
225
web/routes/teams.py
Normal file
225
web/routes/teams.py
Normal file
@@ -0,0 +1,225 @@
|
||||
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
|
||||
import json
|
||||
|
||||
bp = Blueprint('teams', __name__, url_prefix='/teams')
|
||||
|
||||
# --- API Endpoints ---
|
||||
@bp.route('/api/search')
|
||||
def api_search():
|
||||
query = request.args.get('q', '').strip() # Strip whitespace
|
||||
print(f"DEBUG: Search Query Received: '{query}'") # Debug Log
|
||||
|
||||
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')
|
||||
|
||||
print(f"DEBUG: Calling StatsService.get_players with search='{query}'")
|
||||
players, total = StatsService.get_players(page=1, per_page=50, search=query, sort_by=sort_by)
|
||||
print(f"DEBUG: Found {len(players)} players (Total: {total})")
|
||||
|
||||
# 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['basic_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)
|
||||
|
||||
print(f"DEBUG: Returning {len(results)} results")
|
||||
return jsonify(results)
|
||||
|
||||
@bp.route('/api/roster', methods=['GET', 'POST'])
|
||||
def api_roster():
|
||||
# Assume single team mode, always operating on ID=1 or the first lineup
|
||||
lineups = WebService.get_lineups()
|
||||
if not lineups:
|
||||
# Auto-create default team if none exists
|
||||
WebService.save_lineup("My Team", "Default Roster", [])
|
||||
lineups = WebService.get_lineups()
|
||||
|
||||
target_team = dict(lineups[0]) # Get the latest one
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
# Pass lineup_id=target_team['id'] to update existing lineup
|
||||
WebService.save_lineup(target_team['name'], target_team['description'], current_ids, lineup_id=target_team['id'])
|
||||
return jsonify({'status': 'success', 'roster': current_ids})
|
||||
|
||||
# GET: Return detailed player info
|
||||
try:
|
||||
print(f"DEBUG: api_roster GET - Target Team: {target_team.get('id')}")
|
||||
p_ids_json = target_team.get('player_ids_json', '[]')
|
||||
p_ids = json.loads(p_ids_json)
|
||||
print(f"DEBUG: Player IDs: {p_ids}")
|
||||
|
||||
players = StatsService.get_players_by_ids(p_ids)
|
||||
print(f"DEBUG: Players fetched: {len(players) if players else 0}")
|
||||
|
||||
# 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:
|
||||
print(f"ERROR: Processing player failed: {inner_e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
|
||||
return jsonify({
|
||||
'status': 'success',
|
||||
'team': dict(target_team), # Ensure target_team is dict too
|
||||
'roster': enriched
|
||||
})
|
||||
except Exception as e:
|
||||
print(f"CRITICAL ERROR in api_roster: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return jsonify({'error': str(e)}), 500
|
||||
|
||||
# --- Views ---
|
||||
@bp.route('/')
|
||||
def index():
|
||||
# Directly render the Clubhouse SPA
|
||||
return render_template('teams/clubhouse.html')
|
||||
|
||||
# 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):
|
||||
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:
|
||||
player_features.append(f)
|
||||
total_rating += f['basic_avg_rating'] or 0
|
||||
total_kd += f['basic_avg_kd'] or 0
|
||||
total_kast += f['basic_avg_kast'] or 0
|
||||
|
||||
# Radar accumulation
|
||||
r_totals['STA'] += f['basic_avg_rating'] or 0
|
||||
r_totals['BAT'] += f['bat_avg_duel_win_rate'] or 0
|
||||
r_totals['HPS'] += f['hps_clutch_win_rate_1v1'] or 0
|
||||
r_totals['PTL'] += f['ptl_pistol_win_rate'] or 0
|
||||
r_totals['SIDE'] += f['side_rating_ct'] or 0
|
||||
r_totals['UTIL'] += f['util_usage_rate'] or 0
|
||||
else:
|
||||
player_features.append(None)
|
||||
|
||||
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)
|
||||
32
web/routes/wiki.py
Normal file
32
web/routes/wiki.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session
|
||||
from web.services.web_service import WebService
|
||||
from web.auth import admin_required
|
||||
|
||||
bp = Blueprint('wiki', __name__, url_prefix='/wiki')
|
||||
|
||||
@bp.route('/')
|
||||
def index():
|
||||
pages = WebService.get_all_wiki_pages()
|
||||
return render_template('wiki/index.html', pages=pages)
|
||||
|
||||
@bp.route('/view/<path:page_path>')
|
||||
def view(page_path):
|
||||
page = WebService.get_wiki_page(page_path)
|
||||
if not page:
|
||||
# If admin, offer to create
|
||||
if session.get('is_admin'):
|
||||
return redirect(url_for('wiki.edit', page_path=page_path))
|
||||
return "Page not found", 404
|
||||
return render_template('wiki/view.html', page=page)
|
||||
|
||||
@bp.route('/edit/<path:page_path>', methods=['GET', 'POST'])
|
||||
@admin_required
|
||||
def edit(page_path):
|
||||
if request.method == 'POST':
|
||||
title = request.form.get('title')
|
||||
content = request.form.get('content')
|
||||
WebService.save_wiki_page(page_path, title, content, 'admin')
|
||||
return redirect(url_for('wiki.view', page_path=page_path))
|
||||
|
||||
page = WebService.get_wiki_page(page_path)
|
||||
return render_template('wiki/edit.html', page=page, page_path=page_path)
|
||||
Reference in New Issue
Block a user