Центр обновлений больше не предлагает откат на старую версию

Раньше обновление считалось доступным при любом расхождении локального
коммита и origin/main. Нода со сборкой новее main (например, из
ISO-образа ветки) получала предложение «обновиться» на старую версию,
а ночное автообновление откатывало её само. Теперь обновление
предлагается и ставится, только если локальный коммит — предок
удалённого.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
Claude
2026-09-25 07:01:30 +00:00
родитель 967f4c1a7e
Коммит c4cebd916b
5 изменённых файлов: 11 добавлений и 376 удалений
-148
Просмотреть файл
@@ -1,148 +0,0 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>База знаний — Virtuality</title>
<link rel="stylesheet" href="/static/app.css">
</head>
<body>
<div class="v-layout">
<aside class="v-sidebar">
<div class="v-logo">
<strong>Virtuality</strong>
<span>control panel</span>
</div>
<nav class="v-nav">
<a href="/">Обзор</a>
<a href="/vm/create">Создать VM</a>
<div class="v-nav-group">
<button class="v-nav-group-title" type="button">Образы</button>
<a href="/iso">ISO</a>
<a href="/disk-images">Диски</a>
</div>
<a href="/network">Сеть</a>
<a href="/operations">Операции</a>
<a href="/logs">Журналы</a>
<a href="/update">Обновления</a>
<a href="/host">Хост</a>
<a class="v-nav-bottom active" href="/static/knowledge.html">База знаний</a>
</nav>
</aside>
<main class="v-main">
<header class="v-topbar">
<div>
<div class="v-title">База знаний</div>
<div class="v-subtitle">Документация Virtuality, боевые заметки, сетевые схемы и чек-листы</div>
</div>
<div class="top-meta">
<a class="link-pill" href="/">← Дашборд</a>
</div>
</header>
<section class="grid stats compact-stats">
<article class="card stat"><div class="label">Фокус</div><div class="value small">VM / сеть / SSL</div></article>
<article class="card stat"><div class="label">Статус</div><div class="value small">Наполняется</div></article>
<article class="card stat"><div class="label">Правило</div><div class="value small">SSL-ready</div></article>
<article class="card stat"><div class="label">Версия раздела</div><div class="value small">0.8.59</div></article>
</section>
<section class="card">
<div class="card-head"><h2>Главное правило: SSL-ready by default</h2><span class="pill">Let’s Encrypt / HTTP-01</span></div>
<div class="notice">При настройке VM и пробросов Virtuality должна сразу учитывать выпуск SSL-сертификатов. Для Let’s Encrypt критичен внешний доступ к HTTP-01 challenge на TCP 80.</div>
<div class="knowledge-list top-space">
<div><strong>DNS</strong><span>A-запись домена должна указывать на реальный внешний IP сервера или роутера.</span></div>
<div><strong>WAN 80/443</strong><span>Порты не должны быть заняты веб-интерфейсом роутера, FastPanel на хосте или другой службой.</span></div>
<div><strong>Firewall</strong><span>Keenetic, внешний firewall и UFW/nftables должны пропускать TCP 80 и TCP 443.</span></div>
<div><strong>Проверка снаружи</strong><span>Перед certbot надо проверять curl с мобильной сети или внешнего хоста, а не только из локальной Wi-Fi сети.</span></div>
</div>
<pre>curl -I http://domain.ru
# Хорошо: ответил nginx, допустимы 200 / 301 / 404
# Плохо: timeout, connection refused или ответ админки роутера</pre>
</section>
<section class="grid two">
<article class="card">
<div class="card-head"><h2>Bridge VM</h2><span class="pill">br0 / LAN node</span></div>
<p class="muted">Если VM подключена к bridge и имеет полноценный LAN IP, самый чистый путь — пробрасывать WAN-порты роутера напрямую на IP VM.</p>
<pre>WAN 80 → IP_VM:80
WAN 443 → IP_VM:443</pre>
<div class="notice top-space">Для сайтов и FastPanel внутри bridge VM этот вариант проще и надёжнее: меньше NAT-слоёв, меньше магии, меньше боли.</div>
</article>
<article class="card">
<div class="card-head"><h2>NAT VM</h2><span class="pill">Virtuality DNAT</span></div>
<p class="muted">Если VM находится во внутренней NAT-сети Virtuality, внешний роутер должен вести трафик на хост Virtuality, а дальше Virtuality DNAT отправит его в VM.</p>
<pre>WAN 80/443 → HOST:80/443
HOST DNAT → IP_VM:80/443</pre>
<div class="notice top-space">Для NAT-сценария правила Virtuality должны быть применены и проверены через /network и tcpdump.</div>
</article>
</section>
<section class="card">
<div class="card-head"><h2>Ключевые директории</h2><span class="pill">filesystem</span></div>
<table>
<thead><tr><th>Путь</th><th>Назначение</th></tr></thead>
<tbody>
<tr><td class="strong">/opt/virtuality/source</td><td>Исходники GitHub-репозитория Virtuality.</td></tr>
<tr><td class="strong">/opt/virtuality/web</td><td>Установленная web-панель, которую запускает systemd.</td></tr>
<tr><td class="strong">/opt/virtuality/venv</td><td>Python virtualenv панели.</td></tr>
<tr><td class="strong">/var/lib/virtuality/iso</td><td>ISO-образы для установки VM.</td></tr>
<tr><td class="strong">/var/lib/virtuality/images</td><td>qcow2-диски виртуальных машин.</td></tr>
<tr><td class="strong">/var/lib/virtuality/disk-images</td><td>Загруженные готовые .img/.raw/.qcow2 образы и распакованные архивы.</td></tr>
<tr><td class="strong">/var/lib/virtuality/network</td><td>Правила пробросов портов и ручные IP VM.</td></tr>
<tr><td class="strong">/var/log/virtuality</td><td>Логи установки, операций и диагностики.</td></tr>
</tbody>
</table>
</section>
<section class="grid two">
<article class="card">
<div class="card-head"><h2>Обновление панели</h2><span class="pill">GitHub → install</span></div>
<pre>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</pre>
</article>
<article class="card">
<div class="card-head"><h2>Быстрая диагностика</h2><span class="pill">host / VM / network</span></div>
<pre>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</pre>
</article>
</section>
<section class="card">
<div class="card-head"><h2>Проверка проброса к VM</h2><span class="pill">tcpdump</span></div>
<p class="muted">Если certbot или внешний сайт не открывается, сначала надо понять, где теряется пакет: на роутере, на хосте Virtuality или уже внутри VM.</p>
<pre># На хосте Virtuality
sudo tcpdump -ni br0 'tcp port 80 or tcp port 443'
# На VM
sudo ss -tulpn | grep -E ':80|:443'
sudo nginx -t</pre>
</section>
<section class="card">
<div class="card-head"><h2>Правила версионирования</h2><span class="pill">release discipline</span></div>
<div class="knowledge-list">
<div><strong>+0.0.1</strong><span>Мелкий фикс, безопасное уточнение, диагностика, маленький UI-патч.</span></div>
<div><strong>+0.1.0</strong><span>Заметная функция или новый раздел панели.</span></div>
<div><strong>+1.0.0</strong><span>Архитектурный разрыв или несовместимое изменение.</span></div>
<div><strong>Коммиты</strong><span>Сообщения коммитов — на русском, в прошедшем времени, без инфинитива.</span></div>
</div>
</section>
</main>
</div>
<script src="/static/panel.js" defer></script>
</body>
</html>
-205
Просмотреть файл
@@ -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; }