
# План: Params Schema для Action Scripts

## Цель
Scope-классы могут декларировать JSON-схему параметров при регистрации action-скрипта. Vue-клиент отображает динамическую форму в модалке вместо пустого `{}` при запуске. Сервер валидирует входящие параметры по схеме перед вызовом closure.

## Дизайн (утверждён)

**Формат в PHP Scope:**
```php
$this->add_action_script([
    "alias"         => "dim_lights",
    "name"          => "Диммер",
    "params_schema" => [
        "level" => [
            "type"     => "range",
            "label"    => "Яркость, %",
            "min"      => 0,
            "max"      => 100,
            "step"     => 5,
            "default"  => 50,
            "required" => true,
        ],
        "room" => [
            "type"    => "select",
            "label"   => "Комната",
            "options" => ["kitchen" => "Кухня", "hall" => "Зал"],
            "default" => "hall",
        ],
        "instant" => [
            "type"    => "toggle",
            "label"   => "Мгновенно",
            "default" => false,
        ],
    ],
], function($params) { ... });
```

**Типы:** `text`, `number`, `range`, `select`, `toggle`, `textarea`.
**Валидация PHP:** тип, `required`, `min/max` для `number`/`range`, ключ в `options` для `select`.
**UI:** модал `GnModal` с динамической формой; `GnInput`, `GnRange`, `GnSelect`, `GnSwitch`, `GnTextarea`.

---

## Фаза 1 — PHP backend

### 1.1 Миграция
**Файл:** `server/database/migrations/2026_06_08_000003_script_params_schema.php`
- `ALTER TABLE scripts ADD COLUMN params_schema TEXT NULL`

### 1.2 Entity `Script`
**Файл:** `server/SHServ/Entities/Script.php`
- Добавить `"params_schema"` в `$fields`

### 1.3 Model `Scripts` — синхронизация и мерж
**Файл:** `server/SHServ/Models/Scripts.php`
- `prepare_script_to_view()` — мержить `params_schema` из DB entity в ответ (если есть)
- Новый приватный метод `sync_params_schema_to_db($type, $uniq_name, $schema)`:
  - Если запись в БД существует и `params_schema` отличается от registry — `UPDATE`
  - Вызывать из `get_scripts_list()` при подготовке к view (lazy sync)
- Новый метод `get_params_schema($type, $uniq_name): ?array` — декодировать JSON из БД

### 1.4 `ControlScripts::add_action_script()`
**Файл:** `server/SHServ/Middleware/ControlScripts.php`
- Принимать `"params_schema"` из `$attributes`
- Сохранять в `ScriptsRegistry::$actions[$alias]["params_schema"]`
- `add_regular_script()` пока без изменений (можно заложить, но action — MVP)

### 1.5 `ScriptsRegistry`
**Файл:** `server/SHServ/Middleware/ScriptsRegistry.php`
- Entry теперь: `["attributes", "code", "script", "params_schema"]`

### 1.6 Валидация params по schema
**Новый файл:** `server/SHServ/Helpers/ScriptParamsValidator.php`
- Метод `validate($schema, $params): array` — возвращает `["ok" => bool, "errors" => [...]]`
- Проверки по типам:
  - `required` — поле присутствует в `$params`
  - `select` — значение ∈ keys `options`
  - `number`/`range` — `is_numeric`, в границах `min/max`
  - `toggle` — `is_bool`
  - `text`/`textarea` — `is_string`

### 1.7 `ScriptsRESTAPIController::run_action_script()`
**Файл:** `server/SHServ/Controllers/ScriptsRESTAPIController.php`
- После проверки alias и state, перед вызовом `ControlScripts::run_action_script()`:
  1. Получить schema из `ScriptsRegistry::$actions[$alias]["params_schema"]` или из БД
  2. Если schema не null — валидировать `$params` через `ScriptParamsValidator`
  3. Если ошибка — ответ `invalid_params` с `failed_fields`
  4. Применить `default` для отсутствующих необязательных полей
  5. Затем вызвать `ControlScripts::run_action_script($alias, $params)`

### 1.8 `ScriptsRESTAPIController::actions_scripts_list()`
- Убедиться что `params_schema` присутствует в ответе (через `prepare_script_to_view`)

---

## Фаза 2 — Vue frontend

### 2.1 API module (без изменений)
`scriptsApi.runAction(alias, params)` уже поддерживает params — достаточно.

### 2.2 Store (минимальные изменения)
**Файл:** `webclient/src/stores/scripts.js`
- `runScript(alias, params)` уже принимает params
- Добавить state `runModalScript: null` — текущий скрипт для модалки (или управлять через emit)

