diff --git a/docs/features/CLAUDE.md b/docs/features/CLAUDE.md index 2e26b4e8..b777aab9 100644 --- a/docs/features/CLAUDE.md +++ b/docs/features/CLAUDE.md @@ -36,8 +36,10 @@ docs/features/ ## Реестр фич -| Фича | Путь | Краткое описание | -|------|----------------------------------|-------------------------------------------------------------| -| cart | [cart/CLAUDE.md](cart/CLAUDE.md) | Корзина: наполнение, просмотр, передача в оформление заказа | +| Фича | Путь | Краткое описание | +|----------------|--------------------------------------------------------|--------------------------------------------------------------------------| +| cart | [cart/CLAUDE.md](cart/CLAUDE.md) | Корзина: наполнение, просмотр, передача в оформление заказа | +| settings | [settings/CLAUDE.md](settings/CLAUDE.md) | Каркас настроек: форма в админке → хранение → чтение на backend | +| telecart-pulse | [telecart-pulse/CLAUDE.md](telecart-pulse/CLAUDE.md) | Связка с SaaS: события кампаний и heartbeat (телеметрия окружения) | diff --git a/docs/features/settings/CLAUDE.md b/docs/features/settings/CLAUDE.md new file mode 100644 index 00000000..e8a1f470 --- /dev/null +++ b/docs/features/settings/CLAUDE.md @@ -0,0 +1,88 @@ +# Настройки модуля + +## Назначение + +Сквозная подсистема настроек модуля: форма в админ-панели, хранение значений и чтение их +на backend. Это **каркас**, в который встраиваются остальные фичи — когда фиче нужен свой +флаг/параметр, она добавляет поле в этот пайплайн, а не строит собственный механизм. + +## Точки входа + +| action | Метод | Где объявлен | +|--------------------|------------------------------------|---------------------| +| `getSettingsForm` | `SettingsHandler::getSettingsForm` | `bastion/routes.php` | +| `saveSettingsForm` | `SettingsHandler::saveSettingsForm`| `bastion/routes.php` | +| `resetCache` | `SettingsHandler::resetCache` | `bastion/routes.php` | +| `getSystemInfo` | `SettingsHandler::getSystemInfo` | `bastion/routes.php` | + +## Ключевые файлы по слоям + +- **Дефолты:** `module/oc_telegram_shop/upload/oc_telegram_shop/configs/app.php` — структура + всех настроек с дефолтными значениями (секции `app`, `telegram`, `metrics`, `store`, + `orders`, `texts`, `sliders`, `mainpage_blocks`, `pulse`, `cron`). +- **Handler (админка):** `module/.../bastion/Handlers/SettingsHandler.php` — + `getSettingsForm()` отдаёт срез конфига (`Arr::getWithKeys` по списку секций), + `saveSettingsForm()` сохраняет тот же срез. +- **Сохранение:** `module/.../bastion/Services/SettingsService.php` — `update()` пишет в + OpenCart-таблицу `oc_setting` через `model_setting_setting->editSetting('module_telecart', + ['module_telecart_settings' => $data])`, синхронизирует статус модуля `module_tgshop` и + очищает кэш. +- **Чтение (backend):** `Openguru\OpenCartFramework\Config\Settings` — `get('a.b.c')` / + `getAll()`. Расширение для витрины — `App\Services\SettingsService` (наследует `Settings`) + и DTO в `src/DTO/Settings/` с типизированными геттерами (`is*()`, `get*()`). +- **Сборка конфига:** `module/.../bastion/ApplicationFactory.php` (и точка входа + `admin/controller/extension/module/tgshop.php`) — дефолты из `configs/app.php` мержатся с + сохранёнными `module_telecart_settings` через `Arr::mergeArraysRecursively`, результат + отдаётся в `Application`. +- **Фронтенд (стор):** `frontend/admin/src/stores/settings.js` — Pinia setup-store: дефолтный + state, `fetchSettings()` (`getSettingsForm`), `saveSettings()` (`saveSettingsForm`). +- **Фронтенд (поля):** `frontend/admin/src/components/Settings/` — `ItemBool` (toggle), + `ItemInput`, `ItemSelect`, `ItemImage`, `ItemToggleButton`. `ItemBool` → + `Switcher` → `ToggleSwitch` (PrimeVue). Используются во `views/*View.vue`. + +## Поток данных + +Чтение формы: +`getSettingsForm` → `Settings::getAll()` (уже смерженный конфиг) → срез по секциям → +JSON → стор `fetchSettings()` → поля во `*View.vue`. + +Сохранение: +поле (`v-model`) → стор `saveSettings()` → `saveSettingsForm` → `SettingsService::update()` +→ `oc_setting` (`module_telecart_settings`) + сброс кэша. + +Чтение на backend: +дефолты `configs/app.php` ⊕ сохранённые значения → `Application` → `Config\Settings` / DTO +→ сервисы (через DI-провайдеры, которые инжектят `getConfigValue('...')` в конструкторы). + +## Как добавить новый toggle (чек-лист) + +1. **Дефолт:** добавить ключ в нужную секцию `configs/app.php` (для булева — `true`/`false`). +2. **Backend-потребитель:** прочитать значение там, где оно нужно. Идиома модуля — читать в + ServiceProvider (`$app->getConfigValue('секция.ключ')`) и **инжектить** в конструктор + сервиса (не дёргать конфиг из глубины класса). При наличии DTO — добавить геттер. +3. **Стор:** добавить ключ с дефолтом в соответствующую секцию state в + `frontend/admin/src/stores/settings.js`. +4. **View:** добавить компонент поля (для тумблера — `…текст-подсказка…`) в нужный + `*View.vue`. +5. Убедиться, что секция уже перечислена в списках `Arr::getWithKeys` в `getSettingsForm`/ + `saveSettingsForm` (если добавляешь поле в новую секцию — добавить и саму секцию в оба + списка). + +## Связанные фичи + +- [telecart-pulse](../telecart-pulse/CLAUDE.md) — heartbeat использует этот каркас: + флаг `pulse.heartbeat_enabled` проходит весь путь от формы до backend-гейта. + +## Нюансы и подводные камни + +- **PHP 7.4** в backend-коде (без `match`, промоушена свойств, union-типов и т.п.). +- `getSettingsForm`/`saveSettingsForm` работают строго по **белому списку секций** + (`Arr::getWithKeys`). Поле вне перечисленных секций не отдастся и не сохранится. +- Секция `cron` содержит read-only поля (`cli_path`, `last_run`), которые добавляются при + выдаче и вырезаются перед сохранением — не сохраняй вычисляемые значения. +- `saveSettingsForm` сейчас вызывает пустой `validate()` — серверной валидации значений + настроек по сути нет; не полагайся на неё. +- После сохранения кэш сбрасывается целиком (`SettingsService::update` → `cache->clear()`). +- Существующие установки не имеют сохранённого значения нового ключа — оно берётся из + дефолта `configs/app.php` (мерж). Выбирай дефолт так, чтобы поведение «как было». diff --git a/docs/features/telecart-pulse/CLAUDE.md b/docs/features/telecart-pulse/CLAUDE.md new file mode 100644 index 00000000..40a6880b --- /dev/null +++ b/docs/features/telecart-pulse/CLAUDE.md @@ -0,0 +1,78 @@ +# TeleCart Pulse + +## Назначение + +Связка модуля с внешним SaaS `pulse.telecart.pro`. Включает две концептуально разные части: + +- **События кампаний** (`WEBAPP_OPEN`, `ORDER_CREATED`, …) — аналитика маркетинговых + кампаний и Telegram-рассылок. Завязана на действия продавца: событие сохраняется и + отправляется только если в `start_param` пришли `campaign_id` и `tracking_id`, и только + при заданном `pulse.api_key`. +- **Heartbeat** — раз в час отправляет технические сведения об установке (домен, имя бота, + версии PHP/модуля/OpenCart). Используется для статистики установок и техподдержки. + Управляется **отдельным** переключателем `pulse.heartbeat_enabled` (по умолчанию + **выключен**, opt-in), независимым от `pulse.api_key`. + +В коде обе части живут в namespace `Openguru\OpenCartFramework\TeleCartPulse`. + +## Точки входа + +| action | Метод | Где объявлен | +|-------------|------------------------------|------------------| +| `ingest` | `TelemetryHandler::ingest` | `src/routes.php` | +| `heartbeat` | `TelemetryHandler::heartbeat`| `src/routes.php` | + +Триггерятся из SPA (`frontend/spa/src/stores/Pulse.js`, вызов при инициализации WebApp в +`frontend/spa/src/main.js`). Heartbeat кэшируется на 1 час (`telecart_pulse_heartbeat`). + +## Ключевые файлы по слоям + +- **Handler:** `module/.../src/Handlers/TelemetryHandler.php` — `ingest()` (события) и + `heartbeat()` (телеметрия окружения); оба делегируют в `TeleCartPulseService`. +- **Сервис:** `module/.../framework/TeleCartPulse/TeleCartPulseService.php` — + `handleIngest()` (валидация campaign/tracking, сохранение события в БД) и + `handleHeartbeat()` (сбор окружения, подпись, POST). В начале `handleHeartbeat()` — + гейт `if (! $this->heartbeatEnabled) return;`. +- **Отправка событий:** `module/.../framework/TeleCartPulse/TeleCartPulseEventsSender.php` — + POST накопленных событий на `/events` с `Authorization: Bearer `. +- **Регистрация в DI:** `module/.../framework/TeleCartPulse/TeleCartPulseServiceProvider.php` + — читает `pulse.api_key`, секрет подписи (`pulse.heartbeat_secret` / env + `PULSE_HEARTBEAT_SECRET`) и `pulse.heartbeat_enabled`, инжектит в сервисы. +- **Модель/данные:** `module/.../framework/TeleCartPulse/TeleCartEvent.php`, таблица + `telecart_events` (миграция `database/migrations/20260101000010_create_telecart_events_table.php`). +- **Расписание:** `configs/schedule.php` — `TeleCartPulseSendEventsTask` каждые 10 минут; + CLI-аналог — `console/Commands/PulseSendEventsCommand.php` (`pulse:send`). +- **Конфиг/дефолты:** `configs/app.php`, секция `pulse` (`api_key`, `heartbeat_enabled`, + `batch_size`, `max_attempts`). +- **Фронтенд (админка):** `frontend/admin/src/views/TeleCartPulseView.vue` — поля настроек, + включая переключатель heartbeat (`ItemBool`) с раскрытием состава отправляемых данных; + дефолты в `frontend/admin/src/stores/settings.js` (секция `pulse`). + +## Поток данных + +**События:** SPA `ingest` → `handleIngest()` → проверка `api_key` + наличия +`campaign_id`/`tracking_id` → запись в `telecart_events` (status `pending`) → каждые 10 мин +`TeleCartPulseSendEventsTask` → `TeleCartPulseEventsSender::sendEvent()` → POST `/events`. + +**Heartbeat:** SPA `heartbeat` → `handleHeartbeat()` → гейт `heartbeat_enabled` → кэш-чек +(1 ч) → `getMe()` → payload окружения → подпись (если есть signer) → POST `/heartbeat`. + +## Связанные фичи + +- [settings](../settings/CLAUDE.md) — переключатель heartbeat и `api_key` проходят через + общий каркас настроек. + +## Нюансы и подводные камни + +- **PHP 7.4** в backend-коде. +- **Heartbeat и Pulse — независимы.** `pulse.heartbeat_enabled` НЕ завязан на `api_key`. + Heartbeat реально уходит только при `heartbeat_enabled = true` **и** наличии signer + (секрет настроен). Выключенный тумблер → ранний `return`, даже кэш не ставится. +- **События гейтятся `api_key`:** при пустом `pulse.api_key` `handleIngest()` выходит сразу, + ничего не сохраняется и не отправляется. +- **Прозрачность heartbeat — обязательна.** Состав отправляемых данных раскрыт покупателю в + `TeleCartPulseView.vue`; при изменении полей payload в `handleHeartbeat()` синхронизируй + текст раскрытия. +- Дефолт `heartbeat_enabled = false` (opt-in): без явного включения покупателем heartbeat не + отправляется; существующие установки без сохранённого значения тоже ничего не шлют. +- Тест гейта: `tests/Unit/Framework/TeleCartPulse/TeleCartPulseServiceHeartbeatTest.php`. 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 index e5f29036..48c4657d 100644 --- 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 @@ -16,9 +16,11 @@ - **Прозрачность:** в админ-панели покупатель видит, что именно собирается, куда отправляется и зачем. -- **Контроль:** отдельный переключатель, которым покупатель может выключить heartbeat. -- **По умолчанию включено** (текущее поведение сохраняется, но становится видимым и - управляемым). +- **Контроль:** отдельный переключатель, которым покупатель управляет heartbeat. +- **По умолчанию выключено (opt-in).** Без явного включения покупателем на сервер ничего не + уходит — в том числе у существующих установок. (Изначально планировался opt-out с дефолтом + «включено»; решение пересмотрено в пользу opt-in из-за юридических рисков сбора + идентифицирующих данных — домена и имени бота — без согласия.) ## Границы (scope) @@ -62,12 +64,12 @@ ### 1. Backend **`module/.../configs/app.php`** — секция `pulse` (строки 86–90): добавить флаг с дефолтом -`true`. +`false` (opt-in). ```php 'pulse' => [ 'api_key' => '', - 'heartbeat_enabled' => true, // новый флаг + 'heartbeat_enabled' => false, // новый флаг, opt-in 'batch_size' => 50, 'max_attempts' => 3, ], @@ -106,25 +108,29 @@ public function handleHeartbeat(): void - перечислить, что отправляется: домен, имя бота, версия PHP, версия модуля, версии OpenCart; -- указать адрес назначения: `pulse.telecart.pro`; +- получателя назвать обобщённо — «разработчику» (без адреса сервера и периодичности: они не + важны покупателю); +- явно указать, что персональные данные покупателей и заказы **не** передаются; - описать цель мягко: для улучшения продукта и техподдержки (без акцента на проверке - лицензии). + лицензии); +- отметить, что по умолчанию отправка отключена и её можно включить/отключить в любой момент. -Точную формулировку владелец отредактирует; черновик в нейтральном тоне готовит реализация. +Итоговая формулировка согласована с владельцем (нейтральный тон, вариант с оговоркой про +отсутствие персональных данных). ### 3. Поведение для существующих установок -Дефолт `true` в конфиге мержится с сохранёнными настройками +Дефолт `false` в конфиге мержится с сохранёнными настройками (`ApplicationFactory`: `Arr::mergeArraysRecursively`). У установок без сохранённого значения -heartbeat остаётся включённым — поведение не меняется, но теперь переключатель виден. -Когда покупатель выключает тумблер и сохраняет, значение пишется в `oc_setting` +heartbeat **выключен** — на сервер ничего не уходит, пока покупатель явно не включит тумблер. +Когда покупатель сохраняет настройку, значение пишется в `oc_setting` (`module_telecart_settings`) и читается на backend. ### 4. Гейтинг — итоговая логика heartbeat реально отправляется только если **одновременно**: `heartbeat_enabled = true` -**и** есть signer (секрет настроен). Переключатель — пользовательский контроль поверх -существующего серверного гейта; они независимы. +(покупатель включил) **и** есть signer (секрет настроен). Переключатель — пользовательский +контроль поверх существующего серверного гейта; они независимы. ## Тестирование @@ -161,8 +167,7 @@ Unit-тест на `TeleCartPulseService::handleHeartbeat()`: ## Явно вне scope (YAGNI) -- Не делаем opt-in (по умолчанию остаётся включено). -- Не делаем отдельный consent-баннер на первой установке (достаточно видимого - переключателя с раскрытием). +- Делаем opt-in (по умолчанию выключено); отдельный consent-баннер на первой установке не + нужен — достаточно выключенного по умолчанию переключателя с раскрытием. - Не трогаем события кампаний и рассылки Pulse. - Не выносим heartbeat в отдельный namespace/фичу из `TeleCartPulse`.