docs(site): отчёт и скриншоты проверки круга 2 (44/44) + verify-скрипт
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions

Проверка на живом test.food-market.kz после деплоя: React #418 ушёл, дефолтный
«Бизнес» 10 000, чужих систем учёта 0 (Kaspi/маркетплейсы только «в разработке»),
типографика KB, sitemap со слэшами, мобильные правки (Android «Назад», ползунки
−/+, состав панели), честные /status и /kb/billing.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-11 01:29:07 +05:00
parent 6f229ebecc
commit 5c5f92de52
17 changed files with 680 additions and 0 deletions

View file

@ -0,0 +1,155 @@
# Исправления публичного сайта test.food-market.kz — круг 2 (11.10.2026)
**Деплой:** только тестовый сайт через `~/.fm-watchdog/deploy-public-test.sh`
(commit `6f229eb`, статус `OK`). Прод, gateway nginx, прод-БД — не трогались.
**Образ отката:** `192.168.1.193:5001/food-market-public:test-pre-site-fixes-2`
(создан из текущего `:test` ДО деплоя). Откат: перетегировать его в `:test` и
`docker compose up -d public` на prod-vm.
**Автопроверка (Playwright, как человек):** 44/44 — см. `index.html` (desktop 1440
@2x + mobile 390 @3x, 13 скриншотов), сырые данные — `verify.json`.
---
## Уточнение поручения владельца по брендам (важно)
Было два сообщения владельца. Итоговая (уточнённая) трактовка, по которой сделано:
- **Убрать со всего сайта — ТОЛЬКО чужие СИСТЕМЫ УЧЁТА** рознцы (конкуренты):
«МойСклад» / «Мой склад» / MoySklad, «1С» / 1C. Grep по `dist`: **0 вхождений**
вне `/legal/*` (в `/legal/*` их и нет — проверено).
- **Платёжные системы, банки, маркетплейсы** (Kaspi Pay, Kaspi Magazin, Halyk,
Jusan, Forte, Wildberries, Ozon, Visa, Mastercard) — **НЕ** чужие системы учёта,
удалять не требовалось. Оставлены **только как честная пометка «в разработке»**
на `/integrations` и главной (без дат и обещаний), нигде — как готовая функция.
- Промежуточное (ошибочно расширенное) удаление Kaspi/маркетплейсов со страницы
`/integrations` **откатил** — вернул их в блок «В разработке» в прежнем виде
(Kaspi Pay, Kaspi Magazin, Ozon, Wildberries; ОФД РК).
- Из KB/блога/FAQ обещания уже подключённых способов оплаты/интеграций всё равно
убраны — но по причине «**этого нет в продукте**», а не «чужой бренд».
Где Kaspi/маркетплейсы остались (проверено): `/integrations` (блок «В разработке»)
и главная (FAQ — «в планах, сроков не называем»). Больше нигде.
---
## Сделано — по пунктам круга 2
### 1. Оставшиеся «ложные обещания» / честность текстов
- **`/kb/pos-setup`** — касса для **Windows и Linux**; вход = адрес сервера + логин
+ PIN (не «ключ организации»); установка «скоро»; принтеры «58/80 мм (ESC/POS)».
- **`/blog/quick-start`** — убран дубль `# H1`; «Импорт через API» → «Импорт из
других систем учёта — в разработке»; шаги установки .msi/ключ → сервер+логин+PIN;
меню лояльности/маркетинга → «раздел «Акции и скидки»», «лояльность — в расширенном
режиме»; обложка — скриншот продукта (не чужое стоковое фото).
- **`/blog/launch`** — убран дубль H1; «триал» → «честный бесплатный период»;
«Касса для Windows» → «Windows и Linux … приложение — скоро»; маркетплейсы/оплаты
→ «в разработке» (без дат); «все возможности CRM/лояльность/…» → «без доплат за
модули».
- **`/kb/faq`** — бэкапы «каждые 24 ч» → «регулярные»; «TLS 1.3» → «TLS» (без
версии); формулировка про ЗРК смягчена до «в соответствии с законодательством РК»;
импорт — файлы Excel/CSV, из других систем «в разработке»; **«триал» → «бесплатный
период»** (унифицировано); «с SLA» убрано.
- **Унификация «триал» → «бесплатный период»** по сайту (og.mjs тоже). В `/legal/*`
термин «Триал» оставлен как есть (юр. документы не трогаем).
- **og-картинки** перегенерированы (`node scripts/og.mjs`): без чужих систем учёта;
касса «Windows и Linux»; «90 дней бесплатно».
### 2. React #418 (hydration mismatch) на `/signup`
`SignupForm.tsx`: `?plan=` и параметры конфигуратора теперь читаются в `useEffect`
**после** гидрации, поэтому первый клиентский рендер совпадает с SSR-разметкой.
Проверка консоли на `/`, `/pricing`, `/signup?plan=business&…` и `/signup?plan=business`
— **ошибок нет, #418 отсутствует** (8/8 авто-проверок).
### 3. `/signup` без параметров конфигуратора
- `plan=business` без параметров → выбран **«Бизнес»** с дефолтной конфигурацией
**1/1/1/5 = 10 000 ₸** (показана карточка «Ваша конфигурация»). ✓ проверено.
- `plan=network` → «Сеть»; неизвестный план → «Старт». ✓
- Клик по плитке тарифа обновляет URL (`history.replaceState`).
- Плитка «Старт»: «5 000 ₸» → **«5 000 ₸/мес»**; цены плиток без переноса.
### 4. Типографика KB/блога
- `.prose-md` (h2/h3, списки, таблицы, ссылки, код, цитаты) вынесена в
`global.css` — грузится на всех страницах; `/kb/*` больше **не плоский текст**
(раньше стиль был только на странице блога). ✓ проверено (`h2`, `ul li` в
`.prose-md`).
- Блог: убран дублирующий `<h1>` (в теле статей) — на странице ровно один H1. ✓
### 5. Мелочи
- **sitemap.xml** — канонические URL **со слэшем** (убраны 301), добавлены статьи
KB и блога. ✓
- **Footer** «Status» → **«Статус»**.
- **`/changelog`** — убран внутренний жаргон (Phase N, SuperAdmin, retention
period, tenant, inline, Read-only/Edit-mode) → человеческие формулировки;
заголовок «Что нового». ✓
- **`/pricing`** — на конструкторе «Бизнес» бейдж **«Популярный»** (как на главной)
+ **«90 дней бесплатно»**; у «Старт» убран ложный пункт «Импорт из других систем».
- Шапка ~1050px: десктоп-меню переведено на `xl` (до 1280px — гамбургер), CTA без
переноса → на ~1050px и на мобильном меню/кнопка **не ломаются в 2 строки**. ✓
- Инлайн-ссылки в тексте — увеличенная зона нажатия (padding без сдвига вёрстки).
- Эмодзи-фолбэк-стек в `body` (чтобы эмодзи не были квадратами там, где нет
цветного emoji-шрифта).
### 6. Не трогал
`/legal/*` и контакты-плейсхолдеры («[НАЗВАНИЕ ТОО]», «[БИН]», «+7 (___)») — без
изменений. Бренд ALL MARKET, цвета, шрифты — без изменений.
---
## Дополнение по тесту на Android (Firefox 157) — сделано
1. **Мобильное меню + кнопка «Назад» Android.** При открытии панели кладём запись
в историю (`pushState`), закрываем по `popstate` → системная «Назад» закрывает
меню, а не уводит со страницы. Esc/тап-вне/крестик остались. ✓ проверено
(`m-after-back.png` — после «Назад» панель закрыта, остались на `/`).
2. **Состав мобильной панели.** Добавлены «Интеграции» (в основную навигацию) и
внизу панели вторичные ссылки **«Статус»** и **«Что нового»** (раньше были только
в футере). ✓
3. **«Настроить тариф» (главная)** → `/pricing#configurator` со скроллом к
конструктору (у блока `id="configurator"` + `scroll-mt-20`). ✓ якорь есть.
4. **Ползунки конструктора на тач.** Крупная ручка (28px), `touch-action: pan-y`
на инпуте (вертикальный свайп скроллит страницу, а не двигает значение), и
кнопки **«−/+»** у каждого значения (менять без перетаскивания). ✓ проверено.
5. **`/kb/billing`.** Проверено по коду `src/food-market.*`: биллинг/подписки в
продукте **нет** (в вебе явная заглушка «до Phase 4 «Подписки»»; единственная
сущность `Subscription` — это `WebPushSubscription`, push-уведомления). Поэтому
убраны несуществующие: «Настройки → Подписка → История» (скачивание счетов/чеков),
e-mail-напоминания, баннер в админке, формула кредита при понижении, SLA для
«Сеть». Переписано честно: оплата на раннем доступе — вручную через поддержку,
онлайн-оплата и личный кабинет «в разработке». Цены «тг» → **«₸»**. ✓
6. **`/status`.** Убраны фиктивные зелёные «● Работает» (мониторинга нет) — честная
страница «в разработке» + контакты для инцидентов (реальный `/health` не
подключал: это требует серверной части, вне правки статического сайта). ✓
---
## Решения, требующие внимания владельца
1. **Контракт API регистрации.** Форма отправляет `plan`, а для «Бизнес» ещё и
`shops/cashes/warehouses/employees` (вперёд-совместимо). **Текущий серверный
контракт `SignupInput` принимает только `plan`** — конфигурация конструктора на
бэкенде пока игнорируется. Нужно решение: принимать ли её на сервере (тогда —
отдельная задача по API) или оставить как сейчас.
2. **Оборудование на `/integrations`.** В «Уже работает» вернул конкретику «весы
CAS, Масса-К и совместимые» (это производители **весов**, не системы учёта, —
под уточнённое поручение не попадают). Если хотите — заменю на нейтральное
«популярные протоколы».
3. **Юр. документы / реквизиты.** В `/legal/*` чужих систем учёта и брендов нет —
список пуст, ничего выносить не требуется. Плейсхолдеры реквизитов не трогал.
---
## Проверка (как в задаче)
| Критерий | Результат |
|---|---|
| Консоль чистая на `/`, `/pricing`, `/signup` (вариации), React #418 нет | ✓ |
| `plan=business` без параметров → «Бизнес» 10 000 (1/1/1/5) | ✓ |
| `dist`/живой сайт: 0 «МойСклад/Мой склад/moysklad/1С/1C» вне `/legal/*` | ✓ |
| Kaspi/маркетплейсы — только «в разработке» на `/integrations` и главной | ✓ |
| Унификация «бесплатный период»; «тг» → «₸» в KB | ✓ |
| Типографика KB (h2/списки) + один H1 в блоге | ✓ |
| sitemap — канонические URL со слэшем + KB/блог | ✓ |
| Моб.: «Назад» закрывает меню; состав панели; ползунки −/+ | ✓ |
| `/status` без фиктивного статуса; `/changelog` без жаргона; `/kb/billing` честный | ✓ |
Итог авто-проверки: **44/44**.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 527 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 728 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

