From c4cebd916ba1ecd5c65e15100e0fc964958dc4d9 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 25 Sep 2026 07:01:30 +0000 Subject: [PATCH] =?UTF-8?q?=D0=A6=D0=B5=D0=BD=D1=82=D1=80=20=D0=BE=D0=B1?= =?UTF-8?q?=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B1=D0=BE?= =?UTF-8?q?=D0=BB=D1=8C=D1=88=D0=B5=20=D0=BD=D0=B5=20=D0=BF=D1=80=D0=B5?= =?UTF-8?q?=D0=B4=D0=BB=D0=B0=D0=B3=D0=B0=D0=B5=D1=82=20=D0=BE=D1=82=D0=BA?= =?UTF-8?q?=D0=B0=D1=82=20=D0=BD=D0=B0=20=D1=81=D1=82=D0=B0=D1=80=D1=83?= =?UTF-8?q?=D1=8E=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Раньше обновление считалось доступным при любом расхождении локального коммита и origin/main. Нода со сборкой новее main (например, из ISO-образа ветки) получала предложение «обновиться» на старую версию, а ночное автообновление откатывало её само. Теперь обновление предлагается и ставится, только если локальный коммит — предок удалённого. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg --- scripts/auto_update_check.sh | 5 + web/static/knowledge.html | 148 ------------------------- web/static/themes.css | 205 ----------------------------------- web/templates/_sidebar.html | 22 ---- web/update_core.py | 7 +- 5 files changed, 11 insertions(+), 376 deletions(-) delete mode 100644 web/static/knowledge.html delete mode 100644 web/static/themes.css delete mode 100644 web/templates/_sidebar.html diff --git a/scripts/auto_update_check.sh b/scripts/auto_update_check.sh index 03ec864..c01c282 100644 --- a/scripts/auto_update_check.sh +++ b/scripts/auto_update_check.sh @@ -96,6 +96,11 @@ diag_network() { write_state "idle" "Автообновление: новых обновлений нет" exit 0 fi + if [ -n "$latest" ] && [ "$current" != "unknown" ] && ! git merge-base --is-ancestor "$current" "$latest" 2>/dev/null; then + log "local build ${current:0:12} is not behind ${REMOTE}/${BRANCH} (${latest:0:12}); skip to avoid a downgrade" + write_state "idle" "Автообновление: установленная сборка новее или отличается от ${BRANCH}, обновление пропущено" + exit 0 + fi log "update may be available or current commit unknown: ${current:0:12} -> ${latest:0:12}" else log "git fetch failed; diagnostics and apply ZIP fallback will be used" diff --git a/web/static/knowledge.html b/web/static/knowledge.html deleted file mode 100644 index ff777d8..0000000 --- a/web/static/knowledge.html +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - База знаний — Virtuality - - - -
- - -
-
-
-
База знаний
-
Документация Virtuality, боевые заметки, сетевые схемы и чек-листы
-
- -
- -
-
Фокус
VM / сеть / SSL
-
Статус
Наполняется
-
Правило
SSL-ready
-
Версия раздела
0.8.59
-
- -
-

Главное правило: SSL-ready by default

Let’s Encrypt / HTTP-01
-
При настройке VM и пробросов Virtuality должна сразу учитывать выпуск SSL-сертификатов. Для Let’s Encrypt критичен внешний доступ к HTTP-01 challenge на TCP 80.
-
-
DNSA-запись домена должна указывать на реальный внешний IP сервера или роутера.
-
WAN 80/443Порты не должны быть заняты веб-интерфейсом роутера, FastPanel на хосте или другой службой.
-
FirewallKeenetic, внешний firewall и UFW/nftables должны пропускать TCP 80 и TCP 443.
-
Проверка снаружиПеред certbot надо проверять curl с мобильной сети или внешнего хоста, а не только из локальной Wi-Fi сети.
-
-
curl -I http://domain.ru
-
-# Хорошо: ответил nginx, допустимы 200 / 301 / 404
-# Плохо: timeout, connection refused или ответ админки роутера
-
- -
-
-

Bridge VM

br0 / LAN node
-

Если VM подключена к bridge и имеет полноценный LAN IP, самый чистый путь — пробрасывать WAN-порты роутера напрямую на IP VM.

-
WAN 80  → IP_VM:80
-WAN 443 → IP_VM:443
-
Для сайтов и FastPanel внутри bridge VM этот вариант проще и надёжнее: меньше NAT-слоёв, меньше магии, меньше боли.
-
- -
-

NAT VM

Virtuality DNAT
-

Если VM находится во внутренней NAT-сети Virtuality, внешний роутер должен вести трафик на хост Virtuality, а дальше Virtuality DNAT отправит его в VM.

-
WAN 80/443 → HOST:80/443
-HOST DNAT  → IP_VM:80/443
-
Для NAT-сценария правила Virtuality должны быть применены и проверены через /network и tcpdump.
-
-
- -
-

Ключевые директории

filesystem
- - - - - - - - - - - - -
ПутьНазначение
/opt/virtuality/sourceИсходники GitHub-репозитория Virtuality.
/opt/virtuality/webУстановленная web-панель, которую запускает systemd.
/opt/virtuality/venvPython virtualenv панели.
/var/lib/virtuality/isoISO-образы для установки VM.
/var/lib/virtuality/imagesqcow2-диски виртуальных машин.
/var/lib/virtuality/disk-imagesЗагруженные готовые .img/.raw/.qcow2 образы и распакованные архивы.
/var/lib/virtuality/networkПравила пробросов портов и ручные IP VM.
/var/log/virtualityЛоги установки, операций и диагностики.
-
- -
-
-

Обновление панели

GitHub → install
-
cd /opt/virtuality/source
-sudo git fetch --all
-sudo git reset --hard origin/main
-sudo git clean -fd
-sudo bash scripts/install_web_panel.sh
-sudo systemctl restart virtuality-web
-
- -
-

Быстрая диагностика

host / VM / network
-
sudo vhealth
-sudo systemctl status virtuality-web --no-pager
-sudo journalctl -u virtuality-web -n 120 --no-pager
-virsh list --all
-virsh net-list --all
-ip route
-sudo nft list ruleset | grep -A120 virtuality
-
-
- -
-

Проверка проброса к VM

tcpdump
-

Если certbot или внешний сайт не открывается, сначала надо понять, где теряется пакет: на роутере, на хосте Virtuality или уже внутри VM.

-
# На хосте Virtuality
-sudo tcpdump -ni br0 'tcp port 80 or tcp port 443'
-
-# На VM
-sudo ss -tulpn | grep -E ':80|:443'
-sudo nginx -t
-
- -
-

Правила версионирования

release discipline
-
-
+0.0.1Мелкий фикс, безопасное уточнение, диагностика, маленький UI-патч.
-
+0.1.0Заметная функция или новый раздел панели.
-
+1.0.0Архитектурный разрыв или несовместимое изменение.
-
КоммитыСообщения коммитов — на русском, в прошедшем времени, без инфинитива.
-
-
-
-
- - - diff --git a/web/static/themes.css b/web/static/themes.css deleted file mode 100644 index 6bb6baa..0000000 --- a/web/static/themes.css +++ /dev/null @@ -1,205 +0,0 @@ -/* Virtuality theme layer. Default theme is Neon from app.css. */ -html[data-theme="macos"] { - --bg: #f5f5f7; - --bg-soft: #ffffff; - --panel: rgba(255, 255, 255, .92); - --panel-solid: #ffffff; - --panel-2: #f2f2f7; - --line: rgba(60, 60, 67, .14); - --line-strong: rgba(60, 60, 67, .24); - --text: #1d1d1f; - --muted: #6e6e73; - --muted-2: #8e8e93; - --accent: #007aff; - --accent-2: #0a84ff; - --accent-glow: rgba(0, 122, 255, .12); - --green: #34c759; - --yellow: #ff9f0a; - --red: #ff3b30; - --purple: #af52de; - --radius: 16px; - --radius-sm: 10px; - --shadow: 0 20px 55px rgba(0, 0, 0, .08); - --shadow-soft: 0 8px 28px rgba(0, 0, 0, .06); -} - -html[data-theme="macos"] body { - background: linear-gradient(180deg, #fbfbfd 0%, #f5f5f7 48%, #ededf2 100%); - color: var(--text); -} -html[data-theme="macos"] body::before { display: none; } - -html[data-theme="macos"] .v-sidebar { - background: rgba(246, 246, 248, .88); - color: var(--text); - border-right: 1px solid var(--line); - box-shadow: 10px 0 30px rgba(0, 0, 0, .035); - backdrop-filter: saturate(180%) blur(22px); -} -html[data-theme="macos"] .v-logo { border-bottom-color: var(--line); } -html[data-theme="macos"] .v-logo::before { - color: #fff; - border-radius: 12px; - background: linear-gradient(135deg, #007aff, #5e5ce6); - box-shadow: none; -} -html[data-theme="macos"] .v-logo span, -html[data-theme="macos"] .v-nav-group-title, -html[data-theme="macos"] .muted, -html[data-theme="macos"] .small-note, -html[data-theme="macos"] .v-subtitle, -html[data-theme="macos"] .service-row small, -html[data-theme="macos"] .big-action span, -html[data-theme="macos"] .knowledge-list span, -html[data-theme="macos"] .meta-grid span { color: var(--muted); } - -html[data-theme="macos"] .v-nav a, -html[data-theme="macos"] .v-nav-group-title { - color: #3a3a3c; - border-radius: 10px; - font-weight: 700; -} -html[data-theme="macos"] .v-nav a:hover, -html[data-theme="macos"] .v-nav-group-title:hover { - color: var(--text); - background: rgba(0, 0, 0, .045); - border-color: transparent; - transform: none; -} -html[data-theme="macos"] .v-nav a.active { - color: #fff; - background: #007aff; - border-color: transparent; - box-shadow: none; -} -html[data-theme="macos"] .v-nav a.active::before { display: none; } -html[data-theme="macos"] .v-nav-group { background: transparent; border-color: transparent; } -html[data-theme="macos"] .v-nav-group.open { background: rgba(0, 0, 0, .035); border-color: transparent; } -html[data-theme="macos"] .v-nav-bottom { border-top-color: var(--line) !important; } - -html[data-theme="macos"] .v-topbar, -html[data-theme="macos"] .topbar, -html[data-theme="macos"] .card, -html[data-theme="macos"] .auth-card { - background: rgba(255, 255, 255, .86); - border: 1px solid rgba(60, 60, 67, .12); - box-shadow: var(--shadow-soft); - backdrop-filter: saturate(180%) blur(18px); -} -html[data-theme="macos"] .card::before { display: none; } -html[data-theme="macos"] .v-title, -html[data-theme="macos"] .brand, -html[data-theme="macos"] h2, -html[data-theme="macos"] .value, -html[data-theme="macos"] .strong { color: var(--text); } -html[data-theme="macos"] .label, -html[data-theme="macos"] th { color: #86868b; font-weight: 700; } -html[data-theme="macos"] .top-meta span, -html[data-theme="macos"] .pill, -html[data-theme="macos"] .link-pill { - color: #3a3a3c; - background: #f2f2f7; - border-color: rgba(60, 60, 67, .12); - box-shadow: none; -} -html[data-theme="macos"] .link-pill:hover { - color: #007aff; - background: #e8f1ff; - border-color: rgba(0, 122, 255, .18); -} -html[data-theme="macos"] .link-pill.accent, -html[data-theme="macos"] button.primary, -html[data-theme="macos"] .icon-action.accent { - color: #fff; - background: #007aff; - border-color: #007aff; - box-shadow: none; -} -html[data-theme="macos"] .link-pill.accent:hover, -html[data-theme="macos"] button.primary:hover, -html[data-theme="macos"] .icon-action.accent:hover { - color: #fff; - filter: none; - transform: none; - background: #0a84ff; -} - -html[data-theme="macos"] table { border-spacing: 0 7px; } -html[data-theme="macos"] td { color: var(--text); background: #fff; border-color: rgba(60, 60, 67, .10); } -html[data-theme="macos"] tbody tr:hover td { background: #f7f7fa; border-color: rgba(0, 122, 255, .18); } -html[data-theme="macos"] .table-link { color: #007aff; } - -html[data-theme="macos"] button, -html[data-theme="macos"] .icon-action { - color: #1d1d1f; - background: #f2f2f7; - border-color: rgba(60, 60, 67, .12); - border-radius: 10px; - box-shadow: none; - font-weight: 700; -} -html[data-theme="macos"] button:hover, -html[data-theme="macos"] .icon-action:hover { - transform: none; - color: #007aff; - background: #e8f1ff; - border-color: rgba(0, 122, 255, .18); -} -html[data-theme="macos"] button.danger, -html[data-theme="macos"] .icon-action.danger { - color: #ff3b30; - background: #fff1f0; - border-color: rgba(255, 59, 48, .18); -} -html[data-theme="macos"] .icon-action.success { - color: #248a3d; - background: #edf9f0; - border-color: rgba(52, 199, 89, .22); -} - -html[data-theme="macos"] input, -html[data-theme="macos"] select { - color: var(--text); - background: #fff; - border-color: rgba(60, 60, 67, .16); - border-radius: 10px; -} -html[data-theme="macos"] input:focus, -html[data-theme="macos"] select:focus { - border-color: rgba(0, 122, 255, .55); - box-shadow: 0 0 0 4px rgba(0, 122, 255, .10); -} -html[data-theme="macos"] .form-subcard, -html[data-theme="macos"] .upload-box { - background: #f5f5f7; - border-color: rgba(0, 122, 255, .22); -} -html[data-theme="macos"] .service-row, -html[data-theme="macos"] .big-action, -html[data-theme="macos"] .knowledge-list div, -html[data-theme="macos"] .meta-grid div { - background: #f7f7fa; - border-color: rgba(60, 60, 67, .10); -} -html[data-theme="macos"] .service-link:hover, -html[data-theme="macos"] .big-action:hover { background: #eef5ff; border-color: rgba(0, 122, 255, .18); } - -html[data-theme="macos"] .status::before { box-shadow: none; } -html[data-theme="macos"] .status.ok { color: #248a3d; background: #edf9f0; } -html[data-theme="macos"] .status.warn { color: #b26a00; background: #fff7e6; } -html[data-theme="macos"] .status.err { color: #d70015; background: #fff1f0; } -html[data-theme="macos"] .notice { color: #004b8d; background: #eef7ff; border-color: rgba(0, 122, 255, .18); } -html[data-theme="macos"] .alert.danger { color: #d70015; background: #fff1f0; border-color: rgba(255, 59, 48, .20); } -html[data-theme="macos"] .alert.success { color: #248a3d; background: #edf9f0; border-color: rgba(52, 199, 89, .20); } -html[data-theme="macos"] pre { color: #1d1d1f; background: #f2f2f7; border-color: rgba(60, 60, 67, .12); } -html[data-theme="macos"] .progress { background: #e5e5ea; border-color: rgba(60, 60, 67, .10); } -html[data-theme="macos"] .progress span { background: #007aff; box-shadow: none; } -html[data-theme="macos"] .upload-progress-wrap { background: #eef7ff; border-color: rgba(0, 122, 255, .18); } -html[data-theme="macos"] .upload-progress-head span { color: #007aff; } - -.theme-switcher { display: grid; gap: 8px; margin-top: 10px; padding: 10px; border-top: 1px solid var(--line); } -.theme-switcher-label { color: var(--muted); font-size: 10px; font-weight: 900; letter-spacing: .14em; text-transform: uppercase; } -.theme-buttons { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; } -.theme-button { min-height: 34px; padding: 7px 9px; border-radius: 10px; font-size: 12px; } -.theme-button.is-active { color: #00111f; background: linear-gradient(135deg, var(--accent), #60a5fa); border-color: transparent; } -html[data-theme="macos"] .theme-button.is-active { color: #fff; background: #007aff; } diff --git a/web/templates/_sidebar.html b/web/templates/_sidebar.html deleted file mode 100644 index 53c3adf..0000000 --- a/web/templates/_sidebar.html +++ /dev/null @@ -1,22 +0,0 @@ -{% set path = request.url.path %} - diff --git a/web/update_core.py b/web/update_core.py index 9121a26..270b083 100644 --- a/web/update_core.py +++ b/web/update_core.py @@ -142,6 +142,11 @@ def missing_versions(local_version: str, remote_version: str, manifest: dict[str return sorted(versions, key=lambda item: version_tuple(str(item.get('version', '0.0.0')))) +def is_ancestor(older: str, newer: str) -> bool: + # Only offer an update when the remote is ahead; a node running a newer or diverged build must not be downgraded. + return run_cmd(['git', 'merge-base', '--is-ancestor', older, newer], cwd=SOURCE_DIR, timeout=10)['ok'] + + def git_log_between(base: str, head: str, limit: int = 50) -> list[dict[str, str]]: if not base or not head or base == head: return [] @@ -173,7 +178,7 @@ def check_updates(fetch: bool = True) -> dict[str, Any]: manifest = load_manifest(f'{REMOTE}/{DEFAULT_BRANCH}') missed = missing_versions(local_version, remote_version, manifest) commits = git_log_between(local_commit, remote_commit) - has_update = bool(local_commit and remote_commit and local_commit != remote_commit) + has_update = bool(local_commit and remote_commit and local_commit != remote_commit and is_ancestor(local_commit, remote_commit)) data = { 'ok': True,