Files
interview-demo-code/docs/superpowers/specs/2026-06-30-heartbeat-honest-opt-out-design.md
Nikita Kiselev adcca7cbc4 docs: document settings framework and telecart-pulse feature
Каркас системы настроек (docs/features/settings) и фича TeleCart Pulse
с heartbeat (docs/features/telecart-pulse); спека синхронизирована с opt-in.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 15:08:05 +03:00

174 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Дизайн: честный и отключаемый heartbeat
Дата: 2026-06-30
Статус: согласовано (ожидает финального ревью)
## Проблема
Модуль скрытно отправляет на сервер разработчика (`pulse.telecart.pro`) сведения об
окружении, где он запущен (heartbeat). Покупатель об этом не уведомляется и не может
это отключить. Это нечестно по отношению к покупателю и создаёт риски (GDPR / 152-ФЗ,
претензии маркетплейсов).
## Цель
Сделать heartbeat **честным**: прозрачным и отключаемым, сохранив поведение по умолчанию.
- **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда
отправляется и зачем.
- **Контроль:** отдельный переключатель, которым покупатель управляет heartbeat.
- **По умолчанию выключено (opt-in).** Без явного включения покупателем на сервер ничего не
уходит — в том числе у существующих установок. (Изначально планировался opt-out с дефолтом
«включено»; решение пересмотрено в пользу opt-in из-за юридических рисков сбора
идентифицирующих данных — домена и имени бота — без согласия.)
## Границы (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` (строки 168170),
который создаётся в провайдере при наличии секрета (`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` (строки 8690): добавить флаг с дефолтом
`false` (opt-in).
```php
'pulse' => [
'api_key' => '',
'heartbeat_enabled' => false, // новый флаг, opt-in
'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` (строки 8992): добавить
`heartbeat_enabled` в дефолтное состояние state.
**`frontend/admin/src/views/TeleCartPulseView.vue`** — добавить `ItemBool` с переключателем,
привязанным к `settings.items.pulse.heartbeat_enabled`. В слот-описание — **честное, но
нейтральное по тону раскрытие**:
- перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии
OpenCart;
- получателя назвать обобщённо — «разработчику» (без адреса сервера и периодичности: они не
важны покупателю);
- явно указать, что персональные данные покупателей и заказы **не** передаются;
- описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке
лицензии);
- отметить, что по умолчанию отправка отключена и её можно включить/отключить в любой момент.
Итоговая формулировка согласована с владельцем (нейтральный тон, вариант с оговоркой про
отсутствие персональных данных).
### 3. Поведение для существующих установок
Дефолт `false` в конфиге мержится с сохранёнными настройками
(`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`.