Files
interview-demo-code/docs/superpowers/specs/2026-06-30-heartbeat-honest-opt-out-design.md
2026-06-30 14:04:17 +03:00

10 KiB
Raw Blame History

Дизайн: честный и отключаемый 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.

'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(), если флаг выключен:

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.vueItemBool + текст раскрытия.
  • 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.