diff --git a/docs/site-screenshots/index.html b/docs/site-screenshots/index.html
index e9768dd..b92a554 100644
--- a/docs/site-screenshots/index.html
+++ b/docs/site-screenshots/index.html
@@ -13,7 +13,7 @@
.thumb{max-height:620px;overflow:auto}
diff --git a/docs/site-screenshots/pages/index.png b/docs/site-screenshots/pages/index.png
index c4fe7ff..ca615f0 100644
Binary files a/docs/site-screenshots/pages/index.png and b/docs/site-screenshots/pages/index.png differ
diff --git a/docs/site-screenshots/pages/pos.png b/docs/site-screenshots/pages/pos.png
index 36d9805..1db2d8a 100644
Binary files a/docs/site-screenshots/pages/pos.png and b/docs/site-screenshots/pages/pos.png differ
diff --git a/docs/ui-review-fixes-progress.md b/docs/ui-review-fixes-progress.md
index 1d34b38..b8fa40e 100644
--- a/docs/ui-review-fixes-progress.md
+++ b/docs/ui-review-fixes-progress.md
@@ -4,17 +4,34 @@
самой программы (не только скриншотов).
## План / статус
-1. [ ] Английские строки в админке: «Dashboard», «Low-stock alerts», «live» и др. —
- перевести; grep по web/src на видимые латинские строки; лингва-проверка в menu-walk.
-2. [ ] Каталог: дубль «Все товары» (корень дерева + системная группа). Убрать дубль.
-3. [ ] Валюта: везде «₸» и формат «1 300 ₸» (сейчас розничная — «510 KZT»).
-4. [ ] Касса на сайте: заменить рендер HTML-макета на НАСТОЯЩИЙ экран Avalonia-кассы
- (--screenshots, витринные данные «Магазин у дома», с логотипом). Макет не использовать.
-5. [ ] Витрина: packaging по товарам (Авокадо 1шт — штучный, Бешбармак 1кг — весовой и т.п.).
-6. [ ] Выложить сайт+стенд, переснять затронутые скриншоты, посмотреть каждый кадр.
+1. [x] Английские строки в админке → русский. ru.json: `dashboard.title` «Dashboard»→
+ «Сводка», `common.live/offline` «live/offline»→«онлайн/офлайн». Дашборд-виджеты:
+ defaultValue «Low-stock alerts»→«Низкий остаток», «Live-обновление»→«Обновляется в
+ реальном времени». Grep по web/src — других видимых латинских UI-строк нет (бренды/
+ единицы — ок). Тест: лингва-проверка в `stage-ui-menu-walk.spec.ts` (denylist UI-English
+ на видимом тексте каждой страницы, бренды/единицы не ловятся).
+2. [x] Каталог: системная группа «Все товары» больше не дублирует корень дерева.
+ `ProductGroupDto.IsSystem` + `ProductGroupTree` прячет системные группы (корень «Все
+ товары» = все товары, по модели МойСклада). Группа остаётся в справочнике (дефолт для
+ товаров без группы) — фильтры/импорт/карточка не ломаются.
+3. [x] Валюта: розничная цена в каталоге теперь «₸» (символ), а не «KZT» (код). Формат
+ «1 300 ₸» как у себестоимости.
+4. [x] Касса на сайте: HTML-макет заменён НАСТОЯЩИМ экраном Avalonia-кассы (--screenshots):
+ продажа (04-sale-items) и оплата (05-payment-cash) с логотипом, витринные данные
+ «Магазин у дома», кассир Виктория Ким. Убран префикс «Магазин » в шапке кассы (не было
+ «Магазин Магазин у дома»). HTML-макет удалён из публичного пайплайна.
+5. [x] Витрина: фасовка/единица выводится из названия (кг→весовой/кг, иначе штучный/штука).
+ Проверено API: Авокадо 1шт→штучный, Бешбармак 1кг→весовой, Кола 0.5л→штучный.
+6. [x] Выложено: стенд (api+web) + витрина пересеяна (RESET) + публичный сайт. Пересняты
+ затронутые экраны, пережаты в WebP (≤300 КБ). Просмотрено глазами: дашборд «Сводка»/
+ «онлайн»/«Низкий остаток»; каталог ₸/один «Все товары»/корректная фасовка; /pos —
+ настоящая касса с логотипом. Галерея `docs/site-screenshots/index.html`.
## Что НЕ трогаем
Прод, shared-postgres, global.json, gateway nginx, sklad.
## Журнал
- 08.10 — старт: план, разбор замечаний.
+- 08.10 — код: ru.json + виджеты (1), ProductGroupDto.IsSystem + tree (2), ₸ в каталоге (3),
+ ShowcaseSeeder packaging из названия (5), реальная Avalonia-касса + шапка (4), лингва-тест.
+- 08.10 — деплой api+web, RESET-пересев витрины, пересъёмка экранов, деплой сайта, галерея.
diff --git a/src/food-market.web/src/locales/ru.json b/src/food-market.web/src/locales/ru.json
index d406104..e14a157 100644
--- a/src/food-market.web/src/locales/ru.json
+++ b/src/food-market.web/src/locales/ru.json
@@ -146,7 +146,7 @@
"orgTitle": "Настройки организации",
"orgDescription": "Страна, валюта, ставка НДС по умолчанию.",
"telegramTitle": "📨 Telegram владельца",
- "telegramDescription": "Ежедневная сводка (выручка вчера, топ-3 товара, low-stock) в 09:00 МСК.",
+ "telegramDescription": "Ежедневная сводка (выручка вчера, топ-3 товара, низкий остаток) в 09:00 МСК.",
"telegramBotDisabled": "Бот не настроен на сервере (нет токена в env). Привязка пока недоступна.",
"telegramBound": "Привязан: chat_id={{id}}",
"telegramUnbind": "Отвязать",
diff --git a/src/food-market.web/src/pages/OrganizationSettingsPage.tsx b/src/food-market.web/src/pages/OrganizationSettingsPage.tsx
index 22f6f0a..c866074 100644
--- a/src/food-market.web/src/pages/OrganizationSettingsPage.tsx
+++ b/src/food-market.web/src/pages/OrganizationSettingsPage.tsx
@@ -265,7 +265,7 @@ function TelegramSection() {
📨 Telegram владельца
- Ежедневная сводка (выручка вчера, топ-3 товара, low-stock) в 09:00 МСК.
+ Ежедневная сводка (выручка вчера, топ-3 товара, низкий остаток) в 09:00 МСК.
{!s?.botEnabled && (
diff --git a/tests/e2e/scenarios/stage-ui-menu-walk.spec.ts b/tests/e2e/scenarios/stage-ui-menu-walk.spec.ts
index 82ba09c..5ba6915 100644
--- a/tests/e2e/scenarios/stage-ui-menu-walk.spec.ts
+++ b/tests/e2e/scenarios/stage-ui-menu-walk.spec.ts
@@ -94,6 +94,32 @@ async function assertNoErrorToasts(page: Page, where: string) {
}
}
+// Лингва-проверка: в видимом тексте страницы не должно быть непереведённого
+// UI-английского. Проверяем узкий denylist именно «хромовых» строк интерфейса
+// (заголовок/виджет/статус), а НЕ «любое латинское слово» — иначе ловили бы
+// легитимные бренды товаров (Lay's, Snickers, Kaspi), единицы, названия клавиш
+// (Delete/Enter/Esc) и технические идентификаторы в доке. Denylist ловит класс
+// регрессий «забыли перевести строку интерфейса».
+const UI_ENGLISH_DENY: RegExp[] = [
+ /\bDashboard\b/, // заголовок страницы (должно быть «Сводка»)
+ /Low-stock alerts/i, // заголовок виджета (должно быть «Низкий остаток»)
+ /Live-обновл/, // латинское «Live» в русской строке
+ /\blive\b/, /\boffline\b/, // статус-чип (должно быть «онлайн»/«офлайн»)
+]
+// Документация/диагностика содержат названия клавиш, код-идентификаторы и
+// технические термины — там латиница легитимна, лингва-проверку не применяем.
+const LINGUA_SKIP = ['/help', '/whats-new', '/admin/diagnostic']
+async function assertNoUiEnglish(page: Page, where: string, href: string) {
+ if (LINGUA_SKIP.some((p) => href.startsWith(p))) return
+ const bodyText = await page.locator('body').innerText()
+ const hits = UI_ENGLISH_DENY
+ .map((re) => bodyText.match(re)?.[0])
+ .filter((x): x is string => !!x)
+ if (hits.length > 0) {
+ throw new Error(`Непереведённый UI-английский на ${where}: ${[...new Set(hits)].join(', ')}`)
+ }
+}
+
// --------------------------------------------------------------------------
test.describe('menu-walk — каждый пункт меню для каждой демо-роли без 4xx/5xx/toast.error/console.error', () => {
@@ -131,6 +157,7 @@ test.describe('menu-walk — каждый пункт меню для каждо
const bodyText = await page.locator('body').innerText()
expect(bodyText.length, `body на ${where}`).toBeGreaterThan(MIN_BODY_LEN)
await assertNoErrorToasts(page, where)
+ await assertNoUiEnglish(page, where, href)
// Теперь — reload (F5): это тот сценарий, где владелец словил 403-тосты.
await page.reload({ waitUntil: 'domcontentloaded' })