# Аудит Control Scripts — текущее состояние и предложения по улучшению

**Дата:** 2026-06-08  
**Аудитор:** Claude Code  
**Область:** `automation/`, `server/SHServ/Middleware/ControlScripts.php`, сопутствующие сервисы.

---

## Резюме

Система Control Scripts работает, но имеет критические баги, архитектурные ограничения и существенные пробелы в DX. Главные риски:

1. **Regular-скрипты через cron фактически сломаны** — CLI entry point не реализован.
2. **Весь runtime скриптов — static state**, что блокирует тестирование, горячую перезагрузку и масштабируемость.
3. **Добавление любой новой автоматизации требует git commit + deploy** — нет runtime-конфигурации через UI/DB.
4. **Нет защиты от ошибок** — exception в event handler убивает весь async-контекст без логов.

---

## Выполнено (2026-06-08)

- ✅ **P0.1** — `CronController::run_regular_cron_scripts()` запускает regular scripts **inline** (без `exec`/`console.php`), с `set_time_limit(300)` и `flock`.
- ✅ **P0.2** — `ControlScripts::add_event_handler()` обёрнут в `try/catch`, ошибки логируются.
- ✅ **P0.3** — `POST /api/v1/scripts/scopes/update` реализован. Требует `scripts.edit`. Валидация `php -l`, namespace, class name. Backup + `opcache_invalidate`.
- ✅ **P1.4** — `ScriptsRegistry` заменяет static state. Живёт в `app()`, flush per request.
- ✅ **P1.5** — Autodiscover Scope по `automation/Scopes/*.php`. Удалён `scopes-manifest.json`. Проверка `implements ControlScriptsInterface` + namespace `ControlScripts\Scopes`.
- ✅ **P1.7** — `set_time_limit(300)` + `flock` в regular scripts cron.
- ✅ **P2 — Timers / delays** — таблица `shserv_timers`, модель `Timers`, методы `delay_action/delay_event/delay_regular/cancel_timer` в `ControlScripts`, cron `GET /cron/timers`, REST endpoints, тесты `TimersTest.php`.
- ✅ Документация обновлена: `docs/control-scripts-guide.md`, `docs/server-api-v1/scripts.md`, `docs/architecture.md`, `docs/server-api.md`, `docs/events-from-devices.md`.

## Осталось

- P1.3 Namespace хардкожен — низкий приоритет, не критично для текущей структуры.
- P1.6 `get_source_code()` reflection — ограничение известное, не блокирует.
- P2 — System modes, params schema, execution log — отложено по решению владельца.
- P3 — Scope versioning / rollback, DI в ControlScripts — отложено.

---

## Часть 1. Что работает сейчас (кратко)

| Компонент | Статус | Примечание |
|-----------|--------|------------|
| Базовый класс `ControlScripts` | ✅ | 4 abstract метода, static хранилища |
| Scope-автозагрузка | ✅ | Autodiscover: `glob(automation/Scopes/*.php)` + `implements ControlScriptsInterface` |
| Event handlers | ✅ | Fury pub/sub, 5 паттернов имён, async через `fastcgi_finish_request` |
| Action scripts | ✅ | Регистрация, enable/disable, запуск через API, логирование времени |
| Sync map + `DeviceScriptsHelper` | ✅ | Связи relay↔button, синхронизация индикаторов |
| `Common` trait | ✅ | `register_global_device_sync_map()`, `set_btns_click_handlers()`, `btn_on_online()` |
| API list / state toggle | ✅ | `ScriptsRESTAPIController` + `Scripts` model |
| Legacy client support | ✅ | `scope_file` отдаёт source code |

---

## Часть 2. Критические баги (P0) — чинить немедленно

### 2.1. Regular scripts через cron не запускаются

**Где:** `server/SHServ/Controllers/CronController.php:54`, `server/console.php`

```php
// CronController.php
$this -> run_script_cli($alias);
// → exec("php console.php run-regular-script ...")

// console.php — НЕТ такой команды!
```

**Последствие:** Все regular-скрипты (включая `spotlights_by_time`) молча не выполняются.

**Починка:** Добавить в `console.php` обработку `run-regular-script` или отказаться от CLI-обёртки и запускать inline через `ControlScripts::run_regular_script()`.

---

### 2.2. Нет endpoint'а обновления кода Scope

