from flask import Blueprint, render_template, current_app, jsonify, request, redirect, url_for, flash from models import db, User, Model, Vote, EloHistory, ModelType from auth import admin_required from sqlalchemy import func, desc, extract from datetime import datetime, timedelta import json import os admin = Blueprint("admin", __name__, url_prefix="/admin") @admin.route("/") @admin_required def index(): """Admin dashboard homepage""" # Get count statistics stats = { "total_users": User.query.count(), "total_votes": Vote.query.count(), "tts_votes": Vote.query.filter_by(model_type=ModelType.TTS).count(), "conversational_votes": Vote.query.filter_by(model_type=ModelType.CONVERSATIONAL).count(), "tts_models": Model.query.filter_by(model_type=ModelType.TTS).count(), "conversational_models": Model.query.filter_by(model_type=ModelType.CONVERSATIONAL).count(), } # Get recent votes recent_votes = Vote.query.order_by(Vote.vote_date.desc()).limit(10).all() # Get recent users recent_users = User.query.order_by(User.join_date.desc()).limit(10).all() # Get daily votes for the past 30 days thirty_days_ago = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=30) daily_votes = db.session.query( func.date(Vote.vote_date).label('date'), func.count().label('count') ).filter(Vote.vote_date >= thirty_days_ago).group_by( func.date(Vote.vote_date) ).order_by(func.date(Vote.vote_date)).all() # Generate a complete list of dates for the past 30 days date_list = [] current_date = datetime.utcnow() for i in range(30, -1, -1): date_list.append((current_date - timedelta(days=i)).date()) # Create a dictionary with actual vote counts vote_counts = {day.date: day.count for day in daily_votes} # Build complete datasets including days with zero votes formatted_dates = [date.strftime("%Y-%m-%d") for date in date_list] vote_counts_list = [vote_counts.get(date, 0) for date in date_list] daily_votes_data = { "labels": formatted_dates, "counts": vote_counts_list } # Get top models top_tts_models = Model.query.filter_by( model_type=ModelType.TTS ).order_by(Model.current_elo.desc()).limit(5).all() top_conversational_models = Model.query.filter_by( model_type=ModelType.CONVERSATIONAL ).order_by(Model.current_elo.desc()).limit(5).all() return render_template( "admin/index.html", stats=stats, recent_votes=recent_votes, recent_users=recent_users, daily_votes_data=json.dumps(daily_votes_data), top_tts_models=top_tts_models, top_conversational_models=top_conversational_models ) @admin.route("/models") @admin_required def models(): """Manage models""" tts_models = Model.query.filter_by(model_type=ModelType.TTS).order_by(Model.name).all() conversational_models = Model.query.filter_by(model_type=ModelType.CONVERSATIONAL).order_by(Model.name).all() return render_template( "admin/models.html", tts_models=tts_models, conversational_models=conversational_models ) @admin.route("/model/", methods=["GET", "POST"]) @admin_required def edit_model(model_id): """Edit a model""" model = Model.query.get_or_404(model_id) if request.method == "POST": model.name = request.form.get("name") model.is_active = "is_active" in request.form model.is_open = "is_open" in request.form model.model_url = request.form.get("model_url") db.session.commit() flash(f"Model '{model.name}' updated successfully", "success") return redirect(url_for("admin.models")) return render_template("admin/edit_model.html", model=model) @admin.route("/users") @admin_required def users(): """Manage users""" users = User.query.order_by(User.username).all() admin_users = os.getenv("ADMIN_USERS", "").split(",") admin_users = [username.strip() for username in admin_users] return render_template("admin/users.html", users=users, admin_users=admin_users) @admin.route("/user/") @admin_required def user_detail(user_id): """View user details""" user = User.query.get_or_404(user_id) # Get user votes recent_votes = Vote.query.filter_by(user_id=user_id).order_by(Vote.vote_date.desc()).limit(20).all() # Get vote statistics tts_votes = Vote.query.filter_by(user_id=user_id, model_type=ModelType.TTS).count() conversational_votes = Vote.query.filter_by(user_id=user_id, model_type=ModelType.CONVERSATIONAL).count() # Get favorite models (most chosen) favorite_models = db.session.query( Vote.model_chosen, Model.name, func.count().label('count') ).join( Model, Vote.model_chosen == Model.id ).filter( Vote.user_id == user_id ).group_by( Vote.model_chosen, Model.name ).order_by( desc('count') ).limit(5).all() return render_template( "admin/user_detail.html", user=user, recent_votes=recent_votes, tts_votes=tts_votes, conversational_votes=conversational_votes, favorite_models=favorite_models, total_votes=tts_votes + conversational_votes ) @admin.route("/votes") @admin_required def votes(): """View recent votes""" page = request.args.get('page', 1, type=int) per_page = 50 # Get votes with pagination votes_pagination = Vote.query.order_by( Vote.vote_date.desc() ).paginate(page=page, per_page=per_page) return render_template( "admin/votes.html", votes=votes_pagination.items, pagination=votes_pagination ) @admin.route("/statistics") @admin_required def statistics(): """View detailed statistics""" # Get daily votes for the past 30 days by model type thirty_days_ago = datetime.utcnow().replace(hour=0, minute=0, second=0, microsecond=0) - timedelta(days=30) tts_daily_votes = db.session.query( func.date(Vote.vote_date).label('date'), func.count().label('count') ).filter( Vote.vote_date >= thirty_days_ago, Vote.model_type == ModelType.TTS ).group_by( func.date(Vote.vote_date) ).order_by(func.date(Vote.vote_date)).all() conv_daily_votes = db.session.query( func.date(Vote.vote_date).label('date'), func.count().label('count') ).filter( Vote.vote_date >= thirty_days_ago, Vote.model_type == ModelType.CONVERSATIONAL ).group_by( func.date(Vote.vote_date) ).order_by(func.date(Vote.vote_date)).all() # Monthly new users monthly_users = db.session.query( extract('year', User.join_date).label('year'), extract('month', User.join_date).label('month'), func.count().label('count') ).group_by( 'year', 'month' ).order_by('year', 'month').all() # Generate a complete list of dates for the past 30 days date_list = [] current_date = datetime.utcnow() for i in range(30, -1, -1): date_list.append((current_date - timedelta(days=i)).date()) # Create dictionaries with actual vote counts tts_vote_counts = {day.date: day.count for day in tts_daily_votes} conv_vote_counts = {day.date: day.count for day in conv_daily_votes} # Format dates consistently for charts formatted_dates = [date.strftime("%Y-%m-%d") for date in date_list] # Build complete datasets including days with zero votes tts_counts = [tts_vote_counts.get(date, 0) for date in date_list] conv_counts = [conv_vote_counts.get(date, 0) for date in date_list] # Generate all month/year combinations for the past 12 months current_date = datetime.utcnow() month_list = [] for i in range(11, -1, -1): past_date = current_date - timedelta(days=i*30) # Approximate month_list.append((past_date.year, past_date.month)) # Create a dictionary with actual user counts user_counts = {(record.year, record.month): record.count for record in monthly_users} # Build complete monthly datasets including months with zero new users monthly_labels = [f"{month}/{year}" for year, month in month_list] monthly_counts = [user_counts.get((year, month), 0) for year, month in month_list] # Model performance over time top_models = Model.query.order_by(Model.match_count.desc()).limit(5).all() # Get first and last timestamp to create a consistent timeline earliest = datetime.utcnow() - timedelta(days=30) # Default to 30 days ago latest = datetime.utcnow() # Default to now # Find actual earliest and latest timestamps across all models has_elo_history = False for model in top_models: first = EloHistory.query.filter_by(model_id=model.id).order_by(EloHistory.timestamp).first() last = EloHistory.query.filter_by(model_id=model.id).order_by(EloHistory.timestamp.desc()).first() if first and last: has_elo_history = True if first.timestamp < earliest: earliest = first.timestamp if last.timestamp > latest: latest = last.timestamp # If no history was found, use a default range of the last 30 days if not has_elo_history: earliest = datetime.utcnow() - timedelta(days=30) latest = datetime.utcnow() # Make sure the date range is valid (earliest before latest) if earliest > latest: earliest = latest - timedelta(days=30) # Generate a list of dates for the ELO history timeline # Using 1-day intervals for a smoother chart elo_dates = [] current = earliest while current <= latest: elo_dates.append(current.date()) current += timedelta(days=1) # Format dates consistently formatted_elo_dates = [date.strftime("%Y-%m-%d") for date in elo_dates] model_history = {} # Initialize empty data for all top models for model in top_models: model_history[model.name] = { "timestamps": formatted_elo_dates, "scores": [None] * len(formatted_elo_dates) # Initialize with None values } history = EloHistory.query.filter_by( model_id=model.id ).order_by(EloHistory.timestamp).all() if history: # Create a dictionary mapping dates to scores history_dict = {} for h in history: date_key = h.timestamp.date().strftime("%Y-%m-%d") history_dict[date_key] = h.elo_score # Fill in missing dates with the previous score last_score = model.current_elo # Default to current ELO if no history scores = [] for date in formatted_elo_dates: if date in history_dict: last_score = history_dict[date] scores.append(last_score) model_history[model.name]["scores"] = scores else: # If no history, use the current Elo for all dates model_history[model.name]["scores"] = [model.current_elo] * len(formatted_elo_dates) chart_data = { "dailyVotes": { "labels": formatted_dates, "ttsCounts": tts_counts, "convCounts": conv_counts }, "monthlyUsers": { "labels": monthly_labels, "counts": monthly_counts }, "modelHistory": model_history } return render_template( "admin/statistics.html", chart_data=json.dumps(chart_data) ) @admin.route("/activity") @admin_required def activity(): """View recent text generations""" # Check if we have any active sessions from app.py tts_session_count = 0 conversational_session_count = 0 # Access global variables from app.py through current_app if hasattr(current_app, 'tts_sessions'): tts_session_count = len(current_app.tts_sessions) else: # Try to access through app module from app import tts_sessions tts_session_count = len(tts_sessions) if hasattr(current_app, 'conversational_sessions'): conversational_session_count = len(current_app.conversational_sessions) else: # Try to access through app module from app import conversational_sessions conversational_session_count = len(conversational_sessions) # Get recent votes which represent completed generations recent_tts_votes = Vote.query.filter_by( model_type=ModelType.TTS ).order_by(Vote.vote_date.desc()).limit(20).all() recent_conv_votes = Vote.query.filter_by( model_type=ModelType.CONVERSATIONAL ).order_by(Vote.vote_date.desc()).limit(20).all() # Get votes per hour for the last 24 hours current_time = datetime.utcnow() last_24h = current_time.replace(minute=0, second=0, microsecond=0) - timedelta(hours=24) # Use SQLite-compatible date formatting hourly_votes = db.session.query( func.strftime('%Y-%m-%d %H:00', Vote.vote_date).label('hour'), func.count().label('count') ).filter( Vote.vote_date >= last_24h ).group_by('hour').order_by('hour').all() # Generate all hours for the past 24 hours with correct hour formatting hour_list = [] for i in range(24, -1, -1): # Calculate the hour time and truncate to hour hour_time = current_time - timedelta(hours=i) hour_time = hour_time.replace(minute=0, second=0, microsecond=0) hour_list.append(hour_time.strftime('%Y-%m-%d %H:00')) # Create a dictionary with actual vote counts vote_counts = {hour.hour: hour.count for hour in hourly_votes} # Build complete hourly datasets including hours with zero votes hourly_data = { "labels": hour_list, "counts": [vote_counts.get(hour, 0) for hour in hour_list] } return render_template( "admin/activity.html", tts_session_count=tts_session_count, conversational_session_count=conversational_session_count, recent_tts_votes=recent_tts_votes, recent_conv_votes=recent_conv_votes, hourly_data=json.dumps(hourly_data) )