docs: add design spec for honest opt-out heartbeat

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 14:04:17 +03:00
parent d621baae45
commit 24075dceb7

View File

@@ -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` (строки 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): добавить флаг с дефолтом
`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` (строки 8992): добавить
`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`.