from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for from flask_login import login_required from app.guides import GUIDES from app.models import ApplyLog, Profile, Setting, SystemState from app.models import utcnow from app.services.applier import ( APPLY_MODE_FINAL, APPLY_MODE_TEST, apply_profile, confirm_apply, emergency_restore_last_apply, restore_initial, rollback_apply, ) from app.services.backup import get_latest_apply_backup, list_backups, restore_backup apply_bp = Blueprint("apply", __name__, url_prefix="/apply") @apply_bp.route("/") @login_required def index(): profiles = Profile.query.order_by(Profile.name).all() state = SystemState.query.first() pending = None if state and state.pending_apply_log_id: pending = ApplyLog.query.get(state.pending_apply_log_id) test_minutes = int(Setting.get("test_rollback_minutes", "2") or "2") recent_logs = ApplyLog.query.order_by(ApplyLog.started_at.desc()).limit(10).all() return render_template( "apply/index.html", profiles=profiles, pending=pending, recent_logs=recent_logs, test_minutes=test_minutes, guide=GUIDES.get("profiles", {}), ) def _handle_apply(profile_id: int, mode: str): profile = Profile.query.get_or_404(profile_id) state = SystemState.query.first() if state and state.pending_apply_log_id: flash("یک تست در انتظار تأیید است. ابتدا آن را تأیید یا rollback کنید.", "warning") return redirect(url_for("apply.status", log_id=state.pending_apply_log_id)) log = apply_profile(profile, mode=mode) if log.status == "failed": flash("اعمال پروفایل با خطا مواجه شد. تنظیمات قبلی خودکار بازگردانده شد.", "danger") elif mode == APPLY_MODE_FINAL: flash("پروفایل به‌صورت نهایی اعمال و ثبت شد.", "success") return redirect(url_for("main.dashboard")) else: test_minutes = log.get_details().get("rollback_minutes", 2) flash( f"تست موقت اعمال شد. {test_minutes} دقیقه برای بررسی دارید — در صورت عدم تأیید، rollback خودکار انجام می‌شود.", "warning", ) return redirect(url_for("apply.status", log_id=log.id)) @apply_bp.route("/profile//test", methods=["POST"]) @login_required def apply_test(profile_id): return _handle_apply(profile_id, APPLY_MODE_TEST) @apply_bp.route("/profile//final", methods=["POST"]) @login_required def apply_final(profile_id): return _handle_apply(profile_id, APPLY_MODE_FINAL) @apply_bp.route("/status/") @login_required def status(log_id): log = ApplyLog.query.get_or_404(log_id) details = log.get_details() remaining_seconds = 0 if log.rollback_at and not log.confirmed: remaining_seconds = max(0, int((log.rollback_at - utcnow()).total_seconds())) return render_template( "apply/status.html", log=log, details=details, remaining_seconds=remaining_seconds, apply_mode=details.get("mode", APPLY_MODE_TEST), ) @apply_bp.route("/confirm/", methods=["POST"]) @login_required def confirm(log_id): confirm_apply(log_id) flash("تست موفق بود — تغییرات به‌صورت نهایی ثبت شد.", "success") return redirect(url_for("main.dashboard")) @apply_bp.route("/rollback/", methods=["POST"]) @login_required def rollback(log_id): result = rollback_apply(log_id) if result.get("success"): flash("تنظیمات قبلی بازگردانده شد.", "info") else: flash(result.get("error", "خطا در rollback"), "danger") return redirect(url_for("main.dashboard")) @apply_bp.route("/api/pending/") @login_required def pending_api(log_id): log = ApplyLog.query.get_or_404(log_id) remaining = 0 if log.rollback_at and not log.confirmed: remaining = max(0, int((log.rollback_at - utcnow()).total_seconds())) return jsonify( { "status": log.status, "confirmed": log.confirmed, "remaining_seconds": remaining, } ) restore_bp = Blueprint("restore", __name__, url_prefix="/restore") @restore_bp.route("/") @login_required def index(): backups = list_backups() state = SystemState.query.first() latest_apply = get_latest_apply_backup() return render_template( "restore/index.html", backups=backups, state=state, latest_apply_backup=str(latest_apply) if latest_apply else None, guide=GUIDES.get("restore", {}), ) @restore_bp.route("/emergency", methods=["POST"]) @login_required def emergency_restore_view(): result = emergency_restore_last_apply() if result.get("success"): flash(f"بازگردانی فوری انجام شد از: {result.get('backup_path', '')}", "success") else: flash(result.get("error", "خطا در بازگردانی"), "danger") return redirect(url_for("restore.index")) @restore_bp.route("/initial", methods=["POST"]) @login_required def restore_initial_view(): result = restore_initial() if result.get("success"): flash("تنظیمات اولیه بازگردانده شد.", "success") else: flash(result.get("error", "خطا"), "danger") return redirect(url_for("restore.index")) @restore_bp.route("/backup", methods=["POST"]) @login_required def restore_backup_view(): path = request.form.get("backup_path", "").strip() if not path: flash("مسیر backup انتخاب نشده.", "danger") return redirect(url_for("restore.index")) result = restore_backup(path) if result.get("success"): flash("backup بازگردانده شد.", "success") else: flash(result.get("error", "خطا"), "danger") return redirect(url_for("restore.index")) settings_bp = Blueprint("settings", __name__, url_prefix="/settings") @settings_bp.route("/", methods=["GET", "POST"]) @login_required def index(): if request.method == "POST": for key in ( "auto_switch_dns", "auto_switch_apt", "auto_switch_docker", "auto_switch_github", "auto_switch_pip", "auto_switch_npm", ): Setting.set(key, "true" if request.form.get(key) == "on" else "false") Setting.set("rollback_minutes", request.form.get("rollback_minutes", "15")) Setting.set("test_rollback_minutes", request.form.get("test_rollback_minutes", "2")) Setting.set("auto_test_interval_minutes", request.form.get("auto_test_interval_minutes", "30")) Setting.set("max_switches_per_day", request.form.get("max_switches_per_day", "3")) Setting.set( "rollback_on_all_fail", "true" if request.form.get("rollback_on_all_fail") == "on" else "false", ) flash("تنظیمات ذخیره شد.", "success") return redirect(url_for("settings.index")) settings = {key: Setting.get(key) for key in ( "auto_switch_dns", "auto_switch_apt", "auto_switch_docker", "auto_switch_github", "auto_switch_pip", "auto_switch_npm", "rollback_minutes", "test_rollback_minutes", "auto_test_interval_minutes", "max_switches_per_day", "rollback_on_all_fail", )} return render_template("settings/index.html", settings=settings, guide=GUIDES.get("settings", {}))