View file

@ -0,0 +1,65 @@
<!doctype html><meta charset="utf-8"><title>Исправления сайта 2026-10-11 (круг 2)</title>
<style>body{font:15px system-ui;margin:0;padding:24px;background:#f8fafc;color:#0f172a;max-width:1100px;margin:auto}
h1{font-size:22px} figure{margin:0 0 24px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
figcaption{padding:8px 12px;font-size:13px;color:#475569;background:#f1f5f9} img{display:block;width:100%}
ul{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:14px 32px} li{margin:4px 0}
.ok{color:#16a34a} .no{color:#dc2626;font-weight:700} .dt{color:#64748b;font-size:12px}</style>
<h1>Исправления публичного сайта test.food-market.kz — круг 2 (2026-10-10)</h1>
<p>Проверка после деплоя (2026-10-10T20:27:26.438Z). Пройдено: 44/44.</p>
<h2>Автопроверки</h2><ul><li class="ok">✓ консоль чистая: главная</li>
<li class="ok">✓ нет React #418 (hydration): главная</li>
<li class="ok">✓ консоль чистая: /pricing</li>
<li class="ok">✓ нет React #418 (hydration): /pricing</li>
<li class="ok">✓ консоль чистая: /signup?plan=business+params</li>
<li class="ok">✓ нет React #418 (hydration): /signup?plan=business+params</li>
<li class="ok">✓ консоль чистая: /signup?plan=business (без параметров)</li>
<li class="ok">✓ нет React #418 (hydration): /signup?plan=business (без параметров)</li>
<li class="ok">✓ plan=business без параметров: выбран «Бизнес»</li>
<li class="ok">✓ plan=business по умолчанию: цена 10 000 — <span class="dt">Вашаконфигурация«Бизнес»10000₸/месМагази</span></li>
<li class="ok">✓ plan=business по умолчанию: Магазинов 1 / Складов 1 / Сотрудников 5</li>
<li class="ok">✓ plan=network: выбран «Сеть»</li>
<li class="ok">✓ plan=xyz: откат на «Старт»</li>
<li class="ok">✓ плитка «Старт»: цена «5 000 ₸/мес» — <span class="dt">Старт5000₸/мес</span></li>
<li class="ok">✓ KB: markdown отрисован (h2 в .prose-md)</li>
<li class="ok">✓ KB: списки отрисованы (ul li)</li>
<li class="ok">✓ KB billing: нет «Настройки → Подписка → История»</li>
<li class="ok">✓ KB billing: нет «тг», цены в «₸»</li>
<li class="ok">✓ блог quick-start: ровно один <h1> — <span class="dt">h1=1</span></li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): главная</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /integrations</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /import</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /kb/faq</li>
<li class="ok">✓ нет чужих систем учёта (МойСклад/1С): /blog/launch</li>
<li class="ok">✓ /integrations: Kaspi присутствует (в блоке «в разработке»)</li>
<li class="ok">✓ /integrations: блок «В разработке» есть</li>
<li class="ok">✓ /pricing: бейдж «Популярный» на конструкторе</li>
<li class="ok">✓ /pricing: «90 дней бесплатно» в конструкторе</li>
<li class="ok">✓ /pricing: якорь #configurator есть</li>
<li class="ok">✓ /pricing: ползунки с кнопками −/+</li>
<li class="ok">✓ /changelog: без жаргона (Phase/SuperAdmin/retention/tenant)</li>
<li class="ok">✓ /status: нет фиктивного «● Работает»</li>
<li class="ok">✓ /status: честная пометка «в разработке»</li>
<li class="ok">✓ sitemap: канонические URL со слэшем</li>
<li class="ok">✓ sitemap: статьи KB включены</li>
<li class="ok">✓ sitemap: статьи блога включены</li>
<li class="ok">✓ моб. главная: нет горизонтальной прокрутки</li>
<li class="ok">✓ моб. шапка: CTA «Начать бесплатно» в одну строку — <span class="dt">h=44px</span></li>
<li class="ok">✓ моб. меню открывается</li>
<li class="ok">✓ моб. меню: есть «Интеграции»</li>
<li class="ok">✓ моб. меню: есть «Статус»</li>
<li class="ok">✓ моб. меню: есть «Что нового» (changelog)</li>
<li class="ok">✓ моб. «Назад» закрывает меню (не уводит со страницы) — <span class="dt">https://test.food-market.kz/</span></li>
<li class="ok">✓ моб. /pricing: кнопки −/+ у ползунков</li></ul>
<h2>Скриншоты</h2><figure><figcaption>Десктоп 1440: /signup?plan=business без параметров — «Бизнес» с дефолтом 1/1/1/5 = 10 000 ₸</figcaption><img src="d-signup-business-default.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: KB «Тарифы и оплата» — markdown-типографика, честный текст (без несуществующего биллинг-UI), ₸</figcaption><img src="d-kb-billing.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: KB FAQ — типографика, «бесплатный период», TLS без версии</figcaption><img src="d-kb-faq.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: Блог «Быстрый старт» — один заголовок, без чужих систем учёта</figcaption><img src="d-blog-quickstart.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /integrations — платёжки/маркетплейсы только в «В разработке»; «уже работает» без чужих систем учёта</figcaption><img src="d-integrations.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /pricing — конструктор «Бизнес»: бейдж «Популярный», «90 дней бесплатно», кнопки −/+</figcaption><img src="d-pricing.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /changelog («Что нового») — человеческие формулировки без внутреннего жаргона</figcaption><img src="d-changelog.png" loading="lazy"></figure>
<figure><figcaption>Десктоп 1440: /status — без фиктивных зелёных индикаторов, честно «в разработке» + контакты</figcaption><img src="d-status.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. главная 390px — CTA не переносится, гамбургер в шапке</figcaption><img src="m-home-closed.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. меню — полный состав + Статус/Что нового внизу</figcaption><img src="m-menu-open.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. после системной «Назад» — панель закрыта, остались на главной</figcaption><img src="m-after-back.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. конструктор — крупные ручки + кнопки −/+ (значение меняется кнопкой, не случайным свайпом)</figcaption><img src="m-pricing-configurator.png" loading="lazy"></figure>
<figure><figcaption>Моб. 390: Моб. /signup?plan=business без параметров — «Бизнес» с дефолтом 10 000 ₸</figcaption><img src="m-signup-business-default.png" loading="lazy"></figure>

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

View file

@ -0,0 +1,291 @@
{
"results": [
{
"name": "консоль чистая: главная",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): главная",
"ok": true,
"detail": ""
},
{
"name": "консоль чистая: /pricing",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): /pricing",
"ok": true,
"detail": ""
},
{
"name": "консоль чистая: /signup?plan=business+params",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): /signup?plan=business+params",
"ok": true,
"detail": ""
},
{
"name": "консоль чистая: /signup?plan=business (без параметров)",
"ok": true,
"detail": ""
},
{
"name": "нет React #418 (hydration): /signup?plan=business (без параметров)",
"ok": true,
"detail": ""
},
{
"name": "plan=business без параметров: выбран «Бизнес»",
"ok": true,
"detail": ""
},
{
"name": "plan=business по умолчанию: цена 10 000",
"ok": true,
"detail": "Вашаконфигурация«Бизнес»10000₸/месМагази"
},
{
"name": "plan=business по умолчанию: Магазинов 1 / Складов 1 / Сотрудников 5",
"ok": true,
"detail": ""
},
{
"name": "plan=network: выбран «Сеть»",
"ok": true,
"detail": ""
},
{
"name": "plan=xyz: откат на «Старт»",
"ok": true,
"detail": ""
},
{
"name": "плитка «Старт»: цена «5 000 ₸/мес»",
"ok": true,
"detail": "Старт5000₸/мес"
},
{
"name": "KB: markdown отрисован (h2 в .prose-md)",
"ok": true,
"detail": ""
},
{
"name": "KB: списки отрисованы (ul li)",
"ok": true,
"detail": ""
},
{
"name": "KB billing: нет «Настройки → Подписка → История»",
"ok": true,
"detail": ""
},
{
"name": "KB billing: нет «тг», цены в «₸»",
"ok": true,
"detail": ""
},
{
"name": "блог quick-start: ровно один <h1>",
"ok": true,
"detail": "h1=1"
},
{
"name": "нет чужих систем учёта (МойСклад/1С): главная",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /integrations",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /import",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /kb/faq",
"ok": true,
"detail": ""
},
{
"name": "нет чужих систем учёта (МойСклад/1С): /blog/launch",
"ok": true,
"detail": ""
},
{
"name": "/integrations: Kaspi присутствует (в блоке «в разработке»)",
"ok": true,
"detail": ""
},
{
"name": "/integrations: блок «В разработке» есть",
"ok": true,
"detail": ""
},
{
"name": "/pricing: бейдж «Популярный» на конструкторе",
"ok": true,
"detail": ""
},
{
"name": "/pricing: «90 дней бесплатно» в конструкторе",
"ok": true,
"detail": ""
},
{
"name": "/pricing: якорь #configurator есть",
"ok": true,
"detail": ""
},
{
"name": "/pricing: ползунки с кнопками −/+",
"ok": true,
"detail": ""
},
{
"name": "/changelog: без жаргона (Phase/SuperAdmin/retention/tenant)",
"ok": true,
"detail": ""
},
{
"name": "/status: нет фиктивного «● Работает»",
"ok": true,
"detail": ""
},
{
"name": "/status: честная пометка «в разработке»",
"ok": true,
"detail": ""
},
{
"name": "sitemap: канонические URL со слэшем",
"ok": true,
"detail": ""
},
{
"name": "sitemap: статьи KB включены",
"ok": true,
"detail": ""
},
{
"name": "sitemap: статьи блога включены",
"ok": true,
"detail": ""
},
{
"name": "моб. главная: нет горизонтальной прокрутки",
"ok": true,
"detail": ""
},
{
"name": "моб. шапка: CTA «Начать бесплатно» в одну строку",
"ok": true,
"detail": "h=44px"
},
{
"name": "моб. меню открывается",
"ok": true,
"detail": ""
},
{
"name": "моб. меню: есть «Интеграции»",
"ok": true,
"detail": ""
},
{
"name": "моб. меню: есть «Статус»",
"ok": true,
"detail": ""
},
{
"name": "моб. меню: есть «Что нового» (changelog)",
"ok": true,
"detail": ""
},
{
"name": "моб. «Назад» закрывает меню (не уводит со страницы)",
"ok": true,
"detail": "https://test.food-market.kz/"
},
{
"name": "моб. /pricing: кнопки −/+ у ползунков",
"ok": true,
"detail": ""
}
],
"shots": [
{
"file": "d-signup-business-default.png",
"caption": "/signup?plan=business без параметров — «Бизнес» с дефолтом 1/1/1/5 = 10 000 ₸",
"kind": "Десктоп 1440"
},
{
"file": "d-kb-billing.png",
"caption": "KB «Тарифы и оплата» — markdown-типографика, честный текст (без несуществующего биллинг-UI), ₸",
"kind": "Десктоп 1440"
},
{
"file": "d-kb-faq.png",
"caption": "KB FAQ — типографика, «бесплатный период», TLS без версии",
"kind": "Десктоп 1440"
},
{
"file": "d-blog-quickstart.png",
"caption": "Блог «Быстрый старт» — один заголовок, без чужих систем учёта",
"kind": "Десктоп 1440"
},
{
"file": "d-integrations.png",
"caption": "/integrations — платёжки/маркетплейсы только в «В разработке»; «уже работает» без чужих систем учёта",
"kind": "Десктоп 1440"
},
{
"file": "d-pricing.png",
"caption": "/pricing — конструктор «Бизнес»: бейдж «Популярный», «90 дней бесплатно», кнопки −/+",
"kind": "Десктоп 1440"
},
{
"file": "d-changelog.png",
"caption": "/changelog («Что нового») — человеческие формулировки без внутреннего жаргона",
"kind": "Десктоп 1440"
},
{
"file": "d-status.png",
"caption": "/status — без фиктивных зелёных индикаторов, честно «в разработке» + контакты",
"kind": "Десктоп 1440"
},
{
"file": "m-home-closed.png",
"caption": "Моб. главная 390px — CTA не переносится, гамбургер в шапке",
"kind": "Моб. 390"
},
{
"file": "m-menu-open.png",
"caption": "Моб. меню — полный состав + Статус/Что нового внизу",
"kind": "Моб. 390"
},
{
"file": "m-after-back.png",
"caption": "Моб. после системной «Назад» — панель закрыта, остались на главной",
"kind": "Моб. 390"
},
{
"file": "m-pricing-configurator.png",
"caption": "Моб. конструктор — крупные ручки + кнопки −/+ (значение меняется кнопкой, не случайным свайпом)",
"kind": "Моб. 390"
},
{
"file": "m-signup-business-default.png",
"caption": "Моб. /signup?plan=business без параметров — «Бизнес» с дефолтом 10 000 ₸",
"kind": "Моб. 390"
}
]
}

