Files
Claude 4098c270a4 Интерфейс панели был полностью перерисован
Новый дизайн в стиле коммерческих продуктов, рассчитанный на обычного
пользователя:
- светлая и тёмная темы (следует системной), шрифт 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
2026-09-25 07:01:41 +00:00

39 строки
1.9 KiB
JavaScript

// Screenshots of every panel page for design review.
// Usage: node tests/screenshots.mjs [baseUrl] [outDir] [theme]
import { createRequire } from 'node:module';
const { chromium } = createRequire(import.meta.url)('playwright'); // NODE_PATH=$(npm root -g) for a global install
import { mkdirSync } from 'node:fs';
const base = process.argv[2] || 'http://127.0.0.1:8765';
const out = process.argv[3] || 'screenshots';
const theme = process.argv[4] || '';
const pages = [
['login', '/login'], ['dashboard', '/'], ['vm', '/vm/web01'], ['create', '/vm/create'], ['help', '/help'], ['operation', '/operations'],
['iso', '/iso'], ['disks', '/disk-images'], ['network', '/network'], ['operations', '/operations'],
['logs', '/logs'], ['update', '/update'], ['host', '/host'], ['console', '/vm/web01/console'],
];
mkdirSync(out, { recursive: true });
const browser = await chromium.launch();
for (const [width, suffix] of [[1440, ''], [390, '-mobile']]) {
const context = await browser.newContext({ viewport: { width, height: 900 }, deviceScaleFactor: 1 });
const page = await context.newPage();
const errors = [];
page.on('pageerror', (err) => errors.push(err.message));
if (theme) await page.addInitScript((t) => localStorage.setItem('virtualityTheme', t), theme);
await page.goto(base + '/login');
if (suffix === '') await page.screenshot({ path: `${out}/login${suffix}.png`, fullPage: true });
await page.fill('input[name=password]', 'x');
await page.click('button[type=submit]');
await page.waitForURL(base + '/');
for (const [name, path] of pages.slice(1)) {
if (suffix && !['dashboard', 'vm', 'create'].includes(name)) continue;
await page.goto(base + path);
await page.waitForTimeout(400);
await page.screenshot({ path: `${out}/${name}${suffix}.png`, fullPage: true });
}
if (errors.length) console.log('JS errors:', errors.join(' | '));
await context.close();
}
await browser.close();
console.log('saved to', out);