169 lines
10 KiB
Markdown
169 lines
10 KiB
Markdown
# Дизайн: честный и отключаемый 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`.
|