From 24075dceb710cb7b4a5038b02e425c61ff2e97ed Mon Sep 17 00:00:00 2001 From: Nikita Kiselev Date: Tue, 30 Jun 2026 14:04:17 +0300 Subject: [PATCH] docs: add design spec for honest opt-out heartbeat Co-Authored-By: Claude Opus 4.8 --- ...6-06-30-heartbeat-honest-opt-out-design.md | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-30-heartbeat-honest-opt-out-design.md diff --git a/docs/superpowers/specs/2026-06-30-heartbeat-honest-opt-out-design.md b/docs/superpowers/specs/2026-06-30-heartbeat-honest-opt-out-design.md new file mode 100644 index 00000000..e5f29036 --- /dev/null +++ b/docs/superpowers/specs/2026-06-30-heartbeat-honest-opt-out-design.md @@ -0,0 +1,168 @@ +# Дизайн: честный и отключаемый 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`.