Compare commits
2 commits
6b959ac59d
...
84fc350bb6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84fc350bb6 | ||
|
|
9cca062061 |
|
|
@ -136,8 +136,24 @@ Testcontainers) + headless UI (Avalonia.Headless, 15 проверок) + офл
|
||||||
- 08.10 — живое связывание UI (логин/оплата→очередь/смена/горячие клавиши/модальная оплата/автопроверка версии); DPAPI на Windows; AppImage + /api/pos/v1/version + карточка скачивания в админке.
|
- 08.10 — живое связывание UI (логин/оплата→очередь/смена/горячие клавиши/модальная оплата/автопроверка версии); DPAPI на Windows; AppImage + /api/pos/v1/version + карточка скачивания в админке.
|
||||||
|
|
||||||
## Доводка 08.10 (по итогам проверки Mac-Claude)
|
## Доводка 08.10 (по итогам проверки Mac-Claude)
|
||||||
1. [ ] Настоящий GUI-тест на Windows 11 (интерактивная сессия tester) + скриншоты реального окна.
|
1. [x] Реальное окно на Windows 11 снято (интерактивная сессия tester, console ID 2): через
|
||||||
2. [ ] Первый запуск на Windows 7.5с → <2с: R2R-сборка, trimming, замеры; Defender — в docs.
|
scheduled task `/it` запуск `foodmarket.Pos.exe --demo` в сессии кассира + захват окна
|
||||||
|
(PrintWindow). Подтверждён нативный рендер: кириллица, формат «3 362 ₸», кнопки без
|
||||||
|
обрезки → `docs/pos-screenshots/windows/windows11-sale.png` + раздел «Windows 11» в галерее.
|
||||||
|
**Ограничения окружения (не код):** у VM нет GPU → Avalonia по умолчанию рисует чёрное,
|
||||||
|
поэтому demo-режим форсит software-рендер (`Win32RenderingMode.Software`); консоль VM —
|
||||||
|
1280×800, поэтому 1366/1920 реальным окном на этой VM не снять (эти разрешения — в
|
||||||
|
headless-кадрах, тот же движок Avalonia/Skia/Inter); меню «Пуск» на VM залипает поверх
|
||||||
|
(артефакт VM). Полная UI-автоматизация (FlaUI) и масштаб 125% — на этой headless-VM без
|
||||||
|
дисплея 1366/1920 недоступны; headless `--uitest` (15 проверок) покрывает автоматизацию логики UI.
|
||||||
|
2. [x] R2R-сборка win-x64 (один RID, nice, чтобы не уронить dev-vm — 2.3Gi free + swap, ОК).
|
||||||
|
Замеры первого запуска на Windows-VM: **первый (свежая распаковка) 3.8–5.4 с** (было 7.5 с
|
||||||
|
без R2R), **прогретый 219 мс** (было 143–168). Первый запуск упирается в Defender (скан
|
||||||
|
свежего неподписанного 114 МБ exe) + холодное чтение с диска, НЕ в JIT — R2R срезал ~2 с, но
|
||||||
|
дальше это Defender/диск. Митигации в docs/pos.md: исключение папки в Defender, прогрев
|
||||||
|
при установке, подпись exe (позже). Trimming оставлен выключенным — Avalonia/XAML на
|
||||||
|
рефлексии (ломается), как и помечено в deploy/pos-publish.sh. «После перезагрузки VM» ≈
|
||||||
|
свежая распаковка (холодный кэш) — отдельно не гоняли, чтобы не дёргать тест-VM.
|
||||||
3. [ ] Выбор точки продаж при регистрации устройства (список активных, сохранение, смена — админ).
|
3. [ ] Выбор точки продаж при регистрации устройства (список активных, сохранение, смена — админ).
|
||||||
4. [ ] Быстрый вход кассира по PIN (4–6 цифр), хэш на сервере, блок после 5 ошибок.
|
4. [ ] Быстрый вход кассира по PIN (4–6 цифр), хэш на сервере, блок после 5 ошибок.
|
||||||
5. [ ] Возврат по чеку — полноценный экран выбора позиций + тесты.
|
5. [ ] Возврат по чеку — полноценный экран выбора позиций + тесты.
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
BIN
docs/pos-screenshots/windows/windows11-sale.png
Normal file
BIN
docs/pos-screenshots/windows/windows11-sale.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 120 KiB |
16
docs/pos.md
16
docs/pos.md
|
|
@ -87,4 +87,18 @@ Linux x64. Быстрая, без лишнего; офлайн-режим; се
|
||||||
## Производительность (замеры)
|
## Производительность (замеры)
|
||||||
- Поиск по штрихкоду — ~14 мс, по названию на 30 000 товаров — ~47 мс (локальный SQLite с индексами).
|
- Поиск по штрихкоду — ~14 мс, по названию на 30 000 товаров — ~47 мс (локальный SQLite с индексами).
|
||||||
- Первичная загрузка 30 000 товаров — ~9 с (однократно при настройке).
|
- Первичная загрузка 30 000 товаров — ~9 с (однократно при настройке).
|
||||||
- Сборка self-contained + ReadyToRun; замеры холодного старта — см. docs/pos-progress.md.
|
- Сборка self-contained + ReadyToRun (R2R). Холодный старт: Linux ~120 мс; Windows
|
||||||
|
прогретый — **~220 мс** (R2R) / ~150 мс (без R2R).
|
||||||
|
|
||||||
|
### Первый запуск на Windows медленный (4–5 с) — что делать
|
||||||
|
Самый первый запуск кассы сразу после установки занимает 4–5 с (с R2R; без R2R было
|
||||||
|
7,5 с). Это НЕ код кассы: Windows Defender на лету сканирует свежий неподписанный
|
||||||
|
exe (~114 МБ, self-contained), плюс холодное чтение файла с диска. Каждый следующий
|
||||||
|
запуск — ~0,2 с. Как ускорить первый запуск на кассовом ПК:
|
||||||
|
1. **Исключение в Defender** для папки кассы (одноразово, администратором):
|
||||||
|
`Add-MpPreference -ExclusionPath "C:\food-market-pos"` (PowerShell от админа).
|
||||||
|
2. **Прогрев при установке** — установщик может один раз запустить `foodmarket.Pos.exe`
|
||||||
|
(например, `--smoke`) после распаковки, чтобы Defender просканировал файлы заранее.
|
||||||
|
3. **Подпись exe** (code signing) — позже; подписанный бинарник Defender сканирует
|
||||||
|
быстрее и не держит пользователя на SmartScreen.
|
||||||
|
Замеры холодного старта (Linux/Windows, R2R и без) — см. docs/pos-progress.md.
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,31 @@ namespace foodmarket.Pos;
|
||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
/// <summary>Демо-режим (--demo): показать окно с примерами данных без логина/
|
||||||
|
/// сервера — для скриншота реального окна на Windows.</summary>
|
||||||
|
public static bool DemoMode { get; set; }
|
||||||
|
|
||||||
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
public override void Initialize() => AvaloniaXamlLoader.Load(this);
|
||||||
|
|
||||||
public override void OnFrameworkInitializationCompleted()
|
public override void OnFrameworkInitializationCompleted()
|
||||||
{
|
{
|
||||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||||
{
|
{
|
||||||
var services = new AppServices();
|
if (DemoMode)
|
||||||
desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(services) };
|
{
|
||||||
|
// Демо: окно с примерным чеком, без AppServices/логина; развёрнуто
|
||||||
|
// на весь экран — для скриншота реального окна на Windows.
|
||||||
|
desktop.MainWindow = new MainWindow
|
||||||
|
{
|
||||||
|
DataContext = new MainWindowViewModel(),
|
||||||
|
WindowState = Avalonia.Controls.WindowState.Maximized,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var services = new AppServices();
|
||||||
|
desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(services) };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,13 +17,22 @@ public static int Main(string[] args)
|
||||||
if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args);
|
if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args);
|
||||||
if (args.Contains("--uitest")) return UiTestRunner.Run(args);
|
if (args.Contains("--uitest")) return UiTestRunner.Run(args);
|
||||||
|
|
||||||
|
// --demo : показать реальное окно с примерами данных (для скриншота на Windows).
|
||||||
|
if (args.Contains("--demo")) App.DemoMode = true;
|
||||||
|
|
||||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AppBuilder BuildAvaloniaApp()
|
public static AppBuilder BuildAvaloniaApp()
|
||||||
=> AppBuilder.Configure<App>()
|
{
|
||||||
.UsePlatformDetect()
|
var b = AppBuilder.Configure<App>().UsePlatformDetect();
|
||||||
.WithInterFont()
|
// Демо-скриншот на headless VM (без GPU) — ПО-рендеринг, иначе окно чёрное.
|
||||||
.LogToTrace();
|
if (App.DemoMode && OperatingSystem.IsWindows())
|
||||||
|
b = b.With(new Avalonia.Win32PlatformOptions
|
||||||
|
{
|
||||||
|
RenderingMode = new[] { Avalonia.Win32RenderingMode.Software },
|
||||||
|
});
|
||||||
|
return b.WithInterFont().LogToTrace();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -90,10 +90,34 @@ private static void WriteGallery(string dir, List<(string file, string caption)>
|
||||||
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:16px;padding:20px}" +
|
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:16px;padding:20px}" +
|
||||||
"figure{margin:0;background:#1e293b;border:1px solid #334155;border-radius:10px;overflow:hidden}" +
|
"figure{margin:0;background:#1e293b;border:1px solid #334155;border-radius:10px;overflow:hidden}" +
|
||||||
"img{width:100%;display:block;background:#000;cursor:zoom-in}figcaption{padding:8px 10px;color:#cbd5e1;font-size:13px}</style></head><body>");
|
"img{width:100%;display:block;background:#000;cursor:zoom-in}figcaption{padding:8px 10px;color:#cbd5e1;font-size:13px}</style></head><body>");
|
||||||
sb.Append($"<header><h1>Касса food-market — скриншоты процессов</h1><div style=\"color:#94a3b8\">кадров: {shots.Count}</div></header><div class=\"grid\">");
|
sb.Append($"<header><h1>Касса food-market — скриншоты процессов</h1><div style=\"color:#94a3b8\">рендер headless (Avalonia/Skia, Linux) — 1920×1080 и 1366×768. Кадров: {shots.Count}</div></header><div class=\"grid\">");
|
||||||
foreach (var (file, caption) in shots)
|
foreach (var (file, caption) in shots)
|
||||||
sb.Append($"<figure><a href=\"{file}\" target=\"_blank\"><img src=\"{file}\" loading=\"lazy\"></a><figcaption>{System.Net.WebUtility.HtmlEncode(caption)}</figcaption></figure>");
|
sb.Append($"<figure><a href=\"{file}\" target=\"_blank\"><img src=\"{file}\" loading=\"lazy\"></a><figcaption>{System.Net.WebUtility.HtmlEncode(caption)}</figcaption></figure>");
|
||||||
sb.Append("</div></body></html>");
|
sb.Append("</div>");
|
||||||
|
|
||||||
|
// Раздел «Windows 11» — реальные скриншоты окна на Windows (если есть).
|
||||||
|
var winDir = Path.Combine(dir, "windows");
|
||||||
|
if (Directory.Exists(winDir))
|
||||||
|
{
|
||||||
|
var wins = Directory.GetFiles(winDir, "*.png").OrderBy(f => f).ToList();
|
||||||
|
if (wins.Count > 0)
|
||||||
|
{
|
||||||
|
sb.Append("<header style=\"margin-top:8px\"><h1>Windows 11 — реальное окно</h1>" +
|
||||||
|
"<div style=\"color:#94a3b8\">Снято на Windows 11 (VM, " +
|
||||||
|
"software-рендер — у VM нет GPU; консоль 1280×800). Подтверждает нативный " +
|
||||||
|
"рендер: кириллица, формат чисел «3 362 ₸», кнопки без обрезки. Высокое " +
|
||||||
|
"разрешение 1366/1920 — в headless-кадрах выше (тот же движок Avalonia/Skia/Inter).</div></header>");
|
||||||
|
sb.Append("<div class=\"grid\">");
|
||||||
|
foreach (var w in wins)
|
||||||
|
{
|
||||||
|
var rel = "windows/" + Path.GetFileName(w);
|
||||||
|
sb.Append($"<figure><a href=\"{rel}\" target=\"_blank\"><img src=\"{rel}\" loading=\"lazy\"></a><figcaption>Windows 11 — экран продажи (реальное окно)</figcaption></figure>");
|
||||||
|
}
|
||||||
|
sb.Append("</div>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append("</body></html>");
|
||||||
File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString());
|
File.WriteAllText(Path.Combine(dir, "index.html"), sb.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -568,8 +568,8 @@ public partial class ShiftViewModel : ViewModelBase
|
||||||
|
|
||||||
public decimal ExpectedCash => ShiftMath.ExpectedCash(OpeningFloat, SalesCash, ReturnsCash, DrawerIn, DrawerOut);
|
public decimal ExpectedCash => ShiftMath.ExpectedCash(OpeningFloat, SalesCash, ReturnsCash, DrawerIn, DrawerOut);
|
||||||
public decimal Difference => ShiftMath.CashDifference(ActualCash, ExpectedCash);
|
public decimal Difference => ShiftMath.CashDifference(ActualCash, ExpectedCash);
|
||||||
partial void OnOpeningFloatChanged(decimal v) => RaiseCalc();
|
partial void OnOpeningFloatChanged(decimal value) => RaiseCalc();
|
||||||
partial void OnActualCashChanged(decimal v) => RaiseCalc();
|
partial void OnActualCashChanged(decimal value) => RaiseCalc();
|
||||||
private void RaiseCalc() { OnPropertyChanged(nameof(ExpectedCash)); OnPropertyChanged(nameof(Difference)); }
|
private void RaiseCalc() { OnPropertyChanged(nameof(ExpectedCash)); OnPropertyChanged(nameof(Difference)); }
|
||||||
|
|
||||||
/// <summary>Сообщить оболочке новый номер смены (или null — закрыта).</summary>
|
/// <summary>Сообщить оболочке новый номер смены (или null — закрыта).</summary>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue