Центр обновлений больше не предлагает откат на старую версию
Раньше обновление считалось доступным при любом расхождении локального коммита и origin/main. Нода со сборкой новее main (например, из ISO-образа ветки) получала предложение «обновиться» на старую версию, а ночное автообновление откатывало её само. Теперь обновление предлагается и ставится, только если локальный коммит — предок удалённого. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
Этот коммит содержится в:
@@ -96,6 +96,11 @@ diag_network() {
|
|||||||
write_state "idle" "Автообновление: новых обновлений нет"
|
write_state "idle" "Автообновление: новых обновлений нет"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
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}"
|
log "update may be available or current commit unknown: ${current:0:12} -> ${latest:0:12}"
|
||||||
else
|
else
|
||||||
log "git fetch failed; diagnostics and apply ZIP fallback will be used"
|
log "git fetch failed; diagnostics and apply ZIP fallback will be used"
|
||||||
|
|||||||
@@ -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>
|
|
||||||
@@ -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; }
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{% set path = request.url.path %}
|
|
||||||
<aside class="v-sidebar">
|
|
||||||
<div class="v-logo">
|
|
||||||
<strong>Virtuality</strong>
|
|
||||||
<span>control panel</span>
|
|
||||||
</div>
|
|
||||||
<nav class="v-nav">
|
|
||||||
<a data-icon="⌂" class="{{ 'active' if path == '/' else '' }}" href="/">Обзор</a>
|
|
||||||
<a data-icon="+" class="{{ 'active' if path.startswith('/vm/create') else '' }}" href="/vm/create">Создать VM</a>
|
|
||||||
<div class="v-nav-group {{ 'open' if path.startswith('/iso') or path.startswith('/disk-images') else '' }}">
|
|
||||||
<button class="v-nav-group-title" type="button">Образы</button>
|
|
||||||
<a data-icon="◎" class="{{ 'active' if path.startswith('/iso') else '' }}" href="/iso">ISO</a>
|
|
||||||
<a data-icon="▣" class="{{ 'active' if path.startswith('/disk-images') else '' }}" href="/disk-images">Диски</a>
|
|
||||||
</div>
|
|
||||||
<a data-icon="⇄" class="{{ 'active' if path.startswith('/network') else '' }}" href="/network">Сеть</a>
|
|
||||||
<a data-icon="⚙" class="{{ 'active' if path.startswith('/operations') else '' }}" href="/operations">Операции</a>
|
|
||||||
<a data-icon="≡" class="{{ 'active' if path.startswith('/logs') else '' }}" href="/logs">Журналы</a>
|
|
||||||
<a data-icon="↻" class="{{ 'active' if path.startswith('/update') else '' }}" href="/update">Обновления</a>
|
|
||||||
<a data-icon="◆" class="{{ 'active' if path.startswith('/host') else '' }}" href="/host">Хост</a>
|
|
||||||
<a data-icon="?" class="v-nav-bottom {{ 'active' if path.startswith('/static/knowledge') else '' }}" href="/static/knowledge.html">База знаний</a>
|
|
||||||
</nav>
|
|
||||||
</aside>
|
|
||||||
@@ -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'))))
|
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]]:
|
def git_log_between(base: str, head: str, limit: int = 50) -> list[dict[str, str]]:
|
||||||
if not base or not head or base == head:
|
if not base or not head or base == head:
|
||||||
return []
|
return []
|
||||||
@@ -173,7 +178,7 @@ def check_updates(fetch: bool = True) -> dict[str, Any]:
|
|||||||
manifest = load_manifest(f'{REMOTE}/{DEFAULT_BRANCH}')
|
manifest = load_manifest(f'{REMOTE}/{DEFAULT_BRANCH}')
|
||||||
missed = missing_versions(local_version, remote_version, manifest)
|
missed = missing_versions(local_version, remote_version, manifest)
|
||||||
commits = git_log_between(local_commit, remote_commit)
|
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 = {
|
data = {
|
||||||
'ok': True,
|
'ok': True,
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user