### 2.3 Новый компонент `ScriptRunModal.vue`
**Файл:** `webclient/src/components/script/ScriptRunModal.vue`
- Props: `script` (alias, name, params_schema)
- `GnModal :open` + `@update:open`
- Внутри: динамическая форма:
  - `v-for="(config, name) in script.params_schema" :key="name"`
  - Маппинг `type` → компонент:
    - `text` → `GnInput type="text"`
    - `number` → `GnInput type="number"`
    - `range` → `GnRange` (`min`, `max`, `step`)
    - `select` → `GnSelect` (`:options` — массив `{value, label}` из `Object.entries(config.options)`)
    - `toggle` → `GnSwitch`
    - `textarea` → `GnTextarea`
  - `v-model` на реактивный объект `formValues[name]`
  - Инициализация `formValues` значениями `default` из schema
  - Валидация клиента: `required` → красная рамка/сообщение, `GnInput`/`GnRange` props
- Footer: `Run` (disabled если есть required-ошибки) + `Cancel`
- При submit: `$emit('run', { alias: script.alias, params: formValues })`

### 2.4 `ActionScriptsGrid.vue`
**Файл:** `webclient/src/components/script/ActionScriptsGrid.vue`
- Метод `run(alias)` заменить на:
  ```js
  function run(script) {
    if (!script.params_schema || Object.keys(script.params_schema).length === 0) {
      executeRun(script.alias, {});
      return;
    }
    activeScriptForModal.value = script;
    showRunModal.value = true;
  }
  ```
- Добавить `<ScriptRunModal :script="activeScriptForModal" :open="showRunModal" @run="onModalRun" @close="showRunModal = false" />`
- `onModalRun({ alias, params })` → `executeRun(alias, params)`

### 2.5 `ScriptDetailPage.vue`
**Файл:** `webclient/src/features/scripts/pages/ScriptDetailPage.vue`
- Аналогично: если action script имеет `params_schema`, Run открывает модал вместо прямого вызова

---

## Фаза 3 — Тесты

### 3.1 PHP tests
**Новый файл:** `server/tests/ScriptParamsValidatorTest.php`
- Проверка валидации по каждому типу (valid / invalid)
- Проверка `required` и `default`
- Проверка edge cases (null, пустая строка)

**Файл:** `server/tests/ScriptsModelStateTest.php` или новый `ScriptParamsSchemaTest.php`
- Проверка что `prepare_script_to_view` возвращает `params_schema`
- Проверка lazy sync в DB

### 3.2 Vue tests
**Новый файл:** `webclient/src/components/script/__tests__/ScriptRunModal.spec.js`
- Рендеринг полей по schema
- Валидация required
- Событие submit с правильными params

---

## Фаза 4 — Документация

### 4.1 `docs/control-scripts-guide.md`
- Обновить раздел "Action-скрипты": добавить `params_schema` в пример
- Добавить подраздел "Params Schema" с таблицей типов и их полей

### 4.2 `docs/server-api-v1/scripts.md`
- В `GET /api/v1/scripts/actions/list` — добавить `params_schema` в пример ответа
- В `POST /api/v1/scripts/actions/run` — добавить пример с `params` и ошибку `invalid_params`

---

## Критические файлы

| Файл | Действие |
|------|----------|
| `server/database/migrations/2026_06_08_000003_script_params_schema.php` | Создать |
| `server/SHServ/Entities/Script.php` | Добавить поле |
| `server/SHServ/Models/Scripts.php` | Lazy sync + merge |
| `server/SHServ/Middleware/ControlScripts.php` | Принимать params_schema |
| `server/SHServ/Middleware/ScriptsRegistry.php` | Хранить params_schema |
| `server/SHServ/Helpers/ScriptParamsValidator.php` | Создать |
| `server/SHServ/Controllers/ScriptsRESTAPIController.php` | Валидация при run |
| `webclient/src/components/script/ScriptRunModal.vue` | Создать |
| `webclient/src/components/script/ActionScriptsGrid.vue` | Открывать модал |
| `webclient/src/features/scripts/pages/ScriptDetailPage.vue` | Открывать модал |

## Риски

- **Lazy sync side-effect:** `get_scripts_list()` делает UPDATE. Альтернатива: синхронизировать при старте сервера в `ControlScripts::add_action_script()` через `Scripts::set_script_state()` (который уже делает INSERT/UPDATE), но туда нужно добавить `params_schema`. Это чище. Решение: обновить `set_script_state()` чтобы при INSERT/UPDATE сохранять `params_schema` из registry.
- **Каскад миграции:** `params_schema TEXT NULL` совместимо с существующими данными.
- **Vue компонент не в kit:** создаём свой `ScriptRunModal.vue` на базе `GnModal` и kit-инпутов.

## Решение по lazy sync
Вместо lazy sync в read-методе — обновить `Scripts::set_script_state()`. Когда `add_action_script()` вызывается при старте, `ControlScripts` может вызвать `set_script_state("action", $alias, true)` (или новый метод) для записи `params_schema` в БД. Но `set_script_state()` сейчас вызывается только через API enable/disable.

Наиболее чистый путь: в `Scripts::get_scripts_list()` при наличии `$script_entity` сравнивать `params_schema` и делать `thin_builder->update()` если отличается. Это один UPDATE на рестарт, приемлемо.
