Files
Claude 8a4474a305 Мастер первичной настройки и nodectl: добавлены пошаговый /setup, управление узлом через virtuality-ctl и тесты
- web/nodectl.py: факты об узле (сеть, диски, часовые пояса, KVM) и действия через virtuality-ctl
- web/features/setup.py + шаблоны setup*: мастер из 8 шагов (установка, часовой пояс, пароль, HTTPS, сеть, хранилище, обновления, итог)
- безопасное переключение на мост br0 с авто-откатом и подтверждением из браузера
- virtuality-ctl storage-use: перенос данных на отдельный диск
- панель перенаправляет на мастер до его завершения; демо-режим VIRTUALITY_DEV_SETUP
- tests/test_setup.py: 8 тестов на маршруты мастера

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
2026-09-25 14:36:32 +00:00

49 строки
3.3 KiB
HTML

{% extends "setup_base.html" %}
{% from "_ui.html" import icon, alert %}
{% block rail %}{% endblock %}
{% block step %}
<div class="card-header"><div><h2>Перенастраиваем сеть</h2><p>Сервер объединяет сетевой порт в мост br0. Это займёт около 20 секунд, страница не перезагружается.</p></div></div>
<div class="card-body stack" data-bridge-wait data-confirm-url="{{ confirm_url }}" data-next-url="{{ next_url }}">
<div class="row" style="gap:14px"><span class="spinner" style="width:22px;height:22px;color:var(--brand)"></span><strong data-bridge-status>Ждём, пока сервер снова ответит…</strong></div>
<div class="progress indeterminate" data-bridge-progress><span></span></div>
<div class="alert info">{{ icon('info') }}<div><strong>Если страница так и не оживёт</strong>Ничего страшного: через 2 минуты сервер сам вернёт прежнюю сеть. Обновите страницу и выберите «Отдельная сеть (NAT)» или попробуйте ещё раз.</div></div>
</div>
<div class="card-footer">
<form method="post" action="{{ revert_url }}" style="margin-right:auto"><button class="btn btn-ghost">Отменить и вернуть прежнюю сеть</button></form>
<a class="btn btn-primary disabled" href="{{ next_url }}" data-bridge-next>Продолжить{{ icon('arrow-right') }}</a>
</div>
{% endblock %}
{% block scripts %}
<script>
(function () {
var root = document.querySelector('[data-bridge-wait]');
var status = document.querySelector('[data-bridge-status]');
var next = document.querySelector('[data-bridge-next]');
var started = Date.now();
var tries = 0;
function done(ok, text) {
status.textContent = text;
document.querySelector('[data-bridge-progress]').classList.remove('indeterminate');
document.querySelector('[data-bridge-progress] > span').style.width = '100%';
document.querySelector('.spinner').style.display = 'none';
if (ok) { next.classList.remove('disabled'); setTimeout(function () { location.href = root.dataset.nextUrl; }, 1500); }
}
function attempt() {
tries += 1;
fetch('/healthz', { cache: 'no-store' }).then(function (r) {
if (!r.ok) throw new Error('not ready');
return fetch(root.dataset.confirmUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest' } });
}).then(function (r) { return r.json(); }).then(function (data) {
if (data.ok) done(true, 'Сеть работает и подтверждена. Машины будут в вашей домашней сети.');
else done(false, data.message || 'Не удалось подтвердить сеть');
}).catch(function () {
if (Date.now() - started > 150000) { done(false, 'Сервер не отвечает. Через 2 минуты он вернёт прежнюю сеть — обновите страницу.'); return; }
status.textContent = 'Ждём, пока сервер снова ответит… (' + Math.round((Date.now() - started) / 1000) + ' с)';
setTimeout(attempt, tries < 5 ? 3000 : 5000);
});
}
setTimeout(attempt, 4000);
})();
</script>
{% endblock %}