Новый дизайн в стиле коммерческих продуктов, рассчитанный на обычного пользователя: - светлая и тёмная темы (следует системной), шрифт Inter и иконки Lucide вшиты в панель — работает без интернета; - общий каркас base.html и компоненты _ui.html вместо 14 независимых страниц; адаптивная вёрстка для телефона; - простой язык: «Работает / Выключена», «Автозапуск», «Доступ из интернета» вместо running, autostart, DNAT; технические данные убраны во вкладку «Для специалистов»; - главная: загрузка процессора, памяти и хранилища, карточки машин с главным действием и меню остальных, приветственный сценарий для первой машины; - мастер создания машины из трёх шагов с готовыми конфигурациями; - страница машины с вкладками, переключателем автозапуска и порядком загрузки кнопками; - «Сеть и доступ»: открытие SSH, сайта, RDP и т. д. из списка, ручные IP машин, понятная диагностика; - загрузка образов перетаскиванием с прогрессом и оставшимся временем; - подтверждения опасных действий в диалоге и уведомления; - раздел «Помощь»; - web/presenters.py переводит вывод virsh и /proc в данные для страниц; tests/dev_server.py запускает панель с имитацией virsh. Также: исправлено определение модели CPU на x86 («207» вместо названия процессора), история версии 0.10.0 и документация обновлены под Ubuntu 26.04 LTS, в README добавлены скриншоты. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LEUXMcdZo7b87mjkUsPgg
164 строки
12 KiB
HTML
164 строки
12 KiB
HTML
{% extends "base.html" %}
|
|
{% from "_ui.html" import icon, badge, vm_badge, op_badge, meter, alert %}
|
|
{% set page_title = 'Обзор' %}
|
|
{% set running = vms|selectattr('state', 'in', ['running', 'idle', 'blocked'])|list|length %}
|
|
|
|
{% block page_header %}
|
|
<div class="page-header">
|
|
<div>
|
|
<div class="eyebrow">{{ icon('server', 'icon-sm') }} {{ system.hostname }} · {{ system.ip }}</div>
|
|
<h1>{{ greeting }}, {{ user }}</h1>
|
|
<p class="lead">
|
|
{% if vms %}Работает <strong data-live-count="running">{{ running }}</strong> из {{ vms|length }} {{ plural(vms|length, 'машины', 'машин', 'машин') }}.{% else %}Сервер готов к работе — осталось создать первую виртуальную машину.{% endif %}
|
|
</p>
|
|
</div>
|
|
<div class="page-actions">
|
|
<a class="btn btn-primary" href="/vm/create">{{ icon('plus') }}Создать машину</a>
|
|
</div>
|
|
</div>
|
|
{% endblock %}
|
|
|
|
{% block alerts %}
|
|
{% set notice = update_notice() %}
|
|
{% if notice.has_update %}
|
|
<div class="alert brand">{{ icon('sparkles') }}<div><strong>Доступна новая версия {{ notice.latest_version }}</strong>Обновление займёт пару минут, машины продолжат работать.</div><a class="btn btn-sm btn-primary alert-action" href="/update">Посмотреть</a></div>
|
|
{% endif %}
|
|
{% if profile.virtualization_warning %}
|
|
{{ alert('warning', 'Машины будут работать медленно. Включите аппаратную виртуализацию (Intel VT-x / AMD-V) в настройках BIOS сервера.', 'Аппаратное ускорение недоступно') }}
|
|
{% endif %}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="grid grid-4">
|
|
<article class="card stat">
|
|
<div class="stat-head">Машины<span class="stat-icon">{{ icon('monitor', 'icon-sm') }}</span></div>
|
|
<div class="stat-value"><span data-live-count="running">{{ running }}</span><small>/ {{ vms|length }} работают</small></div>
|
|
{{ meter(((running / vms|length * 100) if vms else 0)|round|int, 'brand') }}
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-head">Процессор<span class="stat-icon">{{ icon('cpu', 'icon-sm') }}</span></div>
|
|
<div class="stat-value">{{ host.cpu_pct }}<small>%</small></div>
|
|
{{ meter(host.cpu_pct) }}
|
|
<div class="stat-foot">{{ host.cpu_count }} {{ plural(host.cpu_count, 'ядро', 'ядра', 'ядер') }}</div>
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-head">Память<span class="stat-icon">{{ icon('memory-stick', 'icon-sm') }}</span></div>
|
|
<div class="stat-value">{{ host.mem_used }}<small>из {{ host.mem_total }}</small></div>
|
|
{{ meter(host.mem_pct) }}
|
|
<div class="stat-foot">Занято {{ host.mem_pct }}%</div>
|
|
</article>
|
|
<article class="card stat">
|
|
<div class="stat-head">Хранилище<span class="stat-icon">{{ icon('hard-drive', 'icon-sm') }}</span></div>
|
|
<div class="stat-value">{{ host.disk_free }}<small>свободно</small></div>
|
|
{{ meter(host.disk_pct) }}
|
|
<div class="stat-foot">Всего {{ host.disk_total }}</div>
|
|
</article>
|
|
</section>
|
|
|
|
{% if vms %}
|
|
<section class="card mt-3">
|
|
<div class="card-header">
|
|
<div><h2>Виртуальные машины</h2><p>Нажмите на машину, чтобы открыть её настройки</p></div>
|
|
<a class="btn btn-sm" href="/vm/create">{{ icon('plus', 'icon-sm') }}Новая</a>
|
|
</div>
|
|
<div class="vm-list">
|
|
{% for vm in vms %}
|
|
{% set s = vm.state|vm_state %}
|
|
<div class="vm-row" data-live-vm="{{ vm.name }}" data-running="{{ '1' if s.running else '0' }}" data-reload-on-change>
|
|
<a class="vm-ident" href="/vm/{{ vm.name }}">
|
|
<span class="vm-avatar {{ 'on' if s.running else '' }}">{{ icon('monitor') }}<span class="status-dot {{ s.tone }}" data-live="dot"></span></span>
|
|
<span style="min-width:0">
|
|
<span class="vm-name truncate" style="display:block">{{ vm.name }}</span>
|
|
<span class="vm-sub"><span data-live="ip">{{ vm.ip if vm.ip and vm.ip != '—' else ('IP появится после запуска' if not s.running else 'Получает адрес…') }}</span></span>
|
|
</span>
|
|
</a>
|
|
<div class="vm-col">
|
|
<div class="vm-cell-label">Ресурсы</div>
|
|
<div class="vm-cell-value">{{ vm.vcpus or '—' }} {{ plural(vm.vcpus or 0, 'ядро', 'ядра', 'ядер') }} · {{ vm.memory_mb|format_mb }}</div>
|
|
</div>
|
|
<div class="vm-col">{{ vm_badge(vm.state) }}</div>
|
|
<div class="vm-actions">
|
|
{% if s.running %}
|
|
<a class="btn btn-sm" href="/vm/{{ vm.name }}/console" target="_blank" rel="noopener">{{ icon('monitor', 'icon-sm') }}<span class="btn-label">Экран</span></a>
|
|
{% else %}
|
|
<form method="post" action="/vm/{{ vm.name }}/start" class="inline-form" data-toast="Машина {{ vm.name }} запускается"><button class="btn btn-sm">{{ icon('play', 'icon-sm') }}<span class="btn-label">Запустить</span></button></form>
|
|
{% endif %}
|
|
<details class="menu">
|
|
<summary class="btn btn-sm btn-icon btn-ghost" aria-label="Действия">{{ icon('ellipsis') }}</summary>
|
|
<div class="menu-list">
|
|
<a class="menu-item" href="/vm/{{ vm.name }}">{{ icon('settings') }}Открыть настройки</a>
|
|
{% if s.running %}
|
|
<a class="menu-item" href="/vm/{{ vm.name }}/console" target="_blank" rel="noopener">{{ icon('monitor') }}Открыть экран</a>
|
|
<form method="post" action="/vm/{{ vm.name }}/reboot" data-toast="Машина {{ vm.name }} перезагружается"><button class="menu-item">{{ icon('rotate-cw') }}Перезагрузить</button></form>
|
|
<form method="post" action="/vm/{{ vm.name }}/shutdown" data-toast="Машина {{ vm.name }} выключается"><button class="menu-item">{{ icon('power') }}Выключить</button></form>
|
|
<div class="menu-sep"></div>
|
|
<form method="post" action="/vm/{{ vm.name }}/destroy" data-confirm="Машина будет выключена сразу, как при отключении питания. Несохранённые данные внутри неё пропадут." data-confirm-title="Выключить {{ vm.name }} принудительно?" data-confirm-button="Выключить" data-toast="Машина {{ vm.name }} выключена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('zap') }}Выключить принудительно</button></form>
|
|
{% else %}
|
|
<form method="post" action="/vm/{{ vm.name }}/start" data-toast="Машина {{ vm.name }} запускается"><button class="menu-item">{{ icon('play') }}Запустить</button></form>
|
|
<div class="menu-sep"></div>
|
|
<form method="post" action="/vm/{{ vm.name }}/delete" data-confirm="Машина и все её диски будут удалены без возможности восстановления." data-confirm-title="Удалить {{ vm.name }}?" data-confirm-button="Удалить навсегда" data-toast="Машина {{ vm.name }} удалена" data-toast-tone="warning"><button class="menu-item danger">{{ icon('trash-2') }}Удалить машину</button></form>
|
|
{% endif %}
|
|
</div>
|
|
</details>
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</section>
|
|
{% else %}
|
|
<section class="card hero-card mt-3">
|
|
<div class="row-between" style="align-items:flex-start">
|
|
<div>
|
|
<span class="badge brand" style="background:rgba(255,255,255,.12);color:#fff">{{ icon('sparkles', 'icon-sm') }}Начало работы</span>
|
|
<h2 class="mt-2">Создайте первую виртуальную машину</h2>
|
|
<p>Три шага: загрузите установочный образ системы, создайте машину и установите систему прямо в браузере.</p>
|
|
</div>
|
|
</div>
|
|
<div class="grid grid-3 mt-3">
|
|
<a class="card card-link" href="/iso" style="background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.14);color:#fff">
|
|
<div class="card-body"><span class="list-icon" style="background:rgba(255,255,255,.12);color:#fff">{{ icon('disc-3') }}</span><h3 class="mt-2" style="color:#fff">1. Загрузите образ</h3><p class="small" style="color:rgba(255,255,255,.7)">Ubuntu, Debian, Windows — файл .iso</p></div>
|
|
</a>
|
|
<a class="card card-link" href="/vm/create" style="background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.14);color:#fff">
|
|
<div class="card-body"><span class="list-icon" style="background:rgba(255,255,255,.12);color:#fff">{{ icon('plus') }}</span><h3 class="mt-2" style="color:#fff">2. Создайте машину</h3><p class="small" style="color:rgba(255,255,255,.7)">Выберите готовую конфигурацию</p></div>
|
|
</a>
|
|
<div class="card" style="background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.14);color:#fff">
|
|
<div class="card-body"><span class="list-icon" style="background:rgba(255,255,255,.12);color:#fff">{{ icon('monitor') }}</span><h3 class="mt-2" style="color:#fff">3. Откройте экран</h3><p class="small" style="color:rgba(255,255,255,.7)">Установите систему прямо в браузере</p></div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
{% endif %}
|
|
|
|
<section class="grid grid-2 mt-3">
|
|
<article class="card">
|
|
<div class="card-header"><div><h2>Последние задачи</h2><p>Создание машин, загрузка и конвертация образов</p></div><a class="btn btn-sm btn-ghost" href="/operations">Все{{ icon('chevron-right', 'icon-sm') }}</a></div>
|
|
<div class="list">
|
|
{% for op in operations %}
|
|
{% set os = op.status|operation_state %}
|
|
<a class="list-item" href="/operations/{{ op.id }}">
|
|
<span class="list-icon {{ os.tone }}">{{ icon('circle-check' if op.status == 'success' else 'circle-x' if op.status == 'error' else 'clock') }}</span>
|
|
<div class="grow"><div class="title truncate">{{ op.title }}</div><div class="meta truncate">{{ op.created_at }} · {{ op.message }}</div></div>
|
|
{{ op_badge(op.status) }}
|
|
</a>
|
|
{% else %}
|
|
<div class="empty" style="padding:32px 20px"><p class="subtle">Задач пока не было. Здесь появится прогресс создания машин и загрузки образов.</p></div>
|
|
{% endfor %}
|
|
</div>
|
|
</article>
|
|
|
|
<article class="card">
|
|
<div class="card-header">
|
|
<div><h2>Состояние сервера</h2><p>Службы, от которых зависит работа машин</p></div>
|
|
{% if services_ok %}{{ badge('Всё в порядке', 'success') }}{% else %}{{ badge('Требует внимания', 'danger') }}{% endif %}
|
|
</div>
|
|
<div class="checklist">
|
|
{% for svc in service_rows %}
|
|
<div class="check-item {{ svc.tone }}">
|
|
{{ icon('circle-check' if svc.tone == 'success' else 'triangle-alert' if svc.tone == 'warning' else 'circle-dot' if svc.tone == 'neutral' else 'circle-x') }}
|
|
<div class="row-between" style="flex:1"><strong>{{ svc.name }}</strong><span>{{ svc.label }}</span></div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</article>
|
|
</section>
|
|
{% endblock %}
|