View file

@ -0,0 +1,169 @@
// Проверка второго круга исправлений публичного сайта test.food-market.kz
// (как человек: Playwright, десктоп 1440 + моб. 390). Вывод — docs/site-fixes-2026-10-11/.
import { chromium } from '@playwright/test'
import { mkdirSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
const BASE = process.env.SITE_URL ?? 'https://test.food-market.kz'
const OUT = resolve('/home/nns/food-market', 'docs/site-fixes-2026-10-11')
mkdirSync(OUT, { recursive: true })
const results = []
const shots = []
const ok = (name, cond, detail = '') => { results.push({ name, ok: !!cond, detail }); console.log(`${cond ? '✓' : '✗'} ${name} ${detail}`) }
const norm = (s) => (s || '').replace(/[\s    ]/g, '')
const browser = await chromium.launch({ args: ['--ignore-certificate-errors'] })
// Сбор ошибок консоли/страницы по URL.
async function consoleErrorsFor(ctx, path) {
const errs = []
const p = await ctx.newPage()
p.on('console', (m) => { if (m.type() === 'error') errs.push(m.text()) })
p.on('pageerror', (e) => errs.push(String(e)))
await p.goto(BASE + path, { waitUntil: 'networkidle' }).catch(() => {})
await p.waitForTimeout(1200)
await p.close()
return errs
}
// ---------- DESKTOP 1440x900 @2x ----------
const dctx = await browser.newContext({ viewport: { width: 1440, height: 900 }, deviceScaleFactor: 2, ignoreHTTPSErrors: true })
const d = await dctx.newPage()
async function shotD(path, file, caption) {
await d.goto(BASE + path, { waitUntil: 'networkidle' }).catch(() => {})
await d.waitForTimeout(700)
await d.screenshot({ path: resolve(OUT, file) })
shots.push({ file, caption, kind: 'Десктоп 1440' })
}
// #2 — консоль чистая (React #418 ушёл)
for (const [path, label] of [['/', 'главная'], ['/pricing', '/pricing'], ['/signup?plan=business&shops=3&cashes=4&warehouses=1&employees=5', '/signup?plan=business+params'], ['/signup?plan=business', '/signup?plan=business (без параметров)']]) {
const errs = await consoleErrorsFor(dctx, path)
const react418 = errs.filter((e) => /#418|hydrat|Minified React error/i.test(e))
ok(`консоль чистая: ${label}`, errs.length === 0, errs.length ? errs.slice(0, 2).join(' | ') : '')
ok(`нет React #418 (hydration): ${label}`, react418.length === 0, react418.join(' | '))
}
// #3 — plan=business без параметров → «Бизнес» + дефолтная конфигурация 1/1/1/5 = 10 000
await d.goto(`${BASE}/signup?plan=business`, { waitUntil: 'networkidle' }).catch(() => {})
await d.waitForTimeout(1300)
ok('plan=business без параметров: выбран «Бизнес»', await d.locator('label:has(input[name="plan"]:checked):has-text("Бизнес")').count() > 0)
const cfg = (await d.locator('div:has-text("Ваша конфигурация"):has-text("Магазинов")').last().innerText().catch(() => '')) || ''
ok('plan=business по умолчанию: цена 10 000', norm(cfg).includes('10000'), norm(cfg).slice(0, 40))
ok('plan=business по умолчанию: Магазинов 1 / Складов 1 / Сотрудников 5', /Магазинов:\s*1/.test(cfg) && /Складов:\s*1/.test(cfg) && /Сотрудников:\s*5/.test(cfg))
await d.screenshot({ path: resolve(OUT, 'd-signup-business-default.png'), fullPage: true }); shots.push({ file: 'd-signup-business-default.png', caption: '/signup?plan=business без параметров — «Бизнес» с дефолтом 1/1/1/5 = 10 000 ₸', kind: 'Десктоп 1440' })
// plan=network → «Сеть»; plan=xyz → «Старт»; plan=start tile «5 000 ₸/мес»
await d.goto(`${BASE}/signup?plan=network`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(1200)
ok('plan=network: выбран «Сеть»', await d.locator('label:has(input[name="plan"]:checked):has-text("Сеть")').count() > 0)
await d.goto(`${BASE}/signup?plan=xyz`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(1200)
ok('plan=xyz: откат на «Старт»', await d.locator('label:has(input[name="plan"]:checked):has-text("Старт")').count() > 0)
const startTile = norm(await d.locator('label:has(input[name="plan"]):has-text("Старт")').first().innerText().catch(() => ''))
ok('плитка «Старт»: цена «5 000 ₸/мес»', startTile.includes('5000₸/мес'), startTile)
// #4 — типографика KB (prose-md) + billing честный
await d.goto(`${BASE}/kb/billing`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(500)
ok('KB: markdown отрисован (h2 в .prose-md)', await d.locator('.prose-md h2').count() > 0)
ok('KB: списки отрисованы (ul li)', await d.locator('.prose-md ul li').count() > 0)
const billTxt = await d.locator('.prose-md').innerText().catch(() => '')
ok('KB billing: нет «Настройки → Подписка → История»', !/Настройки.{0,3}Подписка.{0,3}История/.test(billTxt))
ok('KB billing: нет «тг», цены в «₸»', !/\bтг\b/.test(billTxt) && billTxt.includes('₸'))
await d.screenshot({ path: resolve(OUT, 'd-kb-billing.png'), fullPage: true }); shots.push({ file: 'd-kb-billing.png', caption: 'KB «Тарифы и оплата» — markdown-типографика, честный текст (без несуществующего биллинг-UI), ₸', kind: 'Десктоп 1440' })
await shotD('/kb/faq', 'd-kb-faq.png', 'KB FAQ — типографика, «бесплатный период», TLS без версии')
// blog: один h1
await d.goto(`${BASE}/blog/quick-start`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(500)
ok('блог quick-start: ровно один <h1>', await d.locator('article h1, h1').count() === 1, `h1=${await d.locator('h1').count()}`)
await d.screenshot({ path: resolve(OUT, 'd-blog-quickstart.png'), fullPage: true }); shots.push({ file: 'd-blog-quickstart.png', caption: 'Блог «Быстрый старт» — один заголовок, без чужих систем учёта', kind: 'Десктоп 1440' })
// бренды: системы учёта 0, Kaspi только как «в разработке»
for (const [path, label] of [['/', 'главная'], ['/integrations', '/integrations'], ['/import', '/import'], ['/kb/faq', '/kb/faq'], ['/blog/launch', '/blog/launch']]) {
await d.goto(BASE + path, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(600)
const body = await d.locator('body').innerText().catch(() => '')
ok(`нет чужих систем учёта (МойСклад/1С): ${label}`, !/мойсклад|мой склад|moysklad|1с\b|1c\b/i.test(body), '')
}
await d.goto(`${BASE}/integrations`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(500)
const integ = await d.locator('body').innerText().catch(() => '')
ok('/integrations: Kaspi присутствует (в блоке «в разработке»)', /kaspi/i.test(integ))
ok('/integrations: блок «В разработке» есть', /в разработке/i.test(integ))
await d.screenshot({ path: resolve(OUT, 'd-integrations.png'), fullPage: true }); shots.push({ file: 'd-integrations.png', caption: '/integrations — платёжки/маркетплейсы только в «В разработке»; «уже работает» без чужих систем учёта', kind: 'Десктоп 1440' })
// #5 pricing: Популярный + 90 дней + конструктор id; changelog без жаргона; status без фикции
await d.goto(`${BASE}/pricing`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(700)
const priceBody = await d.locator('body').innerText().catch(() => '')
ok('/pricing: бейдж «Популярный» на конструкторе', await d.locator('#configurator :text("Популярный")').count() > 0)
ok('/pricing: «90 дней бесплатно» в конструкторе', /90 дней бесплатно/.test(priceBody))
ok('/pricing: якорь #configurator есть', await d.locator('#configurator').count() > 0)
ok('/pricing: ползунки с кнопками −/+', await d.locator('#configurator button[aria-label^="Увеличить"]').count() >= 3)
await d.screenshot({ path: resolve(OUT, 'd-pricing.png'), fullPage: true }); shots.push({ file: 'd-pricing.png', caption: '/pricing — конструктор «Бизнес»: бейдж «Популярный», «90 дней бесплатно», кнопки −/+', kind: 'Десктоп 1440' })
await d.goto(`${BASE}/changelog`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(400)
const chg = await d.locator('body').innerText().catch(() => '')
ok('/changelog: без жаргона (Phase/SuperAdmin/retention/tenant)', !/phase\s*\d|superadmin|retention|tenant/i.test(chg))
await d.screenshot({ path: resolve(OUT, 'd-changelog.png'), fullPage: true }); shots.push({ file: 'd-changelog.png', caption: '/changelog («Что нового») — человеческие формулировки без внутреннего жаргона', kind: 'Десктоп 1440' })
await d.goto(`${BASE}/status`, { waitUntil: 'networkidle' }).catch(() => {}); await d.waitForTimeout(400)
const st = await d.locator('body').innerText().catch(() => '')
ok('/status: нет фиктивного «● Работает»', !/●\s*Работает/.test(st))
ok('/status: честная пометка «в разработке»', /в разработке/i.test(st))
await d.screenshot({ path: resolve(OUT, 'd-status.png'), fullPage: true }); shots.push({ file: 'd-status.png', caption: '/status — без фиктивных зелёных индикаторов, честно «в разработке» + контакты', kind: 'Десктоп 1440' })
// sitemap — слэши + kb/blog
const sm = await (await dctx.request.get(`${BASE}/sitemap.xml`, { ignoreHTTPSErrors: true }).catch(() => null))?.text?.().catch(() => '') || ''
ok('sitemap: канонические URL со слэшем', /\/features\/<\/loc>/.test(sm) && !/\/features<\/loc>/.test(sm))
ok('sitemap: статьи KB включены', /\/kb\/billing\//.test(sm) && /\/kb\/faq\//.test(sm))
ok('sitemap: статьи блога включены', /\/blog\/launch\//.test(sm) && /\/blog\/quick-start\//.test(sm))
// ---------- MOBILE 390x844 @3x ----------
const mctx = await browser.newContext({ viewport: { width: 390, height: 844 }, deviceScaleFactor: 3, ignoreHTTPSErrors: true, isMobile: true, hasTouch: true })
const m = await mctx.newPage()
await m.goto(BASE + '/', { waitUntil: 'networkidle' }).catch(() => {}); await m.waitForTimeout(800)
ok('моб. главная: нет горизонтальной прокрутки', await m.evaluate(() => document.documentElement.scrollWidth <= window.innerWidth + 1))
// CTA «Начать бесплатно» в одну строку
const ctaH = await m.locator('header a[href="/signup"]').first().evaluate((el) => el.getBoundingClientRect().height).catch(() => 0)
ok('моб. шапка: CTA «Начать бесплатно» в одну строку', ctaH > 0 && ctaH < 56, `h=${Math.round(ctaH)}px`)
await m.screenshot({ path: resolve(OUT, 'm-home-closed.png') }); shots.push({ file: 'm-home-closed.png', caption: 'Моб. главная 390px — CTA не переносится, гамбургер в шапке', kind: 'Моб. 390' })
const burger = m.locator('#mobile-menu-btn')
await burger.click().catch(() => {}); await m.waitForTimeout(400)
ok('моб. меню открывается', await m.locator('#mobile-menu:not(.hidden)').count() > 0)
ok('моб. меню: есть «Интеграции»', await m.locator('#mobile-menu a[href="/integrations"]').count() > 0)
ok('моб. меню: есть «Статус»', await m.locator('#mobile-menu a[href="/status"]').count() > 0)
ok('моб. меню: есть «Что нового» (changelog)', await m.locator('#mobile-menu a[href="/changelog"]').count() > 0)
await m.screenshot({ path: resolve(OUT, 'm-menu-open.png') }); shots.push({ file: 'm-menu-open.png', caption: 'Моб. меню — полный состав + Статус/Что нового внизу', kind: 'Моб. 390' })
// #1 Android «Назад» закрывает меню (history.back при открытой панели)
const urlBefore = m.url()
await m.goBack().catch(() => {}); await m.waitForTimeout(500)
ok('моб. «Назад» закрывает меню (не уводит со страницы)', (await m.locator('#mobile-menu.hidden').count() > 0) && m.url().replace(/#.*$/, '') === urlBefore.replace(/#.*$/, ''), m.url())
await m.screenshot({ path: resolve(OUT, 'm-after-back.png') }); shots.push({ file: 'm-after-back.png', caption: 'Моб. после системной «Назад» — панель закрыта, остались на главной', kind: 'Моб. 390' })
// конструктор на моб. — кнопки −/+ и ручка
await m.goto(`${BASE}/pricing`, { waitUntil: 'networkidle' }).catch(() => {}); await m.waitForTimeout(700)
ok('моб. /pricing: кнопки −/+ у ползунков', await m.locator('#configurator button[aria-label^="Увеличить"]').count() >= 3)
await m.locator('#configurator button[aria-label^="Увеличить: Складов"]').first().click().catch(() => {}); await m.waitForTimeout(300)
await m.screenshot({ path: resolve(OUT, 'm-pricing-configurator.png'), fullPage: true }); shots.push({ file: 'm-pricing-configurator.png', caption: 'Моб. конструктор — крупные ручки + кнопки −/+ (значение меняется кнопкой, не случайным свайпом)', kind: 'Моб. 390' })
await m.goto(`${BASE}/signup?plan=business`, { waitUntil: 'networkidle' }).catch(() => {}); await m.waitForTimeout(1200)
await m.screenshot({ path: resolve(OUT, 'm-signup-business-default.png'), fullPage: true }); shots.push({ file: 'm-signup-business-default.png', caption: 'Моб. /signup?plan=business без параметров — «Бизнес» с дефолтом 10 000 ₸', kind: 'Моб. 390' })
await browser.close()
// index.html
const rows = shots.map((s) => `<figure><figcaption>${s.kind}: ${s.caption}</figcaption><img src="${s.file}" loading="lazy"></figure>`).join('\n')
const checks = results.map((r) => `<li class="${r.ok ? 'ok' : 'no'}">${r.ok ? '✓' : '✗'} ${r.name}${r.detail ? ` — <span class="dt">${r.detail}</span>` : ''}</li>`).join('\n')
const html = `<!doctype html><meta charset="utf-8"><title>Исправления сайта 2026-10-11 (круг 2)</title>
<style>body{font:15px system-ui;margin:0;padding:24px;background:#f8fafc;color:#0f172a;max-width:1100px;margin:auto}
h1{font-size:22px} figure{margin:0 0 24px;background:#fff;border:1px solid #e2e8f0;border-radius:10px;overflow:hidden}
figcaption{padding:8px 12px;font-size:13px;color:#475569;background:#f1f5f9} img{display:block;width:100%}
ul{background:#fff;border:1px solid #e2e8f0;border-radius:10px;padding:14px 32px} li{margin:4px 0}
.ok{color:#16a34a} .no{color:#dc2626;font-weight:700} .dt{color:#64748b;font-size:12px}</style>
<h1>Исправления публичного сайта test.food-market.kz — круг 2 (${new Date().toISOString().slice(0, 10)})</h1>
<p>Проверка после деплоя (${new Date().toISOString()}). Пройдено: ${results.filter((r) => r.ok).length}/${results.length}.</p>
<h2>Автопроверки</h2><ul>${checks}</ul>
<h2>Скриншоты</h2>${rows}`
writeFileSync(resolve(OUT, 'index.html'), html)
writeFileSync(resolve(OUT, 'verify.json'), JSON.stringify({ results, shots }, null, 2))
console.log(`\n[site-verify-2] ${results.filter((r) => r.ok).length}/${results.length} passed → ${OUT}/index.html`)
const failed = results.filter((r) => !r.ok)
if (failed.length) console.log('FAILED:', failed.map((f) => f.name).join('; '))