Дата: 2026-06-08
Аудитор: Claude Code
Область: automation/, server/SHServ/Middleware/ControlScripts.php, сопутствующие сервисы.
Система Control Scripts работает, но имеет критические баги, архитектурные ограничения и существенные пробелы в DX. Главные риски:
CronController::run_regular_cron_scripts() запускает regular scripts inline (без exec/console.php), с set_time_limit(300) и flock.ControlScripts::add_event_handler() обёрнут в try/catch, ошибки логируются.POST /api/v1/scripts/scopes/update реализован. Требует scripts.edit. Валидация php -l, namespace, class name. Backup + opcache_invalidate.ScriptsRegistry заменяет static state. Живёт в app(), flush per request.automation/Scopes/*.php. Удалён scopes-manifest.json. Проверка implements ControlScriptsInterface + namespace ControlScripts\Scopes.set_time_limit(300) + flock в regular scripts cron.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.get_source_code() reflection — ограничение известное, не блокирует.| Компонент | Статус | Примечание |
|---|---|---|
Базовый класс 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 |
Где: server/SHServ/Controllers/CronController.php:54, server/console.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().
Где: 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), но не писать.
Где: ControlScripts::add_event_handler() — callback не обёрнут в try/catch.
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, если вообще попадёт.
protected static $regular_scripts = []; protected static $actions_scripts = []; protected static $sync_map_storage = ["connections" => []];
Что ломает:
flush_statics() есть, но после него нужно заново инстанцировать все Scope. В PHP-FPM это означает kill -USR1 или ждать child restart.Направление решения: Перейти на Registry pattern — ScriptsRegistry как singleton через app(), а static поля заменить на instance-поля реестра. Тогда app()->scripts_registry->flush() работает в рамках одного request lifecycle.
scopes-manifest.jsonЛюбой новый Scope требует правки JSON. Это незабываемый шаг, легко ошибиться.
Направление решения: Autodiscover по automation/Scopes/*.php через glob() + get_declared_classes() или ReflectionClass на файлы. Достаточно проверять implements ControlScriptsInterface.
// App.php:90
$full_script_name = "\\ControlScripts\\Scopes\\{$script_name}";
Нельзя положить Scope в под-пакет (например, ControlScripts\Scopes\Security\AlarmScope).
// ControlScripts.php:24
list($scope_folder, $scope_name) = explode("\\",
str_replace("\\Scopes", "", str_replace("SHServ", "", static::class)));
Это ломается, если класс не в ControlScripts\Scopes\ (например, Vendor\ControlScripts\Scopes\X).
get_source_code() — хрупкая отражение$ref_func = new \ReflectionFunction($func); $file_name = $ref_func -> getFileName(); $start_line = $ref_func -> getStartLine(); // ...
Проблемы:
// CronController.php
foreach($regular_scripts as $alias => $script) {
$this -> run_script_cli($alias); // последовательный exec
}
run_action_script() напрямую вызывает callable. Если внутри HTTP-запрос к offline-устройству — API endpoint зависает до таймаута PHP-FPM.
protected function devices(): Devices {
if(!$this->devices_model) {
$this->devices_model = new Devices(); // жёстко зашито
}
return $this->devices_model;
}
Нельзя подменить Devices на mock в тестах. Нельзя подменить DeviceScriptsHelper.
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 должен читать её, а не хардкодить.
Сейчас Scope — это PHP-класс. Нельзя из веб-интерфейса нажать «Создать автоматизацию» и описать логику.
Направление решения: Rule Engine — хранить скрипты как JSON-структуры в БД (типа "IF event=X THEN action=Y"). Для сложного — оставить PHP Scope, для простого — Rule Engine.
Есть глобальное логирование (logging()->info(...)), но нет таблицы script_runs с историей:
Если обновить PHP-файл Scope и он сломается — нет способа откатиться, кроме git revert. Нет таблицы scope_versions.
docs/control-scripts-guide.md указывал путь server/ControlScripts/Scopes/ — исправлено, актуальный путь automation/Scopes/.
Сейчас только "event → action" 1:1. Нельзя выразить:
"Если датчик движения сработал И свет выключен И время между 22:00 и 06:00, то включить ночник на 30%"
Направление: Добавить Condition interface + composite conditions (AndCondition, TimeRangeCondition, DeviceStateCondition).
Нельзя переключать "режимы" системы: "Дома", "Не дома", "Ночь", "Отпуск". В каждом режиме — разная реакция на одно и то же событие.
Направление: Таблица system_modes + ModeContext, который Scope проверяет в handler'ах.
Нельзя сделать "выключить свет через 5 минут" или "если дверь открыта более 2 минут — писать в лог".
Направление: Таблица script_timers (alias, fire_at, payload) + cron-обработчик GET /cron/script-timers.
Action script не может вызвать другой action script по alias. Приходится дублировать код.
// Хотелось бы:
$this->run_action_script("night_mode_on");
Если устройство offline и HTTP падает — скрипт падает. Нет retry(3, 500ms).
Action scripts принимают $params, но нет JSON Schema для параметров. UI не знает, какие поля показать пользователю (input field, slider, select).
Направление: Добавить params_schema в add_action_script():
$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) { ... });
run-regular-script в console.php.ControlScripts::run_regular_script() прямо в CronController, без CLI обёртки. Добавить try/catch + лог.$handler() в try/catch в add_event_handler(), логировать Exception через logging()->error('php:ControlScripts', ...).flock -n /var/lock/shserv-regular-scripts.lock php ...ScriptsRegistry (не-static), положить в app(). Перенести $actions_scripts, $regular_scripts, $sync_map_storage туда. ControlScripts получает registry через constructor или app()->scripts_registry.scopes-manifest.json, искать классы по automation/Scopes/*.php автоматически.__construct(Devices $devices, DeviceScriptsHelper $helper, ScriptsRegistry $registry) или фабрика.curl timeout в DeviceAPI\Base (сейчас, вероятно, default). Убедиться, что action script не висит >5s.device_sync_map (id, relay_alias, relay_channel, button_alias, button_channel, area_id). Scope читает её. UI для редактирования.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}]RuleEngineScope подписывается на * и исполняет matching rules.params_schema → отображение в UI Vue-клиента.script_timers, cron endpoint.system_modes, API toggle, ModeContext в Scope.script_runs + UI history.DeviceAPI\Base с retry(3, 500ms) для POST /action.scope_versions, сохранять source code перед обновлением, rollback.| Приоритет | Задача | 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) |