# Дизайн: честный и отключаемый heartbeat Дата: 2026-06-30 Статус: согласовано (ожидает финального ревью) ## Проблема Модуль скрытно отправляет на сервер разработчика (`pulse.telecart.pro`) сведения об окружении, где он запущен (heartbeat). Покупатель об этом не уведомляется и не может это отключить. Это нечестно по отношению к покупателю и создаёт риски (GDPR / 152-ФЗ, претензии маркетплейсов). ## Цель Сделать heartbeat **честным**: прозрачным и отключаемым, сохранив поведение по умолчанию. - **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда отправляется и зачем. - **Контроль:** отдельный переключатель, которым покупатель может выключить heartbeat. - **По умолчанию включено** (текущее поведение сохраняется, но становится видимым и управляемым). ## Границы (scope) - **Только heartbeat.** Остальной TeleCart Pulse (Telegram-рассылки, события кампаний) **не трогаем** — он гейтится наличием `pulse.api_key` и, по позиции владельца, прав не нарушает. - Концептуально heartbeat считаем частью фичи **TeleCart Pulse** (одна папка доков), но у него **отдельный собственный выключатель**, не завязанный на `pulse.api_key` и не завязанный на секрет подписи. Pulse и heartbeat включаются/выключаются независимо. ## Терминология - **heartbeat** — периодическая (раз в час по кэшу) отправка информации об окружении: `domain`, `bot_name`, `php_version`, `module_version`, `opencart_version`, `opencart_version_core`. Триггерится из SPA при открытии WebApp. - **TeleCart Pulse** — фича рассылок и аналитики кампаний; heartbeat живёт внутри её namespace в коде (`framework/TeleCartPulse/`). ## Текущее устройство (как есть) - `TeleCartPulseService::handleHeartbeat()` (`module/.../framework/TeleCartPulse/TeleCartPulseService.php:133`) — ставит кэш-флаг `telecart_pulse_heartbeat` (ttl 3600), дёргает `getMe()`, формирует payload и шлёт POST на `PULSE_API_HOST/heartbeat`. - Отправка фактически происходит только если есть `heartbeatPayloadSigner` (строки 168–170), который создаётся в провайдере при наличии секрета (`pulse.heartbeat_secret` / `PULSE_HEARTBEAT_SECRET`). - `TeleCartPulseServiceProvider` (`module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php:22-35`) читает значения из конфига и инжектит их в `TeleCartPulseService` через конструктор. - **Пользовательской (per-install) настройки для heartbeat сейчас нет.** ## Решение ### Принцип Использовать существующий паттерн: провайдер читает значение из конфига и **инжектит** его в сервис через конструктор (как уже делается для `pulse.api_key` и signer). Не вводить новых механизмов гейтинга. ### 1. Backend **`module/.../configs/app.php`** — секция `pulse` (строки 86–90): добавить флаг с дефолтом `true`. ```php 'pulse' => [ 'api_key' => '', 'heartbeat_enabled' => true, // новый флаг 'batch_size' => 50, 'max_attempts' => 3, ], ``` **`TeleCartPulseServiceProvider`** — прочитать `pulse.heartbeat_enabled` и передать булевый флаг в конструктор `TeleCartPulseService` (рядом с `apiKey` и `heartbeatSigner`). Привести к `bool` явно. **`TeleCartPulseService`** — принять флаг в конструкторе (новое поле `private bool $heartbeatEnabled`). В `handleHeartbeat()` — **ранний `return` в самом начале метода**, до проверки/установки кэша и до `getMe()`, если флаг выключен: ```php public function handleHeartbeat(): void { if (! $this->heartbeatEnabled) { return; } // ... существующая логика } ``` Совместимость с PHP 7.4: без named arguments / промоушена свойств / union-типов. Параметр конструктора — позиционный, со значением по умолчанию `true` для обратной совместимости вызовов в тестах. ### 2. Frontend (админка) + прозрачность **`frontend/admin/src/stores/settings.js`** — секция `pulse` (строки 89–92): добавить `heartbeat_enabled` в дефолтное состояние state. **`frontend/admin/src/views/TeleCartPulseView.vue`** — добавить `ItemBool` с переключателем, привязанным к `settings.items.pulse.heartbeat_enabled`. В слот-описание — **честное, но нейтральное по тону раскрытие**: - перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии OpenCart; - указать адрес назначения: `pulse.telecart.pro`; - описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке лицензии). Точную формулировку владелец отредактирует; черновик в нейтральном тоне готовит реализация. ### 3. Поведение для существующих установок Дефолт `true` в конфиге мержится с сохранёнными настройками (`ApplicationFactory`: `Arr::mergeArraysRecursively`). У установок без сохранённого значения heartbeat остаётся включённым — поведение не меняется, но теперь переключатель виден. Когда покупатель выключает тумблер и сохраняет, значение пишется в `oc_setting` (`module_telecart_settings`) и читается на backend. ### 4. Гейтинг — итоговая логика heartbeat реально отправляется только если **одновременно**: `heartbeat_enabled = true` **и** есть signer (секрет настроен). Переключатель — пользовательский контроль поверх существующего серверного гейта; они независимы. ## Тестирование Unit-тест на `TeleCartPulseService::handleHeartbeat()`: - `heartbeat_enabled = false` → метод выходит сразу: HTTP-запрос не выполняется, кэш-флаг `telecart_pulse_heartbeat` **не** ставится, `getMe()` не вызывается. - `heartbeat_enabled = true` + signer присутствует → payload формируется и уходит (как сейчас). Запуск: `make test-unit` (или `./vendor/bin/phpunit --filter ...` через `make ssh`). ## Документация (в этом порядке) 1. **`docs/features/settings/CLAUDE.md`** — каркас системы настроек: путь поля «форма в админке → `saveSettingsForm` → таблица `oc_setting` (`module_telecart_settings`) → `Config\Settings` / DTO → backend», и **чек-лист «как добавить новый toggle»** по слоям (config-дефолт → провайдер/DTO → store → view-компонент `ItemBool`). По шаблону `docs/features/_TEMPLATE.md`. Добавить строку в реестр в `docs/features/CLAUDE.md`. 2. **`docs/features/telecart-pulse/CLAUDE.md`** — фича Pulse целиком: рассылки, события кампаний (гейт `api_key`) и heartbeat с новым выключателем. Ссылка на `[[settings]]`. Добавить строку в реестр. ## Затрагиваемые файлы (сводка) - `module/.../configs/app.php` — флаг `pulse.heartbeat_enabled`. - `module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php` — чтение и инжект. - `module/.../framework/TeleCartPulse/TeleCartPulseService.php` — поле + ранний return. - `frontend/admin/src/stores/settings.js` — дефолт в state. - `frontend/admin/src/views/TeleCartPulseView.vue` — `ItemBool` + текст раскрытия. - `module/.../tests/Unit/...` — тест на гейтинг heartbeat. - `docs/features/settings/CLAUDE.md`, `docs/features/telecart-pulse/CLAUDE.md`, `docs/features/CLAUDE.md` (реестр). ## Явно вне scope (YAGNI) - Не делаем opt-in (по умолчанию остаётся включено). - Не делаем отдельный consent-баннер на первой установке (достаточно видимого переключателя с раскрытием). - Не трогаем события кампаний и рассылки Pulse. - Не выносим heartbeat в отдельный namespace/фичу из `TeleCartPulse`.