Files
virtuality/web/templates/dashboard.html
T
Claude 34684634af Повседневные операции с машиной были добавлены: диск, клон, пауза, заметки, нагрузка
Модуль web/features/vmops.py (router подключён двумя строками в app.py):

- Увеличение диска на вкладке «Диски и загрузка»: размер из qemu-img info,
  только рост, проверка свободного места; выключенная машина — qemu-img resize,
  работающая — virsh blockresize. Диски вне /var/lib/virtuality не трогаются.
- Клонирование: страница /vm/{name}/clone с проверками (машина выключена,
  хватает места, имя свободно), фоновая операция virt-clone с явными --file
  в хранилище образов, прогресс по росту файлов копии, pool-refresh после.
- Пауза, продолжение, спящий режим (managedsave как операция) и удаление
  сохранённого состояния — /vm/{name}/ops/{action}; пункты меню на обзоре
  и странице машины, кнопка «Продолжить» для приостановленной машины.
- Заметки: название и описание через virsh desc (--new-desc=ТЕКСТ одним
  аргументом, чтобы текст вида «--edit» не стал опцией); название видно
  под именем машины на обзоре (virsh list --all --title, парсер в presenters).
- Живая нагрузка: /live/stats по virsh domstats для всех активных машин
  одним вызовом, CPU % по разнице cpu.time между опросами; шкалы в строках
  обзора и карточка «Нагрузка сейчас» на странице машины (panel.js, 5 с).

Тесты для всех парсеров и маршрутов (tests/test_vmops.py), демо-данные
в dev_server.py (три машины в разных состояниях, живые шкалы), значки
notebook-pen и expand в спрайте.

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

170 строки
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 %}
{% set titles = vmops_titles() if vms else {} %}
{% 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 %}
{% include "_vm_ops_alerts.html" %}
{% 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">{% if titles.get(vm.name) %}<span class="vm-title">{{ titles[vm.name] }}</span> · {% endif %}<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>
{% include "_vm_live_meters.html" %}
</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>
{% elif vm.state == 'paused' %}
<form method="post" action="/vm/{{ vm.name }}/ops/resume" class="inline-form" data-toast="Машина {{ vm.name }} продолжает работу"><input type="hidden" name="next" value="/"><button class="btn btn-sm">{{ icon('play', 'icon-sm') }}<span class="btn-label">Продолжить</span></button></form>
{% 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>
{% set ops_name, ops_state, ops_next, ops_hints = vm.name, vm.state, '/', false %}{% include "_vm_ops_menu.html" %}
{% 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>
{% elif vm.state != 'paused' %}
<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 %}