diff --git a/docs/compose/specs/2026-07-05-vikon-partial-updates-design.md b/docs/compose/specs/2026-07-05-vikon-partial-updates-design.md new file mode 100644 index 0000000..af14082 --- /dev/null +++ b/docs/compose/specs/2026-07-05-vikon-partial-updates-design.md @@ -0,0 +1,220 @@ +# VikonPartialUpdates — Частичные обновления модулей VIKON + +## [S1] Проблема + +Текущий модуль `VikonIntegration` поддерживает только полное обновление ядра модуля (`UpdateCoreAction`) и синхронизацию файлов через FM (`SyncFilesAction`). Частичные обновления (обновление отдельных частей модуля — common, struct, document...) не реализованы. + +В `vikon_core` частичные обновления реализованы через 4-шаговый flow: +1. Запрос генерации части на сервере +2. Polling статуса генерации +3. Проверка готовности +4. Скачивание и применение + +Нужно перенести эту логику в VikonIntegration с учётом архитектуры Porto. + +--- + +## [S2] Обзор решения + +**Backend-driven polling**: фронт отправляет один запрос `POST /update-part`, backend выполняет всю цепочку синхронно (request → polling → download → apply), возвращает результат. `HttpTask` уже устанавливает `set_time_limit(600)`, поэтому запрос может блокироваться до 10 минут — это достаточно для polling flow. Фронт показывает спиннер во время ожидания. + +**Полная поддержка**: обычные части (atomic swap папки) + ABITUR (пофайловая синхронизация). + +--- + +## [S3] Backend компоненты + +### Новый Action: `UpdatePartAction` + +**Расположение:** `app/Containers/VikonIntegration/Actions/UpdatePartAction.php` + +**Зависимости:** `HttpTask`, `FilesystemTask`, `PollPartStatusTask` + +**Интерфейс:** +```php +public function run(int $moduleId, string $part, string $accessToken): array +``` + +**Алгоритм:** +1. Валидация `moduleId` и `part` (whitelist из конфига) +2. Запрос генерации: `POST pull_updates/requestGeneratePartByNewCoreJson` с `part` +3. Получение `operation_identity` и `ttl` +4. Polling статуса через `PollPartStatusTask` (`GET pull_updates/getStatusPartGenerationByNewCoreJson`) +5. Проверка готовности: `POST pull_updates/checkPartGenerationByNewCoreResultJson` +6. Скачивание ZIP: `GET pull_updates/downloadPartByNewCoreResult?operation_identity=...&part=...` +7. Распаковка во временную директорию +8. Определение moduleId части (по `$allowedFoldersInCoreByModule`) +9. Применение: + - **Обычная часть**: atomic swap всей папки + - **ABITUR**: пофайловая синхронизация всех entry +10. `cleanUnitCore` с исключениями +11. Очистка temp файлов + +**Возвращаемое значение:** +```php +['success' => true, 'message' => '...', 'synced_count' => int] +``` + +### Новый Task: `PollPartStatusTask` + +**Расположение:** `app/Containers/VikonIntegration/Tasks/PollPartStatusTask.php` + +**Интерфейс:** +```php +public function run(string $operationIdentity, string $accessToken, int $ttl = 120): array +``` + +**Алгоритм:** +1. Отправка GET `pull_updates/getStatusPartGenerationByNewCoreJson` с `operation_identity` +2. Если `status === "completed"` → возврат `['status' => 'completed']` +3. Если `status === "failed"` → возврат `['status' => 'failed', 'error' => ...]` +4. Если `status === "pending"` и время < `ttl` → sleep(3) и повтор +5. Если timeout → возврат `['status' => 'timeout']` + +**Конфигурация:** +- Интервал polling: 3 секунды (configurable через конфиг) +- Max attempts: `ceil(ttl / interval)` + 2 (запас) + +### Модификация `FilesystemTask` + +Добавить метод `atomicSwap()`: +```php +public function atomicSwap( + string $newEntryPath, + string $currentEntryPath, + string $baseDir, + int $moduleId +): bool +``` + +**Алгоритм:** +1. Если `$currentEntryPath` существует: + - Удалить `$currentEntryPath_new` если есть + - Переместить `$newEntryPath` → `$currentEntryPath_new` + - Удалить `$currentEntryPath_old` если есть + - Переместить `$currentEntryPath` → `$currentEntryPath_old` + - Переместить `$currentEntryPath_new` → `$currentEntryPath` +2. Если не существует: + - Переместить `$newEntryPath` → `$currentEntryPath` + +### Модификация `VikonController` + +Добавить метод: +```php +public function updatePart(UpdatePartRequest $request): JsonResponse +``` + +**Маршрут:** `POST /dashboard/vikon-updates/update-part` + +**Middleware:** `access-check`, `dashboard.auth`, `throttle:10,1`, `vikon.refresh` + +--- + +## [S4] Frontend компоненты + +### Модификация `Index.vue` + +**Новые Props:** +- `available_parts`: объект с доступными частями по модулям + +**Новые UI-элементы:** +- Выпадающий список частей для выбранного модуля +- Кнопка "Обновить часть" рядом с каждым модулем +- Прогресс-бар с логом во время обновления +- Индикатор загрузки (спиннер) во время polling + +**Data flow:** +1. Пользователь выбирает модуль → показываются доступные части +2. Пользователь нажимает "Обновить" → отправляется `POST /update-part` +3. Фронт показывает спиннер/прогресс-бар пока backend выполняет цепочку +4. Получает ответ → показывает результат (успех/ошибка) + +--- + +## [S5] API эндпоинты + +### `POST /dashboard/vikon-updates/update-part` + +**Request:** +```json +{ + "module_id": 1, + "part": "common" +} +``` + +**Response (success):** +```json +{ + "success": true, + "message": "Часть 'common' успешно обновлена.", + "operation_identity": "...", + "synced_count": 15 +} +``` + +**Response (error):** +```json +{ + "success": false, + "message": "Ошибка при генерации части: timeout" +} +``` + +--- + +## [S6] ABITUR special case + +Для ABITUR (moduleId=2) в шаге 4 используется пофайловая синхронизация: + +1. Сканирует содержимое распакованного ZIP +2. Для каждого entry (файл/папка) делает atomic swap через `FilesystemTask::atomicSwap()` +3. Исключает `files/` и `.htaccess` из `cleanUnitCore` +4. Не создаёт `sveden/update/index.php` redirect (это только для SVEDEN) + +--- + +## [S7] Ошибки и откат + +| Шаг | Ошибка | Действие | +|-----|--------|----------| +| Запрос генерации | HTTP ошибка | Возврат ошибки клиенту | +| Polling | Timeout (> ttl) | Возврат ошибки, очистка temp | +| Проверка готовности | Часть не готова | Возврат ошибки | +| Скачивание ZIP | HTTP ошибка | Возврат ошибки | +| Распаковка | Невалидный ZIP | Возврат ошибки | +| Atomic swap | Ошибка rename | `restoreUnitCoreAfterFail()` → откат через `_old` суффиксы | +| cleanUnitCore | Ошибка удаления | Логирование, продолжение | + +--- + +## [S8] Конфигурация + +Добавить в `config/vikon.php`: + +```php +'parts' => [ + 1 => ['common', 'struct', 'document', 'education', 'managers', 'employees', 'objects', 'paid_edu', 'budget', 'vacant', 'grants', 'inter', 'catering', 'eduStandarts', 'corruption', 'antiterrorism'], + 2 => ['abitur'], + 6 => ['general', 'structure', 'faq', 'procedures', 'results-and-reports', 'plans', 'survey'], +], +'poll_interval' => 3, +'poll_max_attempts' => 50, +``` + +--- + +## [S9] Зависимости + +- Существующие: `HttpTask`, `FilesystemTask`, `ValidateTokenTask` +- Новые: `PollPartStatusTask`, `UpdatePartAction` +- Frontend: Vue 3 Composition API, Inertia.js + +--- + +## [S10] Тестирование + +1. **Unit-тест `PollPartStatusTask`**: mock HTTP, проверка polling loop +2. **Unit-тест `FilesystemTask::atomicSwap()`**: проверка 3-шагового flow +3. **Integration-тест `UpdatePartAction`**: mock API responses, проверка полного flow +4. **Manual test**: подключение к реальному API VIKON, обновление одной части