from flask import ( Blueprint, flash, jsonify, redirect, render_template, request, session, url_for, ) from web.config import Config from web.auth import admin_required, csrf_protected, validate_csrf from web.database import query_db from web.services.admin_service import AdminService from web.services.etl_service import EtlService import hmac import time bp = Blueprint('admin', __name__, url_prefix='/admin') @bp.route('/login', methods=['GET', 'POST']) def login(): if request.method == 'POST': validate_csrf() token = request.form.get('token') or '' if hmac.compare_digest(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', **AdminService.get_overview(), ) @bp.route('/data-integrity') @admin_required def data_integrity(): from web.services.integrity_service import IntegrityService report = IntegrityService.build_report() if request.args.get('format') == 'json': return jsonify(report) return render_template('admin/data_integrity.html', report=report) @bp.route('/trigger_etl', methods=['POST']) @admin_required @csrf_protected def trigger_etl(): from database.job_store import JobStore store = JobStore(Config.DB_WEB_PATH) job_id = store.create_job( 'manual_pipeline', created_by='admin', ) try: pid = EtlService.start_pipeline(job_id) except Exception as exc: store.finish_job(job_id, False, str(exc), 0) return jsonify({'success': False, 'error': str(exc)}), 500 return jsonify({'success': True, 'job_id': job_id, 'pid': pid}), 202 @bp.route('/import-match', methods=['GET', 'POST']) @admin_required def import_match(): from database.job_store import JobStore from web.services.import_service import ( DuplicateMatchError, ImportValidationError, MatchImportService, ) store = JobStore(Config.DB_WEB_PATH) if request.method == 'POST': validate_csrf() upload = request.files.get('capture') if not upload or not upload.filename: flash('请选择 iframe_network.json 文件。', 'error') return redirect(url_for('admin.import_match')) prepared = None try: prepared = MatchImportService.prepare_import( upload.read(), upload.filename, created_by='admin', replace=request.form.get('replace') == '1', ) EtlService.start_pipeline( prepared['job_id'], match_id=prepared['match_id'], replace=prepared['replace'], ) flash( f"比赛 {prepared['match_id']} 已进入导入队列。", 'success', ) return redirect(url_for( 'admin.jobs', job_id=prepared['job_id'], )) except (DuplicateMatchError, ImportValidationError) as exc: flash(str(exc), 'warning') except Exception as exc: if prepared: store.finish_job( prepared['job_id'], False, f'Failed to start pipeline: {exc}', 0, ) flash(f'启动导入失败:{exc}', 'error') return render_template( 'admin/import_match.html', jobs=store.list_jobs(5), ) @bp.route('/jobs') @admin_required def jobs(): from database.job_store import JobStore status = request.args.get('status') or None job_type = request.args.get('type') or None selected_job_id = request.args.get('job_id', type=int) data = AdminService.get_jobs(status, job_type) data['selected_job'] = ( JobStore(Config.DB_WEB_PATH).get_job(selected_job_id) if selected_job_id else None ) return render_template('admin/jobs.html', **data) @bp.route('/system') @admin_required def system(): return render_template( 'admin/system.html', **AdminService.get_system_status(), ) @bp.route('/api/jobs/') @admin_required def api_job(job_id): from database.job_store import JobStore job = JobStore(Config.DB_WEB_PATH).get_job(job_id) if not job: return jsonify({'error': 'Job not found'}), 404 return jsonify(job) @bp.route('/sql', methods=['GET', 'POST']) @admin_required def sql_runner(): result = None error = None query = "" db_name = request.args.get('db_name', 'l2') if db_name not in {'l2', 'l3', 'web'}: db_name = 'l2' duration_ms = None row_count = None if request.method == 'POST': validate_csrf() query = (request.form.get('query') or '').strip() db_name = request.form.get('db_name', 'l2') statement = query.rstrip(';').strip() if db_name not in {'l2', 'l3', 'web'}: error = "Unknown database." elif not statement.upper().startswith('SELECT '): error = "Only SELECT queries are allowed." elif ';' in statement: error = "Only one SQL statement is allowed." else: try: query = statement if 'LIMIT' not in statement.upper(): query = f"{statement} LIMIT 50" started = time.perf_counter() rows = query_db(db_name, query) duration_ms = (time.perf_counter() - started) * 1000 row_count = len(rows) if rows: columns = rows[0].keys() result = {'columns': columns, 'rows': rows} else: result = {'columns': [], 'rows': []} except Exception as e: error = str(e) try: catalog = AdminService.get_database_catalog(db_name) except ValueError: catalog = [] return render_template( 'admin/sql.html', result=result, error=error, query=query, db_name=db_name, catalog=catalog, duration_ms=duration_ms, row_count=row_count, )