**Где:** `docs/control-scripts-guide.md` обещает `POST /api/v1/scripts/scopes/update`, но в `ScriptsRESTAPI_v1.php` и `ScriptsRESTAPIController` его нет. В `ScriptsRESTAPIController.php:8` висит TODO:
> `- Реализовать апдейт скриптов`

**Последствие:** Редактирование Scope через UI невозможно. Legacy client может читать код (`scope_file`), но не писать.

---

### 2.3. Exception в event handler = тихий провал

**Где:** `ControlScripts::add_event_handler()` — callback не обёрнут в `try/catch`.

```php
events() -> handler("app:{$event_name}", function(Array $params) use ($handler) {
    $handler($params["device"], $params["data"]);
    // ↑ если тут Exception — Fury Events не ловит, лог не пишется
});
```

**Последствие:** Ошибка в automation logic (например, устройство offline → `$relay_api->toggle_channel()` бросает Exception) убивает весь async-контекст. Устройство получило `200 OK`, но автоматизация не сработала, и в лог попадёт только PHP fatal, если вообще попадёт.

---

## Часть 3. Архитектурные недостатки (P1)

### 3.1. Static state — ядро всех проблем

```php
protected static $regular_scripts = [];
protected static $actions_scripts = [];
protected static $sync_map_storage = ["connections" => []];
```

**Что ломает:**
- **Нельзя перезагрузить скрипты без перезапуска процесса** — `flush_statics()` есть, но после него нужно заново инстанцировать все Scope. В PHP-FPM это означает `kill -USR1` или ждать child restart.
- **Нельзя unit-testить** — static state течёт между тестами. PHPUnit в одном процессе получит скрипты от предыдущего теста.
- **Нельзя иметь несколько окружений** в одном процессе.

**Направление решения:** Перейти на **Registry pattern** — `ScriptsRegistry` как singleton через `app()`, а static поля заменить на instance-поля реестра. Тогда `app()->scripts_registry->flush()` работает в рамках одного request lifecycle.

---

### 3.2. Ручной манифест `scopes-manifest.json`

Любой новый Scope требует правки JSON. Это незабываемый шаг, легко ошибиться.

**Направление решения:** Autodiscover по `automation/Scopes/*.php` через `glob()` + `get_declared_classes()` или `ReflectionClass` на файлы. Достаточно проверять `implements ControlScriptsInterface`.

---

### 3.3. Namespace и путь хардкожены

```php
// App.php:90
$full_script_name = "\\ControlScripts\\Scopes\\{$script_name}";
```

Нельзя положить Scope в под-пакет (например, `ControlScripts\Scopes\Security\AlarmScope`).

---

### 3.4. Хрупкий парсинг имени Scope в constructor

```php
// ControlScripts.php:24
list($scope_folder, $scope_name) = explode("\\",
    str_replace("\\Scopes", "", str_replace("SHServ", "", static::class)));
```

