Fix Docker apply crash and add safe profile test flow

Remove invalid _mirror_manager from daemon.json that prevented Docker from starting. Add 2-minute test apply with auto-rollback, final apply confirmation, emergency-restore CLI/UI, and post-restart Docker health checks.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-06-10 20:20:10 +03:30
parent 1c9d647787
commit 63d0699cdd
20 changed files with 635 additions and 324 deletions
+53 -12
View File
@@ -1,12 +1,19 @@
from flask import Blueprint, flash, jsonify, redirect, render_template, request, url_for
from flask_login import login_required
from app.extensions import db
from app.guides import GUIDES
from app.models import ApplyLog, Profile, Setting, SystemState
from app.models import utcnow
from app.services.applier import apply_profile, confirm_apply, get_system_status, restore_initial, rollback_apply
from app.services.backup import list_backups, restore_backup
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")
@@ -20,33 +27,52 @@ def index():
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", {}),
)
@apply_bp.route("/profile/<int:profile_id>", methods=["POST"])
@login_required
def apply(profile_id):
def _handle_apply(profile_id: int, mode: str):
profile = Profile.query.get_or_404(profile_id)
rollback_minutes = int(request.form.get("rollback_minutes", Setting.get("rollback_minutes", "15") or "15"))
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, rollback_minutes=rollback_minutes)
log = apply_profile(profile, mode=mode)
if log.status == "failed":
flash("اعمال پروفایل با خطا مواجه شد. جزئیات را بررسی کنید.", "danger")
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"پروفایل اعمال شد. {rollback_minutes} دقیقه برای تست دارید — سپس rollback خودکار انجام می‌شود.",
f"تست موقت اعمال شد. {test_minutes} دقیقه برای بررسی دارید — در صورت عدم تأیید، rollback خودکار انجام می‌شود.",
"warning",
)
return redirect(url_for("apply.status", log_id=log.id))
@apply_bp.route("/profile/<int:profile_id>/test", methods=["POST"])
@login_required
def apply_test(profile_id):
return _handle_apply(profile_id, APPLY_MODE_TEST)
@apply_bp.route("/profile/<int:profile_id>/final", methods=["POST"])
@login_required
def apply_final(profile_id):
return _handle_apply(profile_id, APPLY_MODE_FINAL)
@apply_bp.route("/status/<int:log_id>")
@login_required
def status(log_id):
@@ -60,6 +86,7 @@ def status(log_id):
log=log,
details=details,
remaining_seconds=remaining_seconds,
apply_mode=details.get("mode", APPLY_MODE_TEST),
)
@@ -67,7 +94,7 @@ def status(log_id):
@login_required
def confirm(log_id):
confirm_apply(log_id)
flash("تغییرات تأیید و ثبت شد.", "success")
flash("تست موفق بود — تغییرات به‌صورت نهایی ثبت شد.", "success")
return redirect(url_for("main.dashboard"))
@@ -106,14 +133,27 @@ restore_bp = Blueprint("restore", __name__, url_prefix="/restore")
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():
@@ -158,6 +198,7 @@ def index():
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(
@@ -170,7 +211,7 @@ def 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", "auto_test_interval_minutes",
"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", {}))