2.0.0 Alpha: Data Refinery
This commit is contained in:
+111
-22
@@ -1,16 +1,17 @@
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash
|
||||
from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify
|
||||
from web.config import Config
|
||||
from web.auth import admin_required
|
||||
from web.database import query_db
|
||||
import os
|
||||
from web.services.etl_service import EtlService
|
||||
import hmac
|
||||
|
||||
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:
|
||||
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:
|
||||
@@ -27,19 +28,104 @@ def logout():
|
||||
def dashboard():
|
||||
return render_template('admin/dashboard.html')
|
||||
|
||||
from web.services.etl_service import EtlService
|
||||
@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
|
||||
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
|
||||
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':
|
||||
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.import_match',
|
||||
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')
|
||||
|
||||
selected_job = None
|
||||
selected_job_id = request.args.get('job_id', type=int)
|
||||
if selected_job_id:
|
||||
selected_job = store.get_job(selected_job_id)
|
||||
return render_template(
|
||||
'admin/import_match.html',
|
||||
jobs=store.list_jobs(30),
|
||||
selected_job=selected_job,
|
||||
)
|
||||
|
||||
|
||||
@bp.route('/api/jobs/<int:job_id>')
|
||||
@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
|
||||
@@ -50,18 +136,21 @@ def sql_runner():
|
||||
db_name = "l2"
|
||||
|
||||
if request.method == 'POST':
|
||||
query = request.form.get('query')
|
||||
query = (request.form.get('query') or '').strip()
|
||||
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."
|
||||
|
||||
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:
|
||||
# Enforce limit if not present
|
||||
if 'LIMIT' not in query.upper():
|
||||
query += " LIMIT 50"
|
||||
query = statement
|
||||
if 'LIMIT' not in statement.upper():
|
||||
query = f"{statement} LIMIT 50"
|
||||
|
||||
rows = query_db(db_name, query)
|
||||
if rows:
|
||||
|
||||
Reference in New Issue
Block a user