Это ломается, если класс не в `ControlScripts\Scopes\` (например, `Vendor\ControlScripts\Scopes\X`).

---

### 3.5. `get_source_code()` — хрупкая отражение

```php
$ref_func = new \ReflectionFunction($func);
$file_name = $ref_func -> getFileName();
$start_line = $ref_func -> getStartLine();
// ...
```

Проблемы:
- Если файл обновился после загрузки (git pull) — отображаемый код ≠ исполняемому.
- Захватывает только closure body, не полную функцию.
- Не работает, если closure создан динамически (eval, create_function).

---

### 3.6. Regular scripts без таймаута и без защиты от overlap

```php
// CronController.php
foreach($regular_scripts as $alias => $script) {
    $this -> run_script_cli($alias);  // последовательный exec
}
```

- **Нет таймаута** — если скрипт висит на HTTP, cron висит.
- **Нет flock/lock** — если cron job запускается чаще, чем выполняется (например, каждую минуту), получаем pile-up процессов.
- **Последовательный запуск** — скрипты независимы, но ждут друг друга.

---

### 3.7. Action scripts без таймаута и без sandbox

`run_action_script()` напрямую вызывает `callable`. Если внутри HTTP-запрос к offline-устройству — API endpoint зависает до таймаута PHP-FPM.

---

### 3.8. Нет dependency injection

```php
protected function devices(): Devices {
    if(!$this->devices_model) {
        $this->devices_model = new Devices();  // жёстко зашито
    }
    return $this->devices_model;
}
```

Нельзя подменить `Devices` на mock в тестах. Нельзя подменить `DeviceScriptsHelper`.

---

## Часть 4. DX-проблемы (P2) — разработка и поддержка

### 4.1. Sync map захардкожен в PHP-коде (`Common` trait)

`Common::register_global_device_sync_map()` содержит ~25 связей с конкретными alias'ами (`spotlight_main_back_1`, `kitchen_buttons_1` и т.д.). Любое новое устройство = git commit + deploy.

**Направление решения:** Вынести sync map в JSON/YAML/DB-таблицу `device_sync_map`, редактируемую через UI. Scope должен читать её, а не хардкодить.

---

### 4.2. Нельзя создать Scope через UI

Сейчас Scope — это PHP-класс. Нельзя из веб-интерфейса нажать «Создать автоматизацию» и описать логику.

**Направление решения:** Rule Engine — хранить скрипты как JSON-структуры в БД (типа "IF event=X THEN action=Y"). Для сложного — оставить PHP Scope, для простого — Rule Engine.

---

### 4.3. Нет runtime-логов по скриптам

Есть глобальное логирование (`logging()->info(...)`), но нет таблицы `script_runs` с историей:
- кто запустил action script (user_id)
- с какими params
- результат (success/failure)
- execution time
- stack trace при ошибке

---

### 3.4. Нет rollback / версионирования Scope

Если обновить PHP-файл Scope и он сломается — нет способа откатиться, кроме `git revert`. Нет таблицы `scope_versions`.

---

### 4.5. Документация устарела

`docs/control-scripts-guide.md` указывал путь `server/ControlScripts/Scopes/` — **исправлено**, актуальный путь `automation/Scopes/`.

---

## Часть 5. Функциональные пробелы (P3) — расширение возможностей

### 5.1. Нет conditional logic / rule engine

Сейчас только "event → action" 1:1. Нельзя выразить:
> "Если датчик движения сработал **И** свет выключен **И** время между 22:00 и 06:00, то включить ночник на 30%"

**Направление:** Добавить `Condition` interface + composite conditions (`AndCondition`, `TimeRangeCondition`, `DeviceStateCondition`).

---

### 5.2. Нет state machine / modes

Нельзя переключать "режимы" системы: "Дома", "Не дома", "Ночь", "Отпуск". В каждом режиме — разная реакция на одно и то же событие.

**Направление:** Таблица `system_modes` + `ModeContext`, который Scope проверяет в handler'ах.

---

### 5.3. Нет timers / delays

Нельзя сделать "выключить свет через 5 минут" или "если дверь открыта более 2 минут — писать в лог".

**Направление:** Таблица `script_timers` (alias, fire_at, payload) + cron-обработчик `GET /cron/script-timers`.

---

### 5.4. Нет chaining / вызова action из action

Action script не может вызвать другой action script по alias. Приходится дублировать код.

```php
// Хотелось бы:
$this->run_action_script("night_mode_on");
```

---

### 5.5. Нет retry logic

Если устройство offline и HTTP падает — скрипт падает. Нет `retry(3, 500ms)`.

---

### 5.6. Нет параметризации action scripts через UI

Action scripts принимают `$params`, но нет JSON Schema для параметров. UI не знает, какие поля показать пользователю (input field, slider, select).

**Направление:** Добавить `params_schema` в `add_action_script()`:
```php
$this->add_action_script([
    "alias" => "dim_lights",
    "params_schema" => [
        ["name" => "brightness", "type" => "int", "min" => 0, "max" => 100, "default" => 50],
        ["name" => "room", "type" => "select", "options" => ["kitchen", "bedroom"]]
    ]
], function($params) { ... });
```

---

## Часть 6. Предложенный план улучшений

### Phase A. Critical fixes (1–2 дня)

1. **Починить regular scripts cron:**
   - Вариант A: Добавить `run-regular-script` в `console.php`.
   - Вариант B (лучше): Запускать inline `ControlScripts::run_regular_script()` прямо в `CronController`, без CLI обёртки. Добавить `try/catch` + лог.
2. **Защитить event handlers:** Обёрнуть `$handler()` в `try/catch` в `add_event_handler()`, логировать `Exception` через `logging()->error('php:ControlScripts', ...)`.
3. **~~Починить документацию~~** ✅ Выполнено — пути обновлены во всех документах.
4. **Добавить flock в cron:** `flock -n /var/lock/shserv-regular-scripts.lock php ...`

### Phase B. Architecture refactor (3–5 дней)

5. **Registry pattern:** Создать `ScriptsRegistry` (не-static), положить в `app()`. Перенести `$actions_scripts`, `$regular_scripts`, `$sync_map_storage` туда. `ControlScripts` получает registry через constructor или `app()->scripts_registry`.
6. **Autodiscover scopes:** Убрать `scopes-manifest.json`, искать классы по `automation/Scopes/*.php` автоматически.
7. **DI в базовом классе:** `__construct(Devices $devices, DeviceScriptsHelper $helper, ScriptsRegistry $registry)` или фабрика.
8. **Таймауты:** Добавить `curl` timeout в `DeviceAPI\Base` (сейчас, вероятно, default). Убедиться, что action script не висит >5s.

### Phase C. Rule Engine + DB-driven sync map (1–2 недели)

9. **Sync map в БД:** Таблица `device_sync_map` (`id`, `relay_alias`, `relay_channel`, `button_alias`, `button_channel`, `area_id`). Scope читает её. UI для редактирования.
10. **Simple Rule Engine (JSON-driven):**
    - Таблица `script_rules` (`id`, `name`, `event_pattern`, `conditions_json`, `actions_json`, `state`, `priority`).
    - `conditions_json`: `[{"type":"time_range","from":"22:00","to":"06:00"}, {"type":"device_state","alias":"kitchen_relay","channel":0,"state":"off"}]`
    - `actions_json`: `[{"type":"toggle","alias":"kitchen_relay","channel":0}]`
    - Scope `RuleEngineScope` подписывается на `*` и исполняет matching rules.
11. **Параметризация action scripts:** `params_schema` → отображение в UI Vue-клиента.

### Phase D. Advanced features (2+ недели)

12. **Timers / delays:** Таблица `script_timers`, cron endpoint.
13. **System modes:** Таблица `system_modes`, API toggle, ModeContext в Scope.
14. **Script execution log:** Таблица `script_runs` + UI history.
15. **Retry logic:** `DeviceAPI\Base` с `retry(3, 500ms)` для `POST /action`.
16. **Scope versioning:** Таблица `scope_versions`, сохранять source code перед обновлением, rollback.

---

## Часть 7. Рекомендуемый приоритет

| Приоритет | Задача | ROI | Сложность |
|-----------|--------|-----|-----------|
| 🔴 P0 | Починить regular scripts cron | Высокий | Низкая |
| 🔴 P0 | try/catch в event handlers | Высокий | Низкая |
| 🟡 P1 | Registry вместо static | Высокий | Средняя |
| 🟡 P1 | Autodiscover scopes | Средний | Низкая |
| 🟡 P1 | Таймауты + overlap lock | Высокий | Низкая |
| 🟢 P2 | Sync map в БД | Высокий | Средняя |
| 🟢 P2 | Rule Engine (simple JSON) | Очень высокий | Средняя |
| 🟢 P2 | Script execution log | Средний | Низкая |
| ✅ | Timers / delays | Средний | Средняя |
| 🔵 P3 | System modes | Средний | Средняя |
| 🔵 P3 | Params schema для UI | Высокий | Низкая |

---

## Приложение: Сводка файлов

| Файл | Роль | Проблемы |
|------|------|----------|
| `automation/Scopes/*.php` | Scope-классы | Хардкод alias'ов, нет версионирования |
| `automation/Common.php` | Общие хелперы | Sync map захардкожен |
| `server/SHServ/Middleware/ControlScripts.php` | Базовый класс | Static state, хрупкий парсинг имени, нет DI |
| `server/SHServ/Controllers/CronController.php` | Cron | Inline запуск regular scripts + timers, flock, timeout |
| `server/SHServ/Middleware/ScriptsRegistry.php` | Registry | Instance-based хранилище скриптов |
| `server/SHServ/Models/Timers.php` | DB model | Таймеры: create, cancel, get_pending, mark_status |
| `server/SHServ/Routes/ScriptsRESTAPI_v1.php` | Роуты | Добавлены `POST /scopes/update` и `POST /timers/cancel` |
| `docs/control-scripts-guide.md` | Документация | Актуализирована (пути, timers) |
