Операции, прерванные перезапуском панели, закрываются при старте; снимки: исправления по ревью

- core.interrupt_orphaned_operations() при запуске панели, active_operations_for() по всем задачам
- снимки: имена с пробелами, снимки вне панели без кнопок, --force только на «revert requires force»,
  память на паузе учитывается, защита от двойного нажатия, надпись «Создаём снимок…»

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
Claude
2026-09-25 15:05:49 +00:00
родитель 2b7223f311
Коммит eb7b71f478
6 изменённых файлов: 96 добавлений и 10 удалений
+22
Просмотреть файл
@@ -122,3 +122,25 @@ def test_ufw_rules_follow_forwards(data_dirs, monkeypatch):
assert find(calls, "ufw", "delete", "allow", "2222/tcp")
assert find(calls, "ufw", "route", "delete", "allow", "in", "on", "eth0")
assert json.loads(network_core.UFW_STATE_FILE.read_text()) == []
# ---------------------------------------------------------------- operations orphaned by a restart
def test_orphaned_operations_are_closed_on_startup(data_dirs):
import core
running = core.new_operation("snapshot_create", "Снимок web01", vm_name="web01")
core.update_operation(running, status="running")
done = core.new_operation("backup", "Копия web01", vm_name="web01")
core.finish_operation(done, True, "Готово")
assert core.active_operations_for("web01")
assert core.interrupt_orphaned_operations() == 1
assert core.read_operation(running["id"])["status"] == "error" and core.read_operation(running["id"])["interrupted"]
assert core.read_operation(done["id"])["status"] == "success"
assert core.active_operations_for("web01") == []
def test_snapshot_names_with_spaces_are_listed():
from features import snapshots
rows = snapshots.parse_snapshot_list(" Name Creation Time State\n------\n before update 2026-09-25 10:12:03 +0000 shutoff\n clean 2026-09-10 12:05:00 +0000 running\n")
assert [row["name"] for row in rows] == ["before update", "clean"]
+2 -1
Просмотреть файл
@@ -273,7 +273,8 @@ def test_delete_snapshot_error(logged_in, data_dirs, recorder, monkeypatch):
def test_snapshot_routes_validate_names(logged_in, data_dirs, recorder):
assert logged_in.post("/vm/web01/snapshots/-bad/revert", follow_redirects=False).status_code == 400
bad = logged_in.post("/vm/web01/snapshots/-bad/revert", follow_redirects=False)
assert bad.status_code == 303 and "snapshot_error=" in bad.headers["location"]
assert logged_in.post("/vm/web01/snapshots/..%2Fx/delete", follow_redirects=False).status_code in (400, 404)
assert logged_in.post("/vm/-bad/snapshots/create", data={}, follow_redirects=False).status_code == 400
assert not find(recorder, "virsh", "snapshot-delete") and not find(recorder, "virsh", "snapshot-revert")