# FN-SESSION — диагностика и фикс (22.06.2026)

> ✅ **УТОЧНЕНИЕ 23.06.2026 (подтверждено владельцем):** остановка лидов с 3 июня = **владелец
> сам выключил проекты** (намеренно) — по сути «бизнес-решение, не дефект» ВЕРНО. Неточны были
> лишь: (1) ярлык «омега» — выключены проекты главного клиента **tenant 2**, не только омеги;
> (2) фраза «к FN-SESSION отношения не имеет» — браузер был реально сломан 4–21.06 (отдельная
> уже починенная проблема). Технический разбор и доказательства прода —
> [2026-06-23-LEADS-STOPPED-correction.md](../findings/2026-06-23-balance-block/2026-06-23-LEADS-STOPPED-correction.md).

> Разбор находки **FN-SESSION** из приёмки боевого 22.06. Диагностика на проде —
> строго read-only (`ssh liderra-prod`). Кодовая фраза стены: «роутер-наставник».

## Вердикт

> ✅ Уточнено 23.06 — см. баннер вверху файла.

**FN-SESSION — это была проблема окружения (отсутствовал исполняемый файл браузера),
уже устранённая переустановкой 21.06, ПЛЮС код-баг диагностируемости (исправлен этой
сессией по TDD).** К «лиды не идут с 3 июня» — **это намеренное решение владельца** (он сам
выключил проекты, подтверждено 23.06), не дефект. Уточнение: «омега» → выключены проекты
главного клиента **tenant 2**; и FN-SESSION (сломанный браузер 4–21.06) — отдельная реальная,
уже починенная проблема, накрывавшая тот же период.

## Что происходило (14–21.06, ~121×/день)

Playwright-скрипт `app/playwright/refresh-session.js` падал, потому что в кэше
браузеров пользователя www-data **не было исполняемого файла** headless-shell:

```
browserType.launch: Executable doesn't exist at
/var/www/.cache/ms-playwright/chromium_headless_shell-1217/chrome-headless-shell-linux64/chrome-headless-shell
```

`chromium.launch()` отклонялся. И вот **почему это было трудно диагностировать**: в коде
`const browser = await chromium.launch()` стоял **вне** `try/catch`, а `refresh(args)`
вызывался **без `.catch()`**. Поэтому отказ запуска браузера превращался в
**unhandled promise rejection → Node выходил с кодом 1** и сырым стеком в stderr —
**неотличимо** от честного «login rejected» (тоже exit 1). По «exit code 1» нельзя было
понять: сломан браузер или отклонён логин.

## Каденс (почему 121×/день)

`RefreshSupplierSessionJob`: `Schedule::hourly()` + `dailyAt('17:45')` + инлайн
`dispatch_sync()` из `SupplierPortalClient` на 401/403/HTML-логин-страницу.
`$tries=3` с backoff. Итого ~72/день от hourly + инлайн-ретраи.

## Текущее состояние (после переустановки 21.06 12:36) — read-only улики

| Проверка | Результат |
|---|---|
| Исполняемый файл браузера | **на месте** (`chrome-headless-shell` 185 МБ, `-rwxr-xr-x`, www-data, точный ожидаемый путь) |
| Ошибки «PlaywrightBridge exit code» 21.06 после 12:36 и за всё 22.06 | **НЕТ** — missing-binary прекратился |
| Воркер | systemd `liderra-queue.service`, `User=www-data`, HOME консистентен с кэшем |
| Supplier-интеграция 22.06 | **жива** — в 02:55 идут аутентифицированные вызовы `SyncSupplierProjectJob` к `/admin/visit/rt-project-save` |

### Разъяснение «`supplier.session.refreshed` = 0 за весь лог»

Это **не сбой**. `supplier.session.refreshed` — это `Log::info`. На проде
`production.INFO`-строк в логе **0** (`LOG_LEVEL >= warning`) → INFO просто не пишется.
Что refresh работает — доказано тем, что tier-1 AJAX `rt-project-save` 22.06 доходит до
поставщика и получает бизнес-конверт (`status=Doubles`, HTTP 200): это возможно только
с валидной сессией из Redis (`SupplierPortalClient::loadSession()`).

## Что исправлено этой сессией (код, по TDD, проверено глазами)

Сделана **диагностируемость**: отказ запуска браузера теперь даёт **чистый exit 4 +
JSON `{error}` в stderr** вместо опасного unhandled-rejection exit 1.

- `app/playwright/refresh-session.js` — `chromium.launch()` внесён в `try`, `browser.close()`
  под guard, добавлен top-level `.catch()`.
- `app/playwright/manage-project.js` — тот же баг, тот же фикс.
- Тесты (`node:test`): `app/playwright/refresh-session.test.js` (новый),
  `app/playwright/manage-project.test.js` (+тест launch-failure). Все зелёные.
- Живая проверка: форсированный отказ запуска (битый `PLAYWRIGHT_BROWSERS_PATH`) →
  **exit 4 + JSON** (было exit 1 + сырой стек). PHP Supplier-тесты 71/71.

После фикса `SupplierAuthException` примет вид
`PlaywrightBridge exit code 4: {"error":"browserType.launch: Executable doesn't exist…"}`
— сразу ясно, что это отказ запуска браузера, а не отклонённый логин.

## Ops-рекомендация (корень рецидива) — действие ВЛАДЕЛЬЦА на проде

Браузер пропал из кэша www-data почти наверняка из-за деплоя, который обновлял/чистил
`node_modules` без переустановки браузеров под www-data. Чтобы не повторялось:

1. В пайплайн деплоя добавить шаг **от имени www-data** (HOME=/var/www):
   `npx playwright install chromium-headless-shell` (или `chromium`) — после `npm ci`.
2. Версия браузера привязана к версии npm-пакета `playwright` (сейчас headless-shell
   `-1217`). При обновлении `playwright` обязательно переустановить браузеры.
3. Желательно — health-check: алёрт, если `RefreshSupplierSessionJob` падает N раз подряд
   (сейчас падения видны только как рост `failed_jobs` + ERROR-строки).

## Поправки к отчёту приёмки

В `docs/superpowers/specs/2026-06-21-acceptance-RUN-report.md` (находки FN-SESSION):

- «Переустановка браузера 21.06 НЕ вылечила» — **неверно**: вылечила (ошибок после
  21.06 12:36 нет). Отчёт смотрел на ошибки ДО переустановки.
- «Вероятно связана с лиды не идут» — **уточнено 23.06:** лиды не идут = **владелец сам выключил
  проекты** (намеренно, подтверждено), не дефект. FN-SESSION (сломанный браузер 4–21.06) — отдельная
  реальная, уже починенная проблема того же периода; не первопричина (первопричина — решение
  владельца). Уточнение ярлыка: выключены проекты главного клиента **tenant 2**, не только омеги.
  Контекст — [2026-06-23-LEADS-STOPPED-correction.md](../findings/2026-06-23-balance-block/2026-06-23-LEADS-STOPPED-correction.md).

## Не сделано (вне объёма)

- Прод не трогался (только read-only диагностика). Код-фикс — локально, не закоммичен,
  не выкачен (ждёт слова владельца; пуш — в gitea).
- Ops-шаг (playwright install в деплое) — действие владельца на проде.
