diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 6e5a6b89..0106b117 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -1,16 +1,16 @@ name: Deploy docs to Pages # Builds the MkDocs (Material) site and publishes it to GitHub Pages. English -# (docs/en) is the single source; Russian (docs/ru) is generated at build time -# by tools/docs/translate.py and is NOT committed. Runs only when docs actually -# change, unlike GitHub's built-in "pages build and deployment". +# (docs/en) is the single source you edit; Russian (docs/ru) is a GENERATED tree +# that is committed and refreshed LOCALLY before a release (`make docs-translate`) +# — translation is intentionally NOT run in CI (it is slow). CI only builds the +# already-translated site. Runs only when docs actually change. on: push: branches: [main] paths: - 'docs/**' - 'mkdocs.yml' - - 'tools/docs/**' - '.github/workflows/pages.yml' workflow_dispatch: @@ -40,33 +40,9 @@ jobs: with: python-version: '3.12' - - name: Install docs toolchain + - name: Install docs build toolchain run: pip install -r docs/requirements.txt - # Per-file translation cache: only English files that changed since the - # last run hit the translation backend. The exact key misses whenever any - # docs/en file changes; restore-keys then pulls the most recent prior - # cache so translate.py can diff against it and re-translate just the delta. - - name: Restore translation cache - uses: actions/cache@v4 - with: - path: .docs-cache - key: docs-tr-${{ hashFiles('docs/en/**', 'tools/docs/**') }} - restore-keys: | - docs-tr- - - # Defaults to `translators` — free web engines, NO API key. A file whose - # translation fails (rate-limit/outage) gracefully falls back to English - # so the deploy never breaks, and is retried next run. Override with the - # DOCS_TRANSLATE_PROVIDER repo variable (+ the matching key secret) to use - # anthropic/deepl. - - name: Generate translated docs (ru) - env: - DOCS_TRANSLATE_PROVIDER: ${{ vars.DOCS_TRANSLATE_PROVIDER || 'translators' }} - ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }} - DEEPL_AUTH_KEY: ${{ secrets.DEEPL_AUTH_KEY }} - run: python tools/docs/translate.py --lang ru - - name: Build site (strict) run: mkdocs build --strict diff --git a/.gitignore b/.gitignore index 457698b1..85d2e802 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,8 @@ coverage.xml build/.php-cs-fixer.cache tools/test-stream-generator/sample.mp4 .understand-anything/* -# Docs: generated translations (from docs/en via tools/docs/translate.py), -# MkDocs build output, and the translation cache — none are committed. -docs/ru/ +# Docs: MkDocs build output and the translation cache. docs/ru IS committed — +# it is generated from docs/en (tools/docs/translate.py) and refreshed locally +# before a release, so CI only has to build it. /site/ .docs-cache/ diff --git a/Makefile b/Makefile index 015dd847..4fac8a4a 100644 --- a/Makefile +++ b/Makefile @@ -404,38 +404,42 @@ new: @mkdir -p ${DIST_DIR} # ─── Documentation (MkDocs Material + auto-translated ru) ──────────── -# English (docs/en) is the single source of truth; docs/ru is GENERATED from it -# by tools/docs/translate.py (never committed) and consumed by the mkdocs -# static-i18n plugin. The Pages workflow runs the same steps in CI. +# EDIT ENGLISH ONLY (docs/en). docs/ru is a GENERATED tree that is committed and +# refreshed by `make docs-translate` LOCALLY before a release — translation is +# deliberately kept out of CI (it is slow). CI (pages.yml) only builds the +# already-committed, already-translated tree. Never hand-edit docs/ru. # -# make docs-venv # one-time: local venv from docs/requirements.txt -# make docs-serve # translate (noop) + live preview at :8000 -# make docs-build # translate + strict static build into ./site -# make docs-translate # (re)generate docs/ru only +# make docs-venv # one-time: local venv (build + translation deps) +# make docs-serve # live preview at :8000 (builds committed en+ru) +# make docs-build # strict static build into ./site (what CI runs) +# make docs-translate # release step: (re)generate docs/ru from docs/en, then commit # -# Engine via DOCS_TRANSLATE_PROVIDER (default: translators = free, no API key): -# make docs-build # free web engines (yandex/...) -# make docs-build DOCS_TRANSLATE_PROVIDER=anthropic # needs ANTHROPIC_API_KEY -# make docs-build DOCS_TRANSLATE_PROVIDER=noop # copy en (fast dry-run) +# Translation engine via DOCS_TRANSLATE_PROVIDER (default: translators = free, no key): +# make docs-translate # free web engines (yandex/...) +# make docs-translate DOCS_TRANSLATE_PROVIDER=anthropic # needs ANTHROPIC_API_KEY +# make docs-translate DOCS_TRANSLATE_PROVIDER=noop # copy en (fast dry-run) DOCS_VENV := .docs-venv DOCS_PY := $(DOCS_VENV)/bin/python DOCS_TRANSLATE_PROVIDER ?= translators .PHONY: docs-venv docs-translate docs-build docs-serve -# The venv's mkdocs binary doubles as the install stamp (built once). +# The venv's mkdocs binary doubles as the install stamp (built once). Installs +# both the build toolchain and the (local-only) translation deps. $(DOCS_VENV)/bin/mkdocs: @python3 -m venv $(DOCS_VENV) @$(DOCS_VENV)/bin/pip install -q --upgrade pip - @$(DOCS_VENV)/bin/pip install -q -r docs/requirements.txt + @$(DOCS_VENV)/bin/pip install -q -r docs/requirements.txt -r tools/docs/requirements.txt docs-venv: $(DOCS_VENV)/bin/mkdocs +# Release-time step: regenerate the committed docs/ru from docs/en. Commit the +# result with the release. NOT part of docs-build (CI builds the committed tree). docs-translate: $(DOCS_VENV)/bin/mkdocs @DOCS_TRANSLATE_PROVIDER=$(DOCS_TRANSLATE_PROVIDER) $(DOCS_PY) tools/docs/translate.py --lang ru -docs-build: docs-translate +docs-build: $(DOCS_VENV)/bin/mkdocs @$(DOCS_PY) -m mkdocs build --strict -docs-serve: docs-translate +docs-serve: $(DOCS_VENV)/bin/mkdocs @$(DOCS_PY) -m mkdocs serve diff --git a/docs/en/builds/updates_checklist.md b/docs/en/builds/updates_checklist.md index c2b1429b..f9e152f8 100644 --- a/docs/en/builds/updates_checklist.md +++ b/docs/en/builds/updates_checklist.md @@ -47,6 +47,25 @@ VERSION="X.Y.Z" > ⚠️ Do not create a separate version-bump commit/push at this step. > Otherwise `dist/changes.md` will include extra release commits and force additional edits. +### Regenerate translated documentation + +Documentation is written in **English only** (`docs/en`). The Russian tree +(`docs/ru`) is a **generated, committed** artifact refreshed locally before each +release — translation is intentionally **not** run in CI (it is slow); CI only +builds the committed tree. If `docs/en` changed since the last release: + +```bash +make docs-translate # regenerate docs/ru from docs/en (free, no API key) +make docs-build # strict build — fails on any broken link/anchor +``` + +- `make docs-translate` re-translates only the English files whose content + changed (per-file cache), so this is fast on an incremental release. +- **Review and commit the regenerated `docs/ru`** — it is included in the single + release commit (step 5). Never hand-edit `docs/ru`. +- Pushing the docs change triggers `pages.yml`, which builds and publishes the + site to GitHub Pages. + --- ## 3. Deleted Files @@ -117,6 +136,7 @@ sed -i "s/define('XC_VM_VERSION', *'[0-9]\+\.[0-9]\+\.[0-9]\+');/define('XC_VM_V ```bash git add src/Core/Config/AppConfig.php changelog.json src/migrations/deleted_files.txt +git add docs/en docs/ru # include any doc edits + the regenerated ru (step 2) git commit -m "Prepare release ${VERSION}" git push ``` @@ -220,3 +240,12 @@ Every `make` target used during release prep, in one place. | `make lb` | Build the LoadBalancer archive into `dist/` | | `make main` | Build the MAIN archive into `dist/` | | `bash tools/test-install/test_release.sh` | Docker install test of the built release | + +**Documentation** (English source in `docs/en`; `docs/ru` is generated + committed): + +| Command | Purpose | +| --- | --- | +| `make docs-venv` | One-time: local venv (build + translation deps) | +| `make docs-translate` | Regenerate `docs/ru` from `docs/en` (before a release) | +| `make docs-build` | Strict MkDocs build into `./site` (what CI runs) | +| `make docs-serve` | Live docs preview at `http://127.0.0.1:8000` | diff --git a/docs/requirements.txt b/docs/requirements.txt index 30934bd0..d0cd5d56 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,15 +1,6 @@ -# Documentation site toolchain (MkDocs + Material) and the translation pipeline. -# Used by `make docs-serve` / `make docs-translate` locally and by the Pages -# workflow (.github/workflows/pages.yml). +# MkDocs site BUILD toolchain — used by CI (.github/workflows/pages.yml) and by +# `make docs-build` / `docs-serve`. Translation is a separate, local-only step; +# its dependencies live in tools/docs/requirements.txt. mkdocs-material==9.7.7 mkdocs-static-i18n==1.3.1 - -# Translation backend for tools/docs/translate.py. Pick per DOCS_TRANSLATE_PROVIDER: -# translators = free, NO API KEY (web engines: yandex/google/bing/...) — default -# anthropic = highest quality for structure-heavy Markdown (needs ANTHROPIC_API_KEY) -# deepl = DeepL API (needs DEEPL_AUTH_KEY) -# noop = copy English verbatim (structural dry-runs; no dependency) -translators>=5.9 -# anthropic>=0.40 # uncomment for DOCS_TRANSLATE_PROVIDER=anthropic -# deepl>=1.18 # uncomment for DOCS_TRANSLATE_PROVIDER=deepl diff --git a/docs/ru/README.md b/docs/ru/README.md new file mode 100644 index 00000000..4f123fb3 --- /dev/null +++ b/docs/ru/README.md @@ -0,0 +1,84 @@ +# XC_VM Панель IPTV + +![Vateron Media Logo](https://avatars.githubusercontent.com/u/149707645?s=400&v=4) + +Современная IPTV-панель с открытым исходным кодом, вдохновленная Xtream Codes. Легкая, быстрая и ориентированная на сообщество. + +--- + +## что это такое? + +** XC_VM** - это современная IPTV-панель, построенная на PHP, Nginx, FFmpeg и MariaDB. + +XC_VM поможет вам развернуть полноценную инфраструктуру IPTV: + +- Управление телевизионными трансляциями, фильмами и сериалами в прямом эфире +- Управление торговыми посредниками и пользователями +- балансировка нагрузки +- EPG и VOD система +- Инструменты мониторинга и API + +💡 * Абсолютно бесплатно. Никаких лицензий. Никаких блокировок сервера.* + +--- + +## Особенности + +- 🚀 Современная архитектура IPTV-панелей +- 🔀 Встроенная поддержка load balancer +- 🎥 Управление VOD и прямыми трансляциями +- 🧩 API, совместимый с Xtream Codes +- 🔐 Улучшенные исправления в системе безопасности +- 📦 Быстрое транскодирование на основе FFmpeg +- 🌍 Документация на нескольких языках +- 🧭 Простой и интуитивно понятный интерфейс +- 🛠 Модульная система расширения + +--- + +## Технологии + +- **Nginx** — обратный прокси и веб-сервер +- **PHP 8.1** — основная серверная часть +- **MariaDB** — база данных +- **База данных ключей** — механизм кэширования/сеанса +- **FFmpeg 8.0** — перекодирование +- **yt-dlp** — медиа-эквайринг + +XC_VM официально поддерживает Ubuntu 22.04 и протестирован 24.04. + +--- + +## Сообщество + +XC_VM - это проект, полностью ориентированный на сообщество. + +- 💬 [Проблемы](https://github.com/Vateron-Media/XC_VM/issues) +- ⭐ Звезды GitHub: поддержите проект, присвоив ему звездочку +- 🔧 Запросы на извлечение: приветствуются + +Если вы хотите внести свой вклад, ознакомьтесь с: +[Руководство по внесению вклада](https://github.com/Vateron-Media/XC_VM/blob/master/CONTRIBUTING.md) + +--- + +## Установка + +Установка на Ubuntu 22.04+: + +```bash +sudo apt update && sudo apt full-upgrade -y +sudo apt install -y python3-pip unzip + +latest_version=$(curl -s https://api.github.com/repos/Vateron-Media/XC_VM/releases/latest | grep '"tag_name":' | cut -d '"' -f 4) +wget "https://github.com/Vateron-Media/XC_VM/releases/download/${latest_version}/XC_VM.zip" + +unzip XC_VM.zip +sudo python3 install +``` + +--- + +## Оговорка + +> Вы несете полную ответственность за использование вами данного программного обеспечения. diff --git a/docs/ru/administration/backup-strategy.md b/docs/ru/administration/backup-strategy.md new file mode 100644 index 00000000..cb81a90c --- /dev/null +++ b/docs/ru/administration/backup-strategy.md @@ -0,0 +1,222 @@ +# Стратегия резервного копирования + +XC_VM поддерживает автоматическое и ручное резервное копирование баз данных с помощью локального хранилища и дополнительной загрузки в Dropbox. +Управление резервными копиями осуществляется с помощью панели администратора, команд CLI и задания cron. + +--- + +## Что сохраняется в резервной Копии + +Резервные копии содержат полную структуру базы данных и данные, **за исключением** следующих таблиц: + +```text +detect_restream_logs, epg_data, lines_activity, lines_live, +lines_logs, login_logs, mag_claims, mag_logs, mysql_syslog, +panel_logs, panel_stats, servers_stats, signals, +streams_errors, streams_logs, streams_stats, syskill_log, +users_credits_logs, users_logs, watch_logs +``` + +> **Примечание:** При восстановлении резервной копии все данные журнала будут удалены. Эти таблицы исключены, чтобы можно было управлять размерами резервных копий. + +Резервные копии **не** включают: + +- Данные файловой системы (записи, VOD files, EPG XML) +- Файлы конфигурации (`config/`) +- Бинарные зависимости (`bin/`) +- Временные файлы (`tmp/`) + +--- + +## Конфигурация + +Настройки находятся в панели администратора в разделе **Резервные копии**: + +|Установка|По умолчанию|Описание| +| --- | --- | --- | +| `automatic_backups` | `off` |частота: `off`, `hourly`, `daily`, `weekly`, `monthly`| +| `backups_to_keep` | `0` |количество локальных хранилищ (0 = неограниченно)| +| `dropbox_remote` | `0` |включить загрузку в Dropbox| +| `dropbox_keep` | `0` |количество удаленных записей (0 = неограниченно)| +| `dropbox_token` | `''` |Токен API Dropbox| + +--- + +## Создание резервных копий + +### Руководство пользователя (панель администратора) + +Нажмите **Создать резервную копию сейчас** на странице резервных копий. При этом задание cron будет запущено в принудительном режиме: + +```bash +/home/xc_vm/console.php cron:backups 1 +``` + +### Автоматический (cron) + +Задание `cron:backups` проверяет расписание при каждом запуске: + +|График|Интервал| +| --- | --- | +| `hourly` |3600 - е годы| +| `daily` |86400-е годы| +| `weekly` |604800-е годы| +| `monthly` |2419200 - е годы| + +Запускается только на главном сервере (`is_main=1`). Используется блокировка на основе PID для предотвращения дублирования запусков. + +### Процесс резервного копирования + +1. Закройте MySQL соединение перед сбросом. +2. Запустите `mysqldump --no-data` (структура) + `mysqldump --ignore-table` (данные, за исключением таблиц журнала). +3. Проверьте размер файла (пустые файлы удаляются). +4. Если включен Dropbox: загружайте с отслеживанием статуса. +5. Примените политику хранения (удалите самые старые файлы, превышающие установленный лимит). + +### Расположение файла + +```text +/home/xc_vm/backups/backup_YYYY-MM-DD_HH:MM:SS.sql +``` + +--- + +## Восстановление резервных копий + +### Из панели администратора + +Нажмите **Восстановить** на любой записи резервной копии. Требуется подтверждение. + +Процесс: + +1. Если существует локальный файл, используйте его. В противном случае загрузите из Dropbox на `/home/xc_vm/tmp/restore.sql`. +2. Удалите базу данных и создайте ее заново. +3. Импортируйте SQL-файл. +4. Повторно загрузите структуру после импорта. + +```php +BackupService::restore($filename, $config) +``` + +> **Важно:** При восстановлении удаляется вся база данных и создается заново. Все данные, отсутствующие в резервной копии, будут потеряны. + +### Из CLI + +Для сценариев миграции с выборочным импортом таблиц: + +```bash +sudo /home/xc_vm/console.php tools migration /path/to/backup.sql +``` + +При этом выполняется восстановление в базе данных `xc_vm_migrate` для выборочного переноса данных, а не перезапись действующей базы данных. + +--- + +## Удержание + +### Локальное удержание + +- Если `backups_to_keep > 0`: сохраняются только N самых последних файлов. Сначала удаляются самые старые. +- If `backups_to_keep = 0`: сохраняет все файлы (неограниченно). + +### Удаленное хранение + +- Если `dropbox_keep > 0`: в Dropbox сохраняются только N самых последних файлов. Самые старые удаляются первыми. +- If `dropbox_keep = 0`: сохраняет все удаленные файлы (неограниченное количество). + +Очистка выполняется автоматически после каждого резервного копирования с помощью `BackupsCronJob`. + +--- + +## Интеграция с Dropbox + +Файл: `src/Core/Storage/DropboxClient.php` + +Когда `dropbox_remote` включен: + +1. После создания локальной резервной копии загрузите ее в Dropbox. +2. Во время загрузки создается файл `.uploading` marker. +3. В случае успеха: `.uploading` удаляется. +4. При сбое: `.error` создается файл с сообщением об ошибке. + +Индикаторы состояния панели администратора: + +|Показатель|Значение| +| --- | --- | +|Зеленый|успешно загружен| +|Желтый|текущая загрузка (менее 10 минут назад)| +|Красный|ошибка загрузки (наведите курсор на сообщение об ошибке)| +|Серый|не загружен| + +Методы: + +```php +BackupService::checkRemoteConnection() // validate Dropbox token +BackupService::uploadRemote($path, $filename, $overwrite = true) // upload backup +BackupService::downloadRemote($path, $filename) // download backup +BackupService::deleteRemote($path) // delete remote backup +BackupService::getRemote() // list remote backups +``` + +--- + +## Команды CLI + +### cron:резервные копии + +Автоматическое задание cron для резервного копирования. Может быть задано принудительно с аргументом `1`: + +```bash +sudo -u xc_vm /home/xc_vm/console.php cron:backups +sudo -u xc_vm /home/xc_vm/console.php cron:backups 1 # force +``` + +### миграция инструментов + +Восстановите резервную копию в базе данных миграции для выборочного импорта: + +```bash +sudo /home/xc_vm/console.php tools migration /path/to/backup.sql +``` + +### база данных инструментов - подтвердите + +Сброс в пустую базу данных (уничтожает все данные): + +```bash +sudo /home/xc_vm/console.php tools database --confirm +``` + +### инструменты mysql + +Повторная авторизация подсистем балансировки нагрузки на MySQL: + +```bash +sudo /home/xc_vm/console.php tools mysql +``` + +--- + +## Конечная точка API + +Действие: `backup` (требуется разрешение `adv:database`) + +|Вспомогательное действие|Описание| +| --- | --- | +| `backup` |запуск немедленного резервного копирования (в фоновом режиме)| +| `delete` |удалить локальную резервную копию + копию из Dropbox| +| `restore` |восстановление базы данных из резервной копии| + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Core/Backup/BackupService.php` |логика резервного копирования/восстановления| +| `src/Core/Storage/DropboxClient.php` |Клиент Dropbox API| +| `src/Cli/CronJobs/BackupsCronJob.php` |автоматизированный cron для резервного копирования| +| `src/Cli/Commands/ToolsCommand.php` |Инструменты миграции командной строки и базы данных| +| `src/Public/Views/admin/backups.php` |пользовательский интерфейс панели администратора| +| `src/Public/Views/admin/api.php` |Обработчик конечной точки API| +| `src/Public/Controllers/Admin/BackupsController.php` |административный контроллер| diff --git a/docs/ru/administration/reseller-system.md b/docs/ru/administration/reseller-system.md new file mode 100644 index 00000000..31deb144 --- /dev/null +++ b/docs/ru/administration/reseller-system.md @@ -0,0 +1,281 @@ +# Система реселлеров + +Система реселлеров обеспечивает многоуровневое управление партнерскими отношениями с предоставлением кредитных линий. +Реселлеры создают IPTV-линии, устройства MAG и Enigma2 и управляют ими в рамках выделенных им кредитов и разрешений. + +--- + +## Обзор + +```text +Admin + └── assigns credits + group permissions + └── Reseller + ├── creates IPTV lines (costs credits) + ├── creates MAG devices (costs credits) + ├── creates Enigma2 devices (costs credits) + └── creates sub-resellers (costs credits) + └── sub-reseller has own lines + credits +``` + +Основная бизнес-логика находится в `src/Domain/User/ResellerAPI.php`. Веб-контроллеры находятся в `src/Public/Controllers/Reseller/`. REST API находится в `src/Public/Controllers/Api/ResellerRestApiController.php`. + +--- + +## Кредитная система + +Кредиты являются валютой для всех операций посредника. Каждое действие имеет определенную стоимость, и баланс посредника должен ее покрывать. + +### Затраты по кредиту + +|Действие|Источник затрат| +| --- | --- | +|Создать линию (официальную)| `package.official_credits` | +|Создать строку (пробная версия)| `package.trial_credits` | +|Создать магнитное устройство|то же, что линия| +|Создание устройства Enigma2|то же, что линия| +|Создать суб-реселлера| `permissions.create_sub_resellers_price` | + +### Переопределение ценообразования + +Реселлеры могут устанавливать индивидуальные цены для каждого пакета с помощью `override_packages` JSON в своей пользовательской записи: + +```php +$rOverride = json_decode($rUserInfo['override_packages'], true); +if (isset($rOverride[$rPackage['id']]['official_credits'])) { + $rCost = intval($rOverride[$rPackage['id']]['official_credits']); +} +``` + +### Кредитный перевод + +Реселлеры могут переводить кредиты своим непосредственным подчиненным с помощью действия `adjust_credits` API. Оба баланса должны оставаться >= 0. + +### Регистрация + +Все кредитные операции записываются в `users_logs`: + +|Поле|Описание| +| --- | --- | +| `owner` |идентификатор пользователя торгового посредника| +| `type` |`line`, `mag`, `enigma`, `user`| +| `action` |`new`, `extend`, `edit`, `adjust_credits`| +| `cost` |потраченные кредиты| +| `credits_after` |баланс после операции| +| `package_id` |использованный пакет| +| `date` |отметка времени| + +--- + +## Линейное управление + +Строки - это подписки пользователей на IPTV. Типы: + +|Тип|Флаги| +| --- | --- | +|Стандартная линия IPTV| `is_mag=0, is_e2=0` | +|МАГНИТНОЕ устройство| `is_mag=1` | +|Устройство Enigma2| `is_e2=1` | + +### Процесс создания + +1. Проверьте доступность пакета (должен быть указан в разрешениях группы реселлеров). +2. Проверьте `credits >= cost`. +3. Сгенерируйте имя пользователя/пароль, если это разрешено разрешениями. +4. Применить пакет: `exp_date`, `max_connections`, `bouquets`, `allowed_outputs`. +5. Установите ограничения: `allowed_ips` (JSON), `allowed_ua`, `bypass_ua`, `is_isplock`. +6. Вставьте в таблицу `lines` через `REPLACE INTO`. +7. Синхронизируйте записи устройства (`mag_devices` или `enigma2_devices`). +8. Передача сигнала о событии на потоковые серверы. +9. Вычтите кредиты и зарегистрируйте транзакцию. + +### Назначение букета + +В каждом пакете указаны доступные букеты с помощью массива `bouquets` JSON. +Если включено разрешение `allow_change_bouquets`, реселлер может выбрать набор букетов из пакета. В противном случае все букеты из пакета будут назначены автоматически. + +--- + +## Управление устройствами + +### МАГНИТНЫЕ устройства + +Управляемый `MagService` (`src/Domain/Device/MagService.php`). +Поля блокировки: `ver`, `device_id2`, `device_id`, `hw_version`, `image_version`, `stb_type`, `sn`. + +### Устройства Enigma2 + +Управляемый `EnigmaService` (`src/Domain/Device/EnigmaService.php`). +Поля блокировки: `token`, `lversion`, `cpu`, `enigma_version`, `modem_mac`, `local_ip`. + +Оба типа устройств поддерживают `lock_device` (аппаратная привязка), `is_isplock` (привязка к провайдеру) и `forced_country`. + +--- + +## Иерархия суб-реселлеров + +Реселлеры могут создавать суб-реселлеров (при наличии разрешения `create_sub_resellers`).: + +- Суб-реселлеры связаны через поле `owner_id`. +- Многоуровневый: суб-реселлер может создавать своих собственных суб-реселлеров. +- Каждое создание стоит `create_sub_resellers_price` кредитов. +- Присвоенный `member_group_id` должен находиться в родительском массиве разрешений `subresellers`. + +Запросы о праве собственности: + +```php +Authorization::check('user', $rID) // checks reseller hierarchy +Authorization::check('line', $rID) // checks if reseller's reports own the line +``` + +Методы: + +```php +UserRepository::getResellers($rOwner, $rIncludeSelf) +UserRepository::getDirectReports() +AuthRepository::getGroupPermissions() // builds all_reports recursively +``` + +--- + +## Разрешения + +Разрешения поступают из таблицы `users_groups`, загружаемой через `AuthRepository::getPermissions()`. + +### Ключевые поля разрешений + +|Разрешение|Тип|Описание| +| --- | --- | --- | +| `is_reseller` | `bool` |пользователь является реселлером| +| `create_line` | `bool` |может создавать IPTV-линии| +| `create_mag` | `bool` |может создавать магнитные устройства| +| `create_enigma` | `bool` |может создавать устройства Enigma2| +| `create_sub_resellers` | `bool` |может создавать суб-реселлеров| +| `create_sub_resellers_price` | `int` |стоимость кредита на одного суб-реселлера| +| `allow_change_bouquets` | `bool` |можно выбрать bouquet подмножество| +| `allow_change_username` | `bool` |можно установить пользовательское имя пользователя| +| `allow_change_password` | `bool` |можно установить пользовательский пароль| +| `allow_restrictions` | `bool` |можно установить ограничения по IP/UA| +| `can_view_vod` | `bool` |может просматривать содержимое VOD| +| `reseller_client_connection_logs` | `bool` |можно просматривать журналы подключений| +| `minimum_username_length` | `int` |минимальная длина имени пользователя| +| `minimum_password_length` | `int` |минимальная длина пароля| + +### Проверки на уровне страницы + +`PageAuthorization::checkResellerPermissions()` сопоставляет страницы с разрешениями: + +|Страницы|Требуемое разрешение| +| --- | --- | +|`user`, `users`| `create_sub_resellers` | +|`line`, `lines`| `create_line` | +|`mag`, `mags`| `create_mag` | +|`enigma`, `enigmas`| `create_enigma` | +|`epg_view`, `streams`, `movies`| `can_view_vod` | +|`live_connections`, `line_activity`| `reseller_client_connection_logs` | + +### Границы + +Что реселлеры ** не могут ** делать: + +- Линии доступа/пользователи за пределами их иерархии. +- Создавайте или изменяйте пакеты. +- Доступ к настройкам доступен только для администратора. +- Превысить их кредитный баланс. +- Обходите групповые ограничения пакетов. + +--- + +## REST API + +Файл: `src/Public/Controllers/Api/ResellerRestApiController.php` + +Аутентификация с помощью API-ключа. Действия: + +|Действие|Описание| +| --- | --- | +| `user_info` |информация об учетной записи реселлера| +| `packages` |доступные пакеты| +|`get_lines` / `get_mags` / `get_enigmas`|список ресурсов| +|`create_line` / `edit_line` / `delete_line`|грубая линия| +|`enable_line` / `disable_line`|переключение состояния линии| +|`create_mag` / `edit_mag` / `delete_mag`|МАГИЧЕСКАЯ ДРЯНЬ| +|`create_enigma` / `edit_enigma` / `delete_enigma`|Загадочная ДРЯНЬ| +|`convert_mag` / `convert_enigma`|преобразовать тип устройства| +|`get_users` / `get_user`|список/просмотр суб-реселлеров| +|`create_user` / `edit_user` / `delete_user`|грубость субпродюсера| +|`enable_user` / `disable_user`|переключение статуса суб-реселлера| +| `adjust_credits` |перевод кредитов суб-реселлеру| +|`activity_logs` / `live_connections`|данные о подключении| +| `user_logs` |журналы действий суб-реселлеров| + +Класс `ResellerAPIWrapper` проверяет API-ключ, инициализирует сеанс с помощью `ResellerAPI` и возвращает отфильтрованные ответы в формате JSON. + +--- + +## Сессия и начальная загрузка + +### Сессия + +Файл: `src/Infrastructure/Bootstrap/reseller_session.php` + +- 60-минутный тайм-аут с отслеживанием последней активности. +- Обнаружение изменения IP-адреса (если включена настройка `ip_logout`). +- Ключи сеанса: `reseller` (идентификатор пользователя), `rip`, `rcode`, `rverify`, `rlast_activity`. + +### Функции начальной загрузки + +Файл: `src/Infrastructure/Bootstrap/reseller_functions.php` + +- Загружает базу данных и утилиты. +- Инициализирует `$rUserInfo` и `$rPermissions`. +- Проверяет целостность сеанса (проверка хэша имени пользователя/пароля). +- Устанавливает часовой пояс и языковые настройки. + +--- + +## Маршруты + +Файл: `src/Public/routes/reseller.php` + +Ключевые маршруты: + +```text +GET /dashboard → ResellerDashboardController +GET /edit_profile → ResellerEditProfileController +POST /post → ResellerPostController (form handler) +GET /api, POST /api → ResellerApiController +GET /table, POST /table → ResellerTableController + +GET /lines → ResellerLinesController +GET /line → ResellerLineController +GET /mags → ResellerMagsController +GET /mag → ResellerMagController +GET /enigmas → ResellerEnigmasController +GET /enigma → ResellerEnigmaController + +GET /users → ResellerUsersController +GET /user → ResellerUserController +GET /user_logs → ResellerUserLogsController +GET /live_connections → ResellerLiveConnectionsController +GET /line_activity → ResellerLineActivityController +GET /tickets → ResellerTicketsController +``` + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Domain/User/ResellerAPI.php` |основная бизнес-логика| +| `src/Public/Controllers/Api/ResellerRestApiController.php` |REST API| +| `src/Public/Controllers/Reseller/*.php` |веб-контроллеры| +| `src/Public/routes/reseller.php` |Маршрутизация URL-адресов| +| `src/Public/Views/reseller/*.php` |просмотр шаблонов| +| `src/Infrastructure/ResellerApiDispatcher.php` |Маршрутизация действий AJAX| +| `src/Infrastructure/ResellerTableRenderer.php` |Рендеринг таблиц данных| +| `src/Infrastructure/Bootstrap/reseller_session.php` |управление сеансами| +| `src/Infrastructure/Bootstrap/reseller_functions.php` |инициализация| +| `src/Core/Auth/Authorization.php` |проверки на право собственности| +| `src/Core/Auth/PageAuthorization.php` |стробирование на уровне страницы| diff --git a/docs/ru/administration/server-diagnostics.md b/docs/ru/administration/server-diagnostics.md new file mode 100644 index 00000000..46917801 --- /dev/null +++ b/docs/ru/administration/server-diagnostics.md @@ -0,0 +1,125 @@ +# Диагностика сервера (`server:diagnose`) + +Панель помечает узел proxy/LB как "отключенный" исключительно из-за устаревшего сердцебиения (`enabled` + `status = 1` + недавний `last_check_ago`), который никогда не сообщает вам, почему узел перестал отчитываться. Команда `server:diagnose` отвечает на этот вопрос. + +```bash +/home/xc_vm/console.php server:diagnose [server_id] +``` + +Команда доступна только для чтения: она запускает только запросы ping/curl/`fsockopen`, `SELECT` и `sudo -n iptables -nL`. Она никогда ничего не перезапускает и не перенастраивает. + +Реализовано `src/Cli/Commands/ServerDiagnoseCommand.php`. Команда поставляется как в основной, так и в LB сборках — весь смысл ее использования на узле заключается в локальном режиме. + +--- + +## Два режима + +Режим автоматически выбирается из того места, где выполняется команда (`server_id` в `config.ini` → `is_main` в таблице `servers`): + +### Режим A — дистанционный датчик от ОСНОВНОГО + +```bash +sudo /home/xc_vm/console.php server:diagnose +``` + +Проверяет целевой узел снаружи и считывает его состояние на панели управления: + +|Проверять|О чем это вам говорит| +| --- | --- | +|Включено / Статус / Сердцебиение|Как панель в данный момент видит узел (`status = 4` означает неудачную установку/подготовку)| +|Пинг по протоколу ICMP|Жив ли вообще носитель| +|TCP к `http_broadcast_port`|Доступен ли nginx, или порт удален| +|HTTP `GET /api`|Действительно ли @@ 1@@ отвечает за @@ 0@@| +|Смещение часов|`time_offset` против панели (перекос > 30 с может привести к переключению узла в режим онлайн/оффлайн)| +|Очередь сигналов|Неиспользованные строки в `signals` для этого узла (задержка > 120 секунд = цикл обратного вызова узла застрял)| + +Комбинации датчиков соответствуют причинам: + +- **Порт ICMP + не закрыт** — узел выключен, разделен сетью или полностью защищен брандмауэром. +- **ICMP отвечает, но порт не указан ** — классический случай: собственные iptables узла заблокировали IP-адрес главного сервера (RootSignals выдает ложные срабатывания), или nginx/служба не работает. Запустите локальный режим на узле для выяснения точной причины. +- **Порт открыт, но `/api` молчит** — nginx активен, PHP нет: проверьте php-fpm на узле. +- **`/api` отвечает, но частота сердечных сокращений устарела ** — демон узла watchdog (программа записи сердечных сокращений) не запущен или он не может выполнять запись в базу данных панели. Запустите локальный режим на узле. + +### Режим B — локальная самодиагностика НА узле + +```bash +sudo /home/xc_vm/console.php server:diagnose +``` + +Запустите это ** на самом узле LB/proxy с отключенным доступом ** — причины обычно находятся там. Аргумент не требуется; узел идентифицирует себя с помощью `config.ini`. Проверки: + +1. **Моя собственная строка панели ** — включено/статус/ сердцебиение в том виде, в каком их видит главный. +2. **Могу ли я подключиться к ГЛАВНОМУ** — подключение к базе данных (неявно подтвержденное), ICMP и TCP к широковещательному порту главного. +3. **Отключил ли я брандмауэр main?** — сканирует цепочку `iptables INPUT` этого узла на наличие `DROP` IP-адреса main, а также файла маркера блокировки флуда. Это классическая причина "узел отключился без причины": защита от наводнений автоматически отключает общедоступные IP-адреса, и обратные вызовы главного сервера перестают поступать. +4. **Сервис / nginx** — `systemctl is-active xc_vm` и локальная проверка TCP на собственном широковещательном порту узла. +5. ** Сторожевой демон ** — фактический регистратор сердцебиений: демон `watchdog` обновляет `last_check_ago` каждые несколько секунд. Когда его MySQL соединение с main прерывается, он ** ожидает возврата к базе данных ** (повторяя попытку каждые 5 секунд) и немедленно возобновляет сердцебиение. Вместо этого были запущены более старые сборки, из—за чего ** все узлы отключались в один и тот же момент ** при любом MySQL перезапуске / сбое на главном сервере, пока `cron:servers` не восстановил их. +6. **Хрон няни** — три дополнительные проверки, потому что мертвый watchdog остается мертвым только тогда, когда цепочка няни разорвана: + - присутствует ли `cron:servers` в **`xc_vm` пользовательской crontab** (если отсутствует, создайте заново с помощью `rm -f /home/xc_vm/tmp/crontab` и перезапустите службу); + - активна ли системная служба cron (нет cron → crontab никогда не запускается); + - является ли предыдущий экземпляр `cron:servers` зависшим из—за блокировки cron ** - зависший экземпляр блокирует каждый последующий запуск на срок до 30 минут (`acquireCronLock` устаревший тайм-аут), что в точности соответствует тому, как один сбой в работе базы данных удерживает узел в автономном режиме в течение получаса. Команда выводит удерживающий PID и команду завершения. +7. **Перекос часов** — `time_offset` по сравнению с панелью. + +> **Примечание:** для проверки iptables требуется sudo без пароля (`sudo -n`). Без этого проверка выдает `cannot check (need sudo iptables)` вместо сбоя — запустите команду как `root` для получения полной картины. + +--- + +## Коды вывода и выхода + +При каждой проверке выводится одна выровненная строка `[OK]`/`[WARN]`, за которой следует пронумерованная сводка о возможных причинах с указанием точной команды устранения, если таковая существует (например, строка `iptables -D INPUT ... -j DROP` разблокировать). + +|Код выхода|Значение| +| --- | --- | +| `0` |Очевидная причина не обнаружена (или цель сама по себе является ОСНОВНОЙ — диагностировать нечего).| +| `1` |Ошибка использования: отсутствует/неизвестно `server_id`| +| `2` |Была найдена и напечатана одна или несколько вероятных причин| + +Пример (локальный режим, самоблокирующийся основной): + +```text +Self-diagnosis on node #3 — LB-Frankfurt (type 1) +---------------------------------------------------------------- +[OK] Enabled yes +[OK] Status 1 (online) +[WARN] Heartbeat last check-in 641s ago (limit 180s) +[OK] DB → main reachable (this query ran) +[OK] Ping main reply (203.0.113.10) +[WARN] Main :8080 closed/timeout +[WARN] Main in iptables DROP present (+flood marker) +[OK] Service xc_vm active +[OK] nginx :8080 listening +[OK] watchdog daemon running +[OK] cron:servers in xc_vm crontab +[OK] Clock offset 2s vs panel +---------------------------------------------------------------- +Probable cause(s): + 1. Heartbeat is stale (641s > 180s): the node stopped reporting — the checks below narrow down why. + 2. This node has DROPPED the main's IP 203.0.113.10 in its own iptables (flood/block false-positive). Unblock: `sudo iptables -D INPUT -s 203.0.113.10 -j DROP && sudo rm -f /home/xc_vm/tmp/flood/block_203.0.113.10`. +``` + +--- + +## Краткий справочник — Распространенные причины + +|Симптом|Вероятная причина|Чинить| +| --- | --- | --- | +|Звенит, порт сброшен|Iptables узла заблокировали основной IP-адрес|`sudo iptables -D INPUT -s -j DROP` + удалить маркер `block_` (команда выводит точную строку)| +|Ни пинга, ни порта|Отключен хост / сетевой раздел / внешний брандмауэр|Проверьте консоль хостинга, маршруты, брандмауэр провайдера| +|Порт открыт, `/api` мертв|сбой php-fpm|Перезапустите службу `xc_vm` на узле| +|`/api` прекрасно, сердцебиение замедлилось.|Сторожевой демон мертв (завершает работу при потере соединения с базой данных)|`sudo -u xc_vm console.php watchdog` на узле; проверьте разрешения базы данных (`tools mysql` на главном)| +|**Все узлы падают в один и тот же момент**|Перезапуск/сбой MySQL на главном сервере приводит к одновременному сбою всех узлов watchdog (фатально для сборок до исправления; текущие сборки переждут это)|Проверьте журнал ошибок main MySQL во время сброса; обновите узлы, чтобы watchdog пережил перебои в работе| +|Закрылки узлов онлайн/оффлайн|Перекос часов > 30 с (или повторяющиеся сигналы MySQL)|Синхронизируйте протокол NTP на узле; проверьте стабильность MySQL на главном| +|Статус = 4|Произошла ошибка установки/обеспечения|Повторно запустите `server:install` из главного| + +--- + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/Cli/Commands/ServerDiagnoseCommand.php` |Диагностическая команда (в обоих режимах)| +| `src/Cli/Commands/WatchdogCommand.php` |Демон watchdog — записывает сердцебиение (`last_check_ago`)| +| `src/Cli/CronJobs/ServersCronJob.php` |Хрон няни — перезапускает мертвого watchdog| +| `src/Cli/CronJobs/RootSignalsCronJob.php` |Применяет блоки iptables (источник ложных срабатываний)| +| `src/Domain/Server/ServerRepository.php` |`servers` доступ к таблице| + +Смотрите также: [Инструменты интерфейса командной строки](../guides/cli-tools.md), [Обновление сервера](../administration/server-update.md). diff --git a/docs/ru/administration/server-update.md b/docs/ru/administration/server-update.md new file mode 100644 index 00000000..9c526f3c --- /dev/null +++ b/docs/ru/administration/server-update.md @@ -0,0 +1,37 @@ +# Обновление сервера + +Пошаговое руководство по обновлению сервера XC_VM. Более подробную информацию о процессе обновления смотрите в разделе [Механизм обновления](../administration/update-system.md). + +> 💾 Перед обновлением рекомендуется создать [резервную копию](../administration/backup-strategy.md). + +## Обновление через панель управления + +**Шаг 1.** Откройте раздел "Серверы" в верхнем меню панели. + +![Servers menu](../../_media/update1.png) + +**Шаг 2.** Выберите **Управление серверами** из выпадающего меню. + +![Manage Servers item](../../_media/update2.png) + +**Шаг 3.** Найдите целевой сервер в таблице "Серверы" и нажмите кнопку "Меню" в столбце "Действия". + +![Actions button](../../_media/update3.png) + +**Шаг 4.** Выберите **Серверные инструменты** в меню. + +![Server Tools item](../../_media/update4.png) + +**Шаг 5.** В диалоговом окне "Серверные инструменты"** нажмите "Обновить сервер"**. + +![Update Server button](../../_media/update5.png) + +При нажатии на кнопку в базу данных вводится сигнал `update`. В течение минуты CRON распознает его, и обновление выполняется автоматически: панель останавливается, обновляется и перезапускается. Вы можете отслеживать прогресс с помощью версии сервера в таблице **Управление серверами**. + +## Обновление вручную (CLI) + +```bash +sudo -u xc_vm /home/xc_vm/console.php update update +``` + +Загружает и применяет последнее обновление с GitHub. Обычно запускается автоматически через веб-панель. diff --git a/docs/ru/administration/ssl-generation.md b/docs/ru/administration/ssl-generation.md new file mode 100644 index 00000000..3718affa --- /dev/null +++ b/docs/ru/administration/ssl-generation.md @@ -0,0 +1,141 @@ +# Генерация SSL-сертификата для Nginx в XC_VM + +В этом руководстве объясняется, как создать самозаверяющий SSL-сертификат, чтобы включить безопасные HTTPS-соединения для встроенного сервера Nginx в проекте XC_VM. + +> **Примечание:** При новой установке уже создается ** уникальный** самозаверяющий сертификат +> автоматически (установщик запускает `openssl` и записывает `server.key`/`server.crt` +> в `bin/nginx/conf/` перед запуском Nginx), а `CertbotCronJob` позже заменяет +> с настоящим сертификатом Let's Encrypt. Следуйте этому руководству только для ** восстановления +> или замените** сертификат вручную. + +--- + +## Обзор + +**SSL (Secure Sockets Layer)** шифрует соединение между клиентом и сервером, обеспечивая конфиденциальность данных и доверие пользователей. +В этом руководстве показано, как создать самозаверяющий SSL-сертификат для встроенного сервера Nginx в проекте @XC_VM*. + +--- + +## Местоположение конфигурации + +Все файлы, связанные с SSL (ключ, сертификат и конфигурация), хранятся в: + +```bash +/home/xc_vm/bin/nginx/conf +``` + +Перейдите в этот каталог, прежде чем продолжить: + +```bash +cd /home/xc_vm/bin/nginx/conf +``` + +--- + +## Шаг 1. Сгенерируйте закрытый ключ + +Сгенерируйте **2048-битный закрытый ключ RSA**: + +```bash +openssl genrsa -out server.key 2048 +``` + +После выполнения появится файл `server.key` — это ваш **закрытый ключ**. +Храните его ** строго в тайне ** — он используется для подписи SSL-сертификата. + +--- + +## Шаг 2. Создайте конфигурационный файл server.cnf. + +Создайте файл конфигурации, содержащий параметры сертификата: + +```bash +cat > server.cnf << EOF +[req] +distinguished_name = req_distinguished_name +x509_extensions = v3_req +prompt = no + +[req_distinguished_name] +C = RU +ST = Moscow +L = Moscow +O = XC_VM +OU = XC_VM +CN = XC_VM + +[v3_req] +keyUsage = keyEncipherment, dataEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names + +[alt_names] +DNS.1 = XC_VM +EOF +``` + +**Объяснение параметров:** + +|Поле|Ценность|Цель| +| --- | --- | --- | +| `C` |ру|Страна| +| `ST` |Москва|Штат / Провинция| +| `L` |Москва|Город/населенный пункт| +| `O` | XC_VM |Организация| +| `OU` | XC_VM |Организационное подразделение| +| `CN` | XC_VM |Общее имя (основное имя хоста)| +| `DNS.1` | XC_VM |Альтернативное имя субъекта (SAN)| + +> ** Совет:** Для реальных доменных имен замените `DNS.1 = XC_VM` на ваш реальный домен (например, `DNS.1 = panel.example.com`), чтобы избежать предупреждений браузера. + +--- + +## Шаг 3. Сгенерируйте самозаверяющий SSL-сертификат + +Сгенерируйте сертификат, используя закрытый ключ и конфигурационный файл: + +```bash +openssl req -new -x509 -key server.key -out server.crt -days 3650 -config server.cnf +``` + +**Объяснение:** + +- `-new -x509` — создает новый самозаверяющий сертификат. +- `-days 3650` — срок действия сертификата (10 лет) +- `-config server.cnf` — использует пользовательскую конфигурацию +- Результат: `server.crt` файл, содержащий общедоступный сертификат. + +--- + +## Окончательные файлы + +После выполнения всех шагов в @@0 должны присутствовать следующие файлы@@: + +|Файл|Цель| +| --- | --- | +| `server.key` |Закрытый ключ| +| `server.crt` |Самозаверяющий SSL-сертификат| +| `server.cnf` |Файл конфигурации сертификата| + +--- + +## Результат + +Ваш сервер **XC_VM Nginx** теперь доступен по протоколу **HTTPS** с использованием недавно созданного самозаверяющего сертификата. +Браузеры будут отображать предупреждение “не доверенный” — это ожидаемое поведение для самозаверяющих сертификатов. + +--- + +## Записи + +- Самозаверяющие сертификаты подходят ** только для внутреннего использования или тестирования**. +- Для общедоступных доменов используйте сертификаты от доверенных центров сертификации (например, [Let's Encrypt](https://letsencrypt.org/)). +- Если вы измените имя домена/хоста (`CN` или `DNS.1`), вы должны повторно создать сертификат. +- Чтобы проверить сгенерированный сертификат: + +```bash +openssl x509 -in server.crt -text -noout +``` + +--- diff --git a/docs/ru/administration/update-system.md b/docs/ru/administration/update-system.md new file mode 100644 index 00000000..a27aec03 --- /dev/null +++ b/docs/ru/administration/update-system.md @@ -0,0 +1,171 @@ +# Механизм обновления в XC_VM + +Система обновления XC_VM реализована как многоуровневый процесс, от веб-интерфейса до сценариев системного уровня. Такой подход обеспечивает надежность, автоматизацию и целостность данных при обновлении панели. + +> 📋 Пошаговое руководство со скриншотами смотрите в разделе [Обновление сервера](../administration/server-update.md). + +--- + +## 1. Инициирование обновления + +Процесс начинается, когда администратор нажимает кнопку "Обновить" в веб-интерфейсе. + +- Сигнал с именем `update` вставляется в таблицу `signals` в базе данных. +- Этот сигнал действует как **триггер** для всей процедуры обновления. + +--- + +## 2. Триггер CRON + +Каждую **минуту** выполняется следующее задание CRON: + +```bash +/home/xc_vm/console.php cron:root_signals +``` + +Задание `root_signals` cron проверяет наличие новых сигналов. +Когда он обнаруживает сигнал `update`, он запускает: + +```bash +/home/xc_vm/console.php update update +``` + +--- + +## 3. Управление обновлениями (PHP Уровень) + +Основная логика находится в классе `UpdateCommand`: + +```text +src/Cli/Commands/UpdateCommand.php +``` + +На этом этапе выполняются следующие действия: + +1. Определите **текущий тип панели** (`MAIN` или `LB`). +2. Извлекать обновленные метаданные из **GitHub**: + - Прямая ссылка на архив обновлений. + - Контрольная сумма SHA для проверки целостности. +3. Загрузите архив во временный каталог. +4. Убедитесь, что загруженный файл соответствует ожидаемому хэшу. +5. Передайте управление программе обновления системного уровня (Python): + +```bash +sudo /usr/bin/python3 /home/xc_vm/update "/home/xc_vm/tmp/.update.tar.gz" "HASH" > /dev/null 2>&1 & +``` + +> 💡 После завершения обновления Python он вызывает `console.php update post-update`, который запускает [миграцию базы данных](../guides/cli-tools.md#database-updates-after-version-upgrade) и очистку после обновления. + +--- + +## 4. Обновление на системном уровне (уровень Python) + +Управление передается скрипту на Python: + +```text +/home/xc_vm/update +``` + +Он выполняет привилегированные системные операции: + +1. **Повторно проверьте контрольную сумму архива. +2. **Остановите панель **, чтобы предотвратить конфликты во время обновления. +3. **Извлеките** архив во временный каталог: + + ```bash + /tmp/xc_vm_update_*/ + ``` + +4. **Удалите исключенные каталоги** из временной копии — двоичные файлы, конфигурации и пользовательские данные, которые нельзя перезаписывать: + +`bin/ffmpeg_bin`, `bin/nginx`, `bin/nginx_rtmp`, `bin/php`, `bin/redis`, `bin/install`, `bin/maxmind`, `bin/certbot`, `content`, `backups`, `tmp`, `config`, `signals` + +5. **Скопируйте оставшиеся файлы** поверх текущей установки: + + ```bash + cp -a /tmp/xc_vm_update_*/. /home/xc_vm/ + ``` + +6. **Закрепить право собственности**: + + ```bash + chown -R xc_vm:xc_vm /home/xc_vm/ + ``` + +7. Выполнение задач после обновления: + + ```bash + /home/xc_vm/console.php update post-update + ``` + +8. **Перезапустите** панель в нормальном рабочем режиме. +9. **Очистите временный каталог и удалите архив. + +> ➡ ️ Для установки и обновления используется один и тот же архив. Фильтрация выполняется на сервере во время обновления — список исключений определяется непосредственно в `src/update`. + +--- + +## 5. Завершение обновления + +Заключительные шаги выполняются на этапе `post-update` из `UpdateCommand`: + +1. Если включено автоматическое обновление **LB** и был обновлен главный узел (`MAIN`) → создайте сигналы `update` для всех подсистем балансировки нагрузки. +2. Обновите **версию панели** в базе данных. +3. Удалите устаревшие файлы. +4. Повторно примените правильные разрешения: + + ```bash + chown -R xc_vm:xc_vm /home/xc_vm/ + ``` + +5. Перезагрузить systemd демонов: + + ```bash + sudo systemctl daemon-reload + ``` + +6. Проверка состояния панели: + + ```bash + sudo /home/xc_vm/console.php status + ``` + +7. Отметьте процесс обновления как завершенный. + +--- + +## 6. Полная схема рабочего процесса + +```text +[ Web Interface ] + │ + ▼ +[ DB: "update" signal ] + │ + ▼ +[ CRON → console.php cron:root_signals ] + │ + ▼ +[ UpdateCommand (PHP): download + verify hash ] + │ + ▼ +[ update (Python): extract to /tmp → remove excluded → copy over ] + │ + ▼ +[ post-update → UpdateCommand ] + │ + ▼ +[ Finalize, restart daemons, update version in DB ] +``` + +--- + +## ключевые функции + +- **Двойная проверка целостности ** (оба уровня PHP и Python проверяют хэш). +- **Автоматическое распространение обновлений с MAIN на все подсистемы балансировки нагрузки. +- **Очистка ** от устаревших файлов и нормализация разрешений. +- **Перезапуск безопасной панели ** после установки. +- **Гибкость и автономность ** благодаря запуску по сигналу CRON +. + +--- diff --git a/docs/ru/api/swagger.md b/docs/ru/api/swagger.md new file mode 100644 index 00000000..2f842dc3 --- /dev/null +++ b/docs/ru/api/swagger.md @@ -0,0 +1,31 @@ +# Интерактивная ссылка на API (Swagger) + +Интерактивный, всегда синхронизированный пользовательский интерфейс Swagger для каждого XC_VM API, созданный на основе спецификаций OpenAPI 3.0. Используйте вкладки API в верхней части страницы для переключения между API или откройте один из них напрямую по ссылкам ниже. + +|интерфейс прикладного программирования|Описание|Автор|Открыть| +| --- | --- | --- | --- | +|**API администратора**|XUI.ONE-совместимое администрирование — линии, пользователи, потоки, VOD, серии, серверы, настройки (104 конечные точки)|`api_key` + код доступа|[Открыть ↗](../../_media/swagger-ui.html?spec=admin)| +|**Системный API**|Внутренний `/api.php` — поток/VOD управление, статистика, процессы, файлы, подключения (31 действие)|`password` (`live_streaming_pass`)|[Открыть ↗](../../_media/swagger-ui.html?spec=system)| +|**API для игроков**|Проигрыватель XtreamCodes — Прямой эфир, VOD, Серия, EPG|`username` + `password`|[Открыть ↗](../../_media/swagger-ui.html?spec=player)| +|**API списка воспроизведения**|`/playlist` аутентификация + создание списка воспроизведения|`username`/`password` или `token`|[Открыть ↗](../../_media/swagger-ui.html?spec=playlist)| + +--- + +## Используя "Попробуй это" + +1. Откройте спецификацию, затем используйте вкладки **API** для переключения API и вкладки **Документация / Интерактивные (Swagger)** для каждого API. +2. Разверните любую конечную точку → ** Попробуйте ** → заполните параметры → ** Выполните**, чтобы увидеть реальный URL запроса, команду cURL и оперативный ответ. +3. Чтобы получить доступ к API администратора, нажмите "Авторизоваться" и вставьте свой API—ключ - затем он автоматически прикрепляется к каждому запросу. + +> ** Примечание CORS:** CORS может заблокировать прямые вызовы "Попробуйте" из браузера на ваш сервер. Это ожидаемо — вместо этого используйте сгенерированную команду cURL, Postman или Insomnia. Ошибка не означает, что API не работает. + +--- + +## Исходные технические характеристики + +Каждая спецификация может быть импортирована в Postman, Insomnia или любой другой инструментарий OpenAPI 3.0: + +- [`admin-api.openapi.yaml`](../../_media/admin-api.openapi.yaml) +- [`system-api.openapi.yaml`](../../_media/system-api.openapi.yaml) +- [`player-api.openapi.yaml`](../../_media/player-api.openapi.yaml) +- [`playlist-api.openapi.yaml`](../../_media/playlist-api.openapi.yaml) diff --git a/docs/ru/builds/build_system.md b/docs/ru/builds/build_system.md new file mode 100644 index 00000000..732770cc --- /dev/null +++ b/docs/ru/builds/build_system.md @@ -0,0 +1,275 @@ +# XC_VM Система сборки (ОСНОВНАЯ против LB) + +Как XC_VM создает два варианта сборки из одной кодовой базы: полноценный основной сервер и облегченный сервер балансировки нагрузки (LB). + +--- + +## Варианты сборки + +XC_VM поддерживает две роли развертывания из одного дерева исходных текстов: + +|Вариант|Архив|Цель| +| --- | --- | --- | +|**ОСНОВНОЙ**| `xc_vm.tar.gz` |Полное приложение — админ-панель, потоковое вещание, все модули, задания cron| +|**Фунт** (балансировщик нагрузки)| `loadbalancer.tar.gz` |Сервер только для потоковой передачи — нет панели администратора, нет управления пользователями| + +**MAIN** - это основной сервер, который управляет всем: пользовательским интерфейсом администратора, записями в базу данных, управлением пользователями/устройствами, обработкой EPG, резервным копированием и т.д. + +**LB** - это облегченный потоковый узел, который получает потоки из MAIN (или других источников) и доставляет их клиентам. Он подключается к базе данных master в режиме только для чтения и не имеет панели администратора или возможностей управления. + +--- + +## Целевые объекты Makefile + +|Цель|Выход|Описание| +| --- | --- | --- | +| `make main` | `dist/xc_vm.tar.gz` |Полная ОСНОВНАЯ сборка| +| `make lb` | `dist/loadbalancer.tar.gz` |Сборка LB (подмножество только для потоковой передачи)| +| `make main_update` | `dist/update.tar.gz` |Постепенное ОСНОВНОЕ обновление| +| `make lb_update` | `dist/loadbalancer_update.tar.gz` |Постепенное обновление LB| +| `make new` |Обе полные сборки|Короткий путь: `main` + `lb`| + +Дополнительные выходы: + +- `XC_VM.zip` — установочный пакет (`install/` + `xc_vm.tar.gz`) +- `hashes.md5` — Контрольные суммы MD5 для проверки целостности + +--- + +## Composer Зависимости + +`src/vendor/` (автозагрузчик Composer PSR-4 плюс производственные зависимости) - это +**зафиксировано** и отправлено как есть - путь развертывания не содержит Composer и никогда не выполняется +`composer install`. Он поддерживается только в рабочем состоянии через `composer install --no-dev`, так что +оба варианта сборки предназначены для бережливого производства без инструментов разработки. + +- `src/composer.lock` фиксируется, поэтому `composer install` является воспроизводимым. +- Инструменты разработки (PHPStan, PHP-CS-Fixer) являются `require-dev` и ** отсутствуют** в +зарегистрированный поставщик или архивы. Разработчики и CI добавляют их с помощью `make dev-tools` +(`composer install`); шлюз `check-vendor-prod-only` завершается сбоем, если пакет разработчика +когда-либо совершались под `src/vendor/`. +- Нет шага поставщика во время сборки — `make main` / `make lb` скопируйте зафиксированный +`vendor/` непосредственно в архив. + +--- + +## Что входит в Каждую Сборку + +### ОСНОВНАЯ сборка + +ОСНОВНАЯ сборка содержит **весь ** каталог `src/`. + +### Каталоги— включенные в сборку LB + +Только эти каталоги копируются в архив LB: + +```text +bin/ Cli/ config/ content/ Core/ +Domain/ Infrastructure/ public/ resources/ +signals/ Streaming/ tmp/ www/ +``` + +Плюс корневые файлы: `bootstrap.php`, `console.php`, `service`, `update`. + +### Содержимое— исключенное из сборки LB + +После копирования содержимое, относящееся к администратору, **удаляется** из сборки LB: + +**Удаленные каталоги:** + +|Путь|Причина| +| --- | --- | +| `bin/install/` |Установочные скрипты (в LB они не нужны)| +| `bin/redis/` |Redis двоичный файл (LB не запускает свой собственный Redis)| +| `bin/nginx/conf/codes/` |Страницы с кодами ошибок (пользовательский интерфейс администратора)| +| `Public/Controllers/Admin/` |Контроллеры панели администратора| +| `Public/Controllers/Player/` |Контроллеры панели проигрывателя| +| `Public/Controllers/Reseller/` |Контроллеры панели реселлера| +| `Public/Views/` |Шаблоны панелей| +| `Public/assets/` |Панель статических активов| +| `Public/routes/` |Карты маршрутов на панели| +| `Domain/User/` |Управление пользователями| +| `Domain/Device/` |Регистрация устройства| +| `Domain/Auth/` |Управление авторизацией (panel auth)| +| `resources/langs/` |Файлы языковых ресурсов| +| `resources/libs/` |Администрирование библиотечных ресурсов| + +**Удаленные файлы:** + +|Файл|Причина| +| --- | --- | +| `Public/Controllers/Api/AdminApiController.php` |Полный admin API удален из LB| +| `Public/Controllers/Api/ResellerRestApiController.php` |API реселлера удален из LB| +| `Infrastructure/legacy/reseller_api.php` |Устаревший bootstrap API реселлера не нужен в LB| +|`www/xplugin.php`, `www/probe.php`, `www/playlist.php`|Конечные точки администрирования| +|`www/player_api.php`, `www/epg.php`, `www/enigma2.php`|Конечные точки клиентского API (обслуживаемые MAIN)| +| `www/stream/auth.php` |Конечная точка аутентификации удалена из пакета LB| +|`www/admin/api.php`, `www/admin/proxy_api.php`|API администратора| +| `bin/maxmind/GeoLite2-City.mmdb` |GeoIP DB поставляется отдельно| +| `config/rclone.conf` |Конфигурация резервного копирования| +| `Domain/Epg/EPG.php` |EPG класс обработки| +| `bin/nginx/conf/gzip.conf` |Конфигурация Gzip (LB использует собственную)| + +**Команды CLI удалены:** + +|Файл|Причина| +| --- | --- | +| `Cli/Commands/MigrateCommand.php` |Миграция является ОСНОВНОЙ| +| `Cli/Commands/CacheHandlerCommand.php` |Обработчик кэша доступен только для MAIN| +| `Cli/Commands/ServerInstallCommand.php` |Установщик сервера (не требуется для самой LB)| +| `Cli/Commands/LbInstallFlow.php` |Помощник по установке LB (не требуется для самого LB)| +| `Cli/Commands/ProxyInstallFlow.php` |Помощник по установке прокси-сервера (не требуется для самой LB)| + +**Удалены задания Cron:** + +|Файл|Причина| +| --- | --- | +| `Cli/CronJobs/RootMysqlCronJob.php` |Обслуживание базы данных (только для ОСНОВНОЙ системы)| +| `Cli/CronJobs/BackupsCronJob.php` |Резервные копии (только для ОСНОВНОЙ системы)| +| `Cli/CronJobs/CacheEngineCronJob.php` |Полная перестройка кэша (только для основного)| +| `Cli/CronJobs/EpgCronJob.php` |EPG обработка (только для ОСНОВНОЙ системы)| +| `Cli/CronJobs/UpdateCronJob.php` |Проверка обновлений (только для основной системы)| +| `Cli/CronJobs/ProvidersCronJob.php` |Синхронизация с поставщиком (только для ОСНОВНОГО)| +| `Cli/CronJobs/SeriesCronJob.php` |Метаданные серии (только для основной версии)| + +> ** Примечание:** Связанные с модулем crons (TMDB, Plex, Watch) теперь находятся внутри `modules//` и автоматически исключаются из сборок LB, поскольку `modules/` отсутствует в `LB_DIRS`. + +### Конфигурации, замененные при сборке LB + +Эти файлы из `lb_configs/` ** заменяют** основные версии: + +|Источник|Цель|Цель| +| --- | --- | --- | +| `lb_configs/nginx.conf` | `bin/nginx/conf/nginx.conf` |Настроенная производительность nginx для потоковой передачи| +| `lb_configs/live.conf` | `bin/nginx_rtmp/conf/live.conf` |RTMP перехваты обратного вызова| + +--- + +## ОСНОВНЫЕ отличия от LB — Key Differences + +|Аспект|главный|фунт| +| --- | --- | --- | +|Панель администратора|✅ Полный пользовательский интерфейс|❌ Не входит в комплект поставки| +|Роль базы данных|Чтение + запись|Пользователь, доступный только для чтения| +|Управление пользователями/устройствами|✅|❌| +|EPG обработка|✅|❌| +|Резервные копии|✅|❌| +|Инструмент для миграции|✅|❌| +|Потоковая доставка|✅|✅| +|RTMP прием внутрь|✅|✅| +|Транскодирование (FFmpeg)|✅|✅| +|Команды CLI|26|~15 (удалено только для администратора)| +|Задания Cron|25|~16 (удалено только для администратора)| +|Модульная система|✅|❌| + +--- + +## Конфигурация LB Nginx + +В сборке LB используется специализированная конфигурация nginx, оптимизированная для потоковой передачи с высокой пропускной способностью: + +|Установка|Ценность|Цель| +| --- | --- | --- | +|Рабочие процессы| `auto` |Масштабирование до ядер центрального процессора| +|Рабочие связи|16,000|Высокая параллельность на одного работника| +|Максимальное количество файловых дескрипторов|300,000|Ограничение системных ресурсов| +|Пул потоков|`pool_xc_vm` (32 потока)|Асинхронный ввод-вывод для потоковой передачи| +|Gzip-файл|прочь|Потоковые данные уже сжаты| +|Журналы доступа|прочь|Сократите накладные расходы на ввод-вывод| +|Ограничение скорости|20 запросов в секунду на IP-адрес|Смягчение последствий DDoS-атак| +|Тайм-аут отправки|20 мин|Поддержка длительных потоков| + +RTMP перехватывает (`lb_configs/live.conf`) маршрутизацию аутентификации через локальные HTTP-обратные вызовы вместо панели администратора: + +```nginx +on_play http://127.0.0.1:8080/stream/rtmp; +on_publish http://127.0.0.1:8080/stream/rtmp; +on_play_done http://127.0.0.1:8080/stream/rtmp; +``` + +--- + +## Поведение во время выполнения на LB + +### Загрузка условной команды + +`console.php` использует `file_exists()` guards для команд, которые могут отсутствовать на серверах LB: + +```php +if (file_exists(__DIR__ . '/Cli/Commands/CacheHandlerCommand.php')) { + $rRegistry->register(new CacheHandlerCommand()); +} +``` + +Это предотвращает сбои при попытке LB зарегистрировать команду, файл которой был удален во время сборки. + +### Цепочка потоковых зависимостей + +Серверы LB сохраняют полный конвейер потоковой передачи: + +```text +www/stream/*.php + ├── www/stream/init.php + ├── vendor/autoload.php (Composer PSR-4 autoloader) + ├── bootstrap.php (lightweight stream/bootstrap path) + ├── Core/* (Config, Database, Cache, Auth, Http, Logging, Util) + ├── Domain/Stream, Domain/Server, Domain/Vod, Domain/Bouquet + ├── Streaming/* (Auth, Delivery, Codec, Protection) + ├── Infrastructure/Redis, Infrastructure/Database + └── resources/data +``` + +--- + +## Добавление нового кода в сборки + +### Новый каталог, относящийся к потоковой передаче, в разделе `src/` + +Добавьте его в `LB_DIRS` в Makefile: + +```makefile +LB_DIRS = bin cli config content core domain ... your_dir +``` + +### Новый каталог, доступный только для администратора + +Добавьте его в `LB_DIRS_TO_REMOVE`: + +```makefile +LB_DIRS_TO_REMOVE = ... your_dir/admin_stuff +``` + +### Новый файл, доступный только для администратора + +Добавьте его в `LB_FILES_TO_REMOVE`: + +```makefile +LB_FILES_TO_REMOVE = ... your_dir/admin_file.php +``` + +### Новая команда CLI (только для администратора) + +1. Добавить `file_exists()` guard в `console.php` +2. Добавьте файл в `LB_FILES_TO_REMOVE` + +--- + +## Проверка сборки + +После изменения сборки проверьте оба варианта: + +```bash +# Build both +make new + +# Check LB contains streaming code +tar -tzf dist/loadbalancer.tar.gz | grep -cE "Core/|Domain/Stream|Streaming/" +# Expected: > 0 + +# Check LB does NOT contain admin code +tar -tzf dist/loadbalancer.tar.gz | grep -cE "admin/|player/|ministra|reseller" +# Expected: 0 + +# Compare sizes (LB should be significantly smaller) +ls -lh dist/xc_vm.tar.gz dist/loadbalancer.tar.gz +``` diff --git a/docs/ru/builds/updates_checklist.md b/docs/ru/builds/updates_checklist.md new file mode 100644 index 00000000..60f103b3 --- /dev/null +++ b/docs/ru/builds/updates_checklist.md @@ -0,0 +1,251 @@ +# XC_VM Контрольный список подготовки к выпуску + +Пошаговое руководство по подготовке и публикации релиза XC_VM. + +--- + +## 1. Журнал изменений + +**Создание журнала фиксации (только рабочие фиксации):** + +```bash +PREV_TAG=$(git describe --tags --abbrev=0) +mkdir -p dist +git log --pretty=format:"- %s (%h)" "$PREV_TAG"..main > dist/changes.md +``` + +> ❗️ требуется `mkdir -p dist`: `dist/` не существует в новом клоне, и `make new` стирает его — без этого перенаправление завершается ошибкой с сообщением "Нет такого файла или каталога". + +**Обновление `changelog.json`** в корневом каталоге репозитория — этот файл содержит только изменения для предстоящего выпуска: + +```json +{ + "version": "X.Y.Z", + "changes": [ + "Description of change 1", + "Description of change 2" + ] +} +``` + +Панель автоматически извлекает этот файл из тега release через `GithubReleases::getChangelog()`. + +> 💬 Делайте описания краткими — сосредоточьтесь на улучшениях и исправлениях, с которыми сталкиваются пользователи. + +--- + +## 2. Подготовить базовый уровень выпуска + +Сначала завершите всю работу с функциями / исправлениями / документами и убедитесь, что она уже находится в `main`. + +Установите переменную version один раз и повторно используйте ее во всех приведенных ниже командах: + +```bash +VERSION="X.Y.Z" +``` + +> ❗️ Не создавайте отдельную версию-измените фиксацию/push на этом шаге. +> В противном случае `dist/changes.md` будет включать дополнительные фиксации релиза и принудительно вносить дополнительные правки. + +### Восстановление переведенной документации + +Документация написана только на **английском языке** (`docs/en`). Русское дерево +(`docs/ru`) - это ** сгенерированный, зафиксированный** артефакт, обновляемый локально перед каждым +release — перевод намеренно **не** выполняется в CI (он медленный); только CI +создает зафиксированное дерево. Если `docs/en` изменено с момента последнего выпуска: + +```bash +make docs-translate # regenerate docs/ru from docs/en (free, no API key) +make docs-build # strict build — fails on any broken link/anchor +``` + +- `make docs-translate` повторно переводит только те файлы на английском языке, содержимое которых +изменен (для каждого файлового кэша), так что при постепенном выпуске это происходит быстро. +- **Просмотрите и зафиксируйте восстановленный `docs/ru`** — он включен в единый +снимите фиксацию (шаг 5). Никогда не редактируйте `docs/ru` вручную. +- Нажатие кнопки изменения документов запускает `pages.yml`, которая создает и публикует +страницы сайта на GitHub. + +--- + +## 3. Удаленные файлы + +Перед сборкой сгенерируйте список файлов для удаления при обновлении: + +```bash +make generate_deleted_files +``` + +Это запускает `git diff` между `LAST_TAG` и `HEAD`@, извлекает удаленные файлы под `src/`, удаляет префикс `src/` и записывает результат в `src/migrations/deleted_files.txt`. + +Если `LAST_TAG` не может быть автоматически обнаружен (нет сети / нет выпусков), передайте его явно: + +```bash +make generate_deleted_files LAST_TAG=1.2.16 +``` + +**Просмотрите созданный файл ** — убедитесь, что по ошибке в списке нет важных файлов: + +```bash +cat src/migrations/deleted_files.txt +``` + +После проверки, `make main` / `make lb` файл будет помещен в архив с помощью `delete_files_list` / `lb_delete_files_list`. + +Во время `php console.php update post-update`, `MigrationRunner::runFileCleanup()` считывает его и автоматически удаляет перечисленные файлы. + +> ⚠️ Строки, начинающиеся с `#`, являются комментариями и будут проигнорированы. Вы можете закомментировать файлы, которые хотите сохранить. + +--- + +## 4. Предварительная проверка + +Перед публикацией проверьте, работает ли сборка: + +**Проверка качества ** (CI использует тот же набор, что и на бирке — убедитесь, что он зеленый): + +```bash +make dev-tools && make phpstan && make cs && make gates +php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist +make dev-clean # remove the dev tools afterwards, restoring the prod-only vendor/ +``` + +> ➡ ️ Тестовая установка Docker перенесена на шаг 6 — для этого требуется встроенный `dist/XC_VM.zip`. + +** Проверка безопасности:** выполняется автоматически при нажатии / PR через `.github/workflows/security-scan.yml` (Semgrep) — никаких действий вручную. + +--- + +## 5. Обновите версию и создайте единую фиксацию выпуска + +Отредактируйте константу версии, отключите флаг доступа phpMiniAdmin и снимите пароль в: + +```text +src/Core/Config/AppConfig.php +``` + +**Быстрые команды:** + +```bash +sed -i "s/define('DB_ACCESS_ENABLED', true);/define('DB_ACCESS_ENABLED', false);/" src/Core/Config/AppConfig.php +sed -i "s/define('DB_ACCESS_PWD', *\"[^\"]*\");/define('DB_ACCESS_PWD', \"\");/" src/Core/Config/AppConfig.php +sed -i "s/define('XC_VM_VERSION', *'[0-9]\+\.[0-9]\+\.[0-9]\+');/define('XC_VM_VERSION', '${VERSION}');/" src/Core/Config/AppConfig.php +``` + +**Создайте одну окончательную фиксацию/толчок выпуска:** + +```bash +git add src/Core/Config/AppConfig.php changelog.json src/migrations/deleted_files.txt +git add docs/en docs/ru # include any doc edits + the regenerated ru (step 2) +git commit -m "Prepare release ${VERSION}" +git push +``` + +> ❗️ Это устраняет необходимость в многократных фиксациях релиза. + +--- + +## 6. Создавать архивы + +> 🤖 **Рабочие сборки** обрабатываются с помощью GitHub Actions (`.github/workflows/build-release.yml`) при публикации релиза. Ресурсы добавляются автоматически. + +**Для локальных сборок:** + +```bash +make new +make lb +make main +``` + +После построения `dist/` должен содержать: + +|Файл|Описание| +| --- | --- | +| `XC_VM.zip` |ОСНОВНОЙ установщик (установить скрипт + xc_vm.tar.gz)| +| `xc_vm.tar.gz` |ОСНОВНОЙ архив (установка и обновление)| +| `loadbalancer.tar.gz` |Архив LB (установка и обновление)| +| `hashes.md5` |Контрольные суммы MD5| + +> Один и тот же архив используется как для чистой установки, так и для обновлений. +> Скрипт обновления (`src/update`) отфильтровывает двоичные каталоги/каталоги конфигурации во время выполнения, используя жестко заданный список `UPDATE_EXCLUDE_DIRS` внутри самого скрипта Python. + +**Проверка целостности:** + +```bash +cd dist && md5sum -c hashes.md5 +``` + +**Тестовая установка Docker** (смотрите `tools/test-install/`) — только после сборки, так как для этого требуется `dist/XC_VM.zip`: + +```bash +bash tools/test-install/test_release.sh +``` + +При этом создается образ, контейнер запускается с помощью systemd и программа установки запускается автоматически. +`dist/XC_VM.zip` монтируется в контейнер как том, доступный только для чтения. + +> ✅ Убедитесь, что панель загружается с `http://localhost:8880` и логин администратора работает. + +--- + +## 7. Релиз на GitHub + +1. Перейти к [Релизам на GitHub](https://github.com/Vateron-Media/XC_VM/releases) +2. Создайте новый релиз с тегом, указанным на первом шаге +3. Вставьте список изменений в качестве описания выпуска +4. Публиковать ** без прикрепления файлов ** — действия GitHub создадут и прикрепят их + +После публикации рабочий процесс будет автоматически запущен: + +- Собрать все архивы + контрольные суммы +- Прикрепите их к фиксатору +- Отправьте уведомление в Telegram через `release-notifier.yml` + +> ✅ Дождитесь завершения рабочего процесса действий, затем убедитесь, что все файлы доступны для загрузки. + +--- + +## 8. После выпуска + +- [ ] Убедитесь, что все 4 ресурса присоединены к релизу +- [ ] Запустите `md5sum -c hashes.md5` для загруженных файлов +- [ ] Проверьте, отправлено ли уведомление Telegram +- [ ] Тесно связанные с GitHub проблемы / вехи + +--- + +## Ссылка на команду + +Все целевые объекты `make`, использованные во время подготовки релиза, в одном месте. + +**Проверка качества ** — сначала запустите `make dev-tools`, после завершения - `make dev-clean`: + +|Команда|Цель| +| --- | --- | +| `make dev-tools` |Установите инструменты для разработки (PHPStan, PHP-CS-Fixer) через `composer install`| +| `make phpstan` |Статический анализ (также выявляет синтаксические ошибки)| +| `make phpstan-baseline` |Восстановите базовую линию PHPStan| +| `make cs` |Проверка стиля кода - импорт/гигиена пространства имен (PHP—CS-Fixer, пробный запуск)| +| `make cs-fix` |Примените исправления в стиле кода на месте| +| `make gates` |PSR-4 регрессионные шлюзы (для процедурного использования, LB-архив, только для продукта поставщика)| +| `make dev-clean` |Снова удалите инструменты разработки, восстановив рабочий режим только для `vendor/`| +| `php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist` |Модульные тесты| + +**Подготовка к выпуску и сборка:** + +|Команда|Цель| +| --- | --- | +| `make generate_deleted_files` |Регенерировать `src/migrations/deleted_files.txt`| +| `make new` |Сбросьте выходной каталог `dist/` (запустите перед сборкой)| +| `make lb` |Создайте архив LoadBalancer в `dist/`| +| `make main` |Создайте ОСНОВНОЙ архив в `dist/`| +| `bash tools/test-install/test_release.sh` |Установочный тест Docker для встроенного выпуска| + +**Документация** (англоязычный источник в `docs/en`; `docs/ru` сгенерирован + зафиксирован): + +|Команда|Цель| +| --- | --- | +| `make docs-venv` |Одноразовый: локальный venv (сборка + переводы)| +| `make docs-translate` |Восстановить `docs/ru` из `docs/en` (перед выпуском)| +| `make docs-build` |Строгая MkDocs сборка в `./site` (что запускает CI)| +| `make docs-serve` |Предварительный просмотр документов в режиме реального времени на странице `http://127.0.0.1:8000`| diff --git a/docs/ru/development/architecture.md b/docs/ru/development/architecture.md new file mode 100644 index 00000000..37834559 --- /dev/null +++ b/docs/ru/development/architecture.md @@ -0,0 +1,113 @@ +# Обзор архитектуры + +## Тип проекта + +Структурированный монолит PHP с модульным дополнительным слоем. + +- Никакого DDD, никакой гексагональности, никакой чистой архитектуры — намеренно. +- Разбивка по контексту с минимальными абстракциями: `Controller → Service → Repository`. +- Два артефакта сборки из одной кодовой базы: **MAIN** (полная панель) и **LB** (load balancer subset). + +--- + +## Дерево исходных текстов + +|Путь|Роль| +| ---- | ---- | +| `src/Core/` |Примитивы инфраструктуры: Контейнер DI, события, HTTP, настройка, аутентификация, ведение журнала| +| `src/Domain/` |Бизнес-контексты: Поток, VOD, линия, пользователь, Сервер, безопасность и т.д.| +| `src/Modules/` |Дополнительный слой расширения — загружается с помощью `ModuleLoader`| +| `src/Public/` |Передний контроллер, маршрутизатор, контроллеры, представления, ресурсы| +| `src/Cli/` |Консольные команды и точки входа в cron| +| `src/Ministra/` |Портал Сталкера — в ядре; обслуживается по адресу `/home/xc_vm/Ministra`| + +--- + +## Модель времени выполнения + +Зависимости перетекают друг в друга — модули могут использовать ядро и домен, но никогда наоборот. + +``` +Public/index.php + └── XC_Bootstrap::boot(BootContext::ADMIN) + └── ServiceContainer (DI) + ├── EventDispatcher (PSR-14) + ├── ModuleLoader → loadAll() → bootAll() + └── Router → dispatch() +``` + +Классы домена получают базу данных через `setDb()` injection (вызывается из +`bootstrap.php::wireDomainDatabase()`). В пути веб-запроса нет `global $db`. + +--- + +## Модульная система + +Модули представляют собой изолированные каталоги под `src/Modules/` с манифестом `module.json` +и класс, расширяющий `BaseModule`. Смотрите [Модульная система](modules.md) для получения полной информации. + +``` +src/Modules/my-module/ +├── module.json # metadata +├── MyModuleModule.php # extends BaseModule, namespace XcVm\Module\MyModule +└── ... +``` + +--- + +## Контексты начальной загрузки + +Четыре контекста определяют, какие подсистемы инициализируются. Смотрите [Контексты начальной загрузки](bootstrap-contexts.md). + +|Контекст|Используется для| +| ------- | -------- | +| `BootContext::MINIMAL` |Скрипты, которым нужны только пути / конфигурация| +| `BootContext::CLI` |Задания Cron и команды CLI| +| `BootContext::STREAM` |Конечные точки потоковой передачи| +| `BootContext::ADMIN` |Панель администратора/реселлера| + +--- + +## Варианты сборки (ОСНОВНАЯ и LB) + +| |главный|фунт| +| --- | ---- | -- | +|Панель администратора|✅|❌| +|Потоковый|✅|✅| +|Модульная система|✅|подмножество| + +Управляется `ServerEnvironment` перечислением и `module.json` `environment` полем (`main` / `lb` / `any`). + +--- + +## Ключевые моменты расширения + +|Механизм|Как использовать| +| --------- | ---------- | +|События PSR-14|@атрибут @0@@ или `#[ListensTo]`@| +|Оформление сервиса| `$container->decorate('id', callable, priority)` | +|Потоковое промежуточное программное обеспечение|Реализовать `StreamMiddlewareProviderInterface`| +|Записи Cron|Переопределить `getCronEntries()` в классе модуля| +|Миграции баз данных|Реализовать `MigratableInterface::getMigrations()`| + +--- + +## Правила для участников + +1. Модули не должны изменять основные файлы. +2. Никаких `eval`, исправлений monkey или замены файлов во время выполнения. +3. Любой модуль можно отключить с помощью `config/modules.php`, не прикасаясь к ядру. +4. Защищенные сервисы (`db`, `settings`, `config`, `auth`) не могут быть оформлены. +5. Синхронизируйте документы EN и RU в одном и том же коммите. + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/Core/` |Примитивы фреймворка (DI, события, HTTP, config, auth, ведение журнала)| +| `src/Domain/` |Бизнес-контексты (Поток, VOD, Линия, Пользователь, Сервер, безопасность)| +| `src/Infrastructure/` |Внешние адаптеры (DatabaseFactory, CacheReader, Redis)| +| `src/Streaming/` |Подсистема потоковой передачи| +| `src/Modules/` |Дополнительные модули (загружаются с помощью ModuleLoader)| +| `src/Public/` |Передний контроллер, контроллеры, виды| +| `src/Cli/` |Консольные команды и задания cron| diff --git a/docs/ru/development/autoloader.md b/docs/ru/development/autoloader.md new file mode 100644 index 00000000..6e266e66 --- /dev/null +++ b/docs/ru/development/autoloader.md @@ -0,0 +1,112 @@ +# Автоматическая загрузка (PSR-4) + +XC_VM автоматически загружает классы с помощью стандартного **Composer PSR-4** автозагрузчика; пространство имен кодирует путь к файлу, поэтому разрешение выполняется напрямую `file_exists` без сканирования и кэширования. + +--- + +## Обзор + +Каждый сторонний класс находится в пространстве имен `XcVm\` root, сопоставленном с `src/`: + +```text +XcVm\Core\Auth\Authenticator -> src/Core/Auth/Authenticator.php +XcVm\Domain\Stream\StreamService -> src/Domain/Stream/StreamService.php +XcVm\Public\Controllers\Admin\UserController -> src/Public/Controllers/Admin/UserController.php +``` + +Сопоставление объявлено в `src/composer.json`: + +```json +"autoload": { + "psr-4": { + "XcVm\\": "./", + "M3uParser\\": "Core/Parsing/M3uParser/src/", + "Chrisyue\\PhpM3u8\\": "Core/Parsing/PhpM3u8/src/" + } +} +``` + +`src/vendor/` (зависимости Composer autoloader + production) зафиксированы и +отправлено — путь развертывания не содержит Composer и никогда не выполняется `composer install`. Там +нет ли ** кэша карты классов ** (нет `optimize-autoloader`): пропущенный класс - это простой путь +поиск, а не повторное сканирование каталога. + +## Добавление нового класса + +Создайте файл по пути, соответствующему его пространству имен, — вот и все; Composer разрешает его по требованию: + +```php +// src/Domain/Billing/InvoiceService.php +namespace XcVm\Domain\Billing; + +class InvoiceService { + public static function generate(int $userId): string { /* ... */ } +} +``` + +Ссылайтесь на него из другого кода в пространстве имен с помощью `use` import или по его полному номеру: + +```php +use XcVm\Domain\Billing\InvoiceService; +``` + +Не нужно очищать кэш, редактировать реестр. Совершенно новое подпространство имен (например, +`XcVm\Domain\Billing`) срабатывает немедленно, потому что он отображается прямо на +каталог. + +## Правила присвоения имен + +|Правило|Пример| +| --- | --- | +|Имя файла **должно ** совпадать с именем класса|`InvoiceService.php` → `class InvoiceService`| +|Один класс на файл|PSR-4 разрешает один класс для каждого пути; разбивает файлы на несколько классов| +|Пространство имен **должно** совпадать с путем к каталогу (с учетом регистра)|`src/Domain/Billing/` → `namespace XcVm\Domain\Billing;`| +|Классы и каталоги PascalCase|`StreamService`, `DatabaseHandler`, `Core/Auth/`| +|Соглашение о проекте: нет `declare(strict_types=1)`|—| + +Поскольку пространство имен содержит местоположение, дублируйте короткие имена в разных +пространства имен больше не конфликтуют — `XcVm\Public\Controllers\Admin\PlexController` и +`XcVm\Module\Plex\PlexController` различны. + +## Процедурные файлы и файлы третьих лиц + +Некоторые файлы намеренно **не** разделены пространством имен и загружаются явным образом. +`require`, а не автозагрузчик: + +- процедурные точки входа, представления и загрузочный клей (например, `Public/index.php`, +`Public/Views/**`, `Infrastructure/Bootstrap/*.php`); +- глобальные константы и функции (`Core/Config/*`, обработчик ошибок); +- класс ioncube `XC_VM` и комплектный `Infrastructure/Tmdb/lib/*`. + +У продаваемых пакетов `M3uParser` и `Chrisyue\PhpM3u8` есть свои собственные пакеты PSR-4 +префиксы (указанные выше) и автозагрузка выполняются в обычном режиме. + +## Модули + +Классы модулей используют пространство имен `XcVm\Module\\…`, но **не** зарегистрированы +в `composer.json` (каталогах модулей/торговых площадок — `plex`, `watch-d2bho` — +не соответствуют ни одному правилу PSR-4). Они разрешаются с помощью `ModuleLoader` собственного PSR-4 +распознаватель: он удаляет базовое пространство имен модуля и отображает оставшееся в +вложенный путь в каталоге модуля. Смотрите [Модульная система](modules.md). + +## Инструменты для разработки + +Исправленный `vendor/` предназначен только для рабочей среды. PHPStan и PHP-CS-Fixer являются +`require-dev` пакетов — установите их локально с помощью: + +```bash +make dev-tools # = cd src && composer install +``` + +Они никогда не фиксируются (CI-шлюз обеспечивает фиксацию поставщика только для продукта). Видеть +[Рабочий процесс разработки](../guides/dev-workflow.md). + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/composer.json` |PSR-4 префиксная карта + зависимости| +| `src/composer.lock` |зафиксированная блокировка для воспроизводимого `composer install`| +| `src/vendor/` |зафиксировано Composer автозагрузчик + производственные потери| +| `src/bootstrap.php` |определяет `MAIN_HOME`, требует `vendor/autoload.php`| +| `src/Core/Module/ModuleLoader.php` |PSR-4 распознаватель для классов модулей| diff --git a/docs/ru/development/bootstrap-contexts.md b/docs/ru/development/bootstrap-contexts.md new file mode 100644 index 00000000..7d5fe4c5 --- /dev/null +++ b/docs/ru/development/bootstrap-contexts.md @@ -0,0 +1,170 @@ +# Контексты начальной загрузки + +`XC_Bootstrap` - это единая точка входа для инициализации системы. +Каждый контекст загружает только те подсистемы, которые необходимы для его пути выполнения. +Контекст выражается в виде значения `BootContext` enum. + +--- + +## Краткий справочник + +|Случай перечисления|Типичное использование| +| --- | --- | +| `BootContext::MINIMAL` |Скрипты, которым нужны только пути /конфигурация| +| `BootContext::CLI` |Задания Cron и команды CLI| +| `BootContext::STREAM` |Конечные точки потоковой передачи (`live`, `vod`, `timeshift`)| +| `BootContext::ADMIN` |Панель администратора/реселлера| + +--- + +## Детали контекста + +### BootContext::МИНИМАЛЬНЫЙ + +Загружает константы, пути, конфигурацию, логгер и обработчики ошибок. +Нет подключения к базе данных. + +Включает в себя: + +- Composer PSR-4 автозагрузчик (`vendor/autoload.php`) +- константы пути (`MAIN_HOME`, `INCLUDES_PATH`, ...) +- регистратор (`Logger::init()`) +- помощники по устранению ошибок (`generateError()`, `generate404()`) + +Не включает: базу данных, Redis, сеансы, переводчик, API-интерфейсы администратора. + +```php +require_once '/home/xc_vm/bootstrap.php'; +XC_Bootstrap::boot(BootContext::MINIMAL); +``` + +--- + +### Загрузочный текст::CLI + +Используется для задач cron и CLI. +Добавляет инициализацию базы данных и устаревшего ядра через `MINIMAL`. + +Включает в себя: + +- Подключение к базе данных +- `LegacyInitializer` +- необязательный Redis (`'redis' => true`) +- необязательный заголовок процесса (`'process' => '...'`) + +```php +require_once '/home/xc_vm/bootstrap.php'; +XC_Bootstrap::boot(BootContext::CLI, [ + 'cached' => true, + 'process' => 'xc_vm: my-job', +]); +``` + +--- + +### BootContext::ПОТОК + +Облегченный контекст для конечных точек потоковой передачи с высокой нагрузкой. + +Включает в себя: + +- Подключение к базе данных (`cached=true`) +- защита от наводнений и проверка хостинга + +Не включает: Redis, переводчик, API-интерфейсы администратора, сеансы. + +```php +require_once '/home/xc_vm/bootstrap.php'; +XC_Bootstrap::boot(BootContext::STREAM, ['cached' => true]); +``` + +--- + +### BootContext::АДМИНИСТРАТОР + +Полная инициализация панели администратора/реселлера. + +Включает в себя: + +- защищенный сеанс (`SameSite=Strict`) +- Подключение к базе данных (`cached=false`) +- `LegacyInitializer` +- Redis +- API-интерфейсы администратора/реселлера +- переводчик +- обработчик завершения работы +- постоянные состояния и глобальные параметры администратора + +```php +require_once '/home/xc_vm/bootstrap.php'; +XC_Bootstrap::boot(BootContext::ADMIN); +``` + +--- + +## Матрица подсистемы + +|Подсистема|минимальный|КЛИ|течение|администратор| +| --- | :---: | :---: | :---: | :---: | +|Константы/пути|✅|✅|✅|✅| +|Лесоруб|✅|✅|✅|✅| +|Защита от наводнений|—|—|✅|✅| +|Проверка хостинга|—|—|✅|✅| +|База данных|—|✅|✅|✅| +|Инициализатор наследия|—|✅|—|✅| +| Redis |—|выбирать|—|✅| +|Сессия|—|—|—|✅| +|API администратора|—|—|—|✅| +|Переводчик|—|—|—|✅| + +--- + +## `boot()` параметры + +```php +XC_Bootstrap::boot(BootContext $context, array $options = []); +``` + +|Вариант|Тип|По умолчанию|Описание| +| --- | --- | --- | --- | +| `cached` | `bool` |`true` для ПОТОКА, `false` в противном случае|Использовать кэшированные настройки| +| `redis` | `bool` |`true` для АДМИНИСТРАТОРА, `false` в противном случае|Подключение Redis| +| `process` | `string` | `''` |Название процесса для CLI| +| `shutdown` | `callable` |встроенный|Переопределить обратный вызов завершения работы| + +--- + +## Идемпотентность + +`boot()` выполняется один раз для каждого процесса. Повторные вызовы игнорируются. + +```php +XC_Bootstrap::boot(BootContext::ADMIN); +XC_Bootstrap::boot(BootContext::CLI); // ignored +``` + +Для проведения тестов: + +```php +XC_Bootstrap::reset(); +``` + +--- + +## Общедоступные методы + +```php +XC_Bootstrap::getContext(): ?BootContext +XC_Bootstrap::isBooted(): bool +XC_Bootstrap::isCli(): bool +XC_Bootstrap::getDatabase(): ?Database +XC_Bootstrap::getContainer(): ServiceContainer +``` + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/bootstrap.php` |Определяет MAIN_HOME, требует автозагрузки Composer, загружает контекст| +| `src/Core/Enum/BootContext.php` |Перечисление контекста загрузки| +| `src/Core/Init/LegacyInitializer.php` |Унаследованная инициализация для каждого контекста| diff --git a/docs/ru/development/caching-and-redis.md b/docs/ru/development/caching-and-redis.md new file mode 100644 index 00000000..1f19a35b --- /dev/null +++ b/docs/ru/development/caching-and-redis.md @@ -0,0 +1,296 @@ +# Кэширование и стратегия Redis + +XC_VM использует двухуровневую стратегию кэширования: + +- **Файловый кэш (igbinary)** - основной уровень, используемый как потоковыми, так и административными потоками. +- **Redis/KeyDB** — дополнительный высокопроизводительный уровень для определения состояния соединения и расширенных операций + +Путь потоковой передачи считывается исключительно из файлового кэша (никаких запросов к базе данных). +Путь администратора считывается из базы данных с дополнительным кратковременным кэшем. + +--- + +## Интерфейс кэширования + +Файл: `src/Core/Cache/CacheInterface.php` + +```php +get($key, $maxAge = null) +set($key, $data, $ttl = 0) +delete($key) +has($key, $maxAge = null) +flush() +``` + +- `$ttl = 0` означает кэширование навсегда (до ручного удаления или сброса). +- `$maxAge` проверяет время изменения файла на свежесть (только для файлового кэша). + +--- + +## Файловый кэш + +Файл: `src/Core/Cache/FileCache.php` + +Реализация кэша по умолчанию. Данные, сериализованные в двоичном формате, хранятся в виде плоских файлов. + +```php +$cache = new FileCache(CACHE_TMP_PATH); +$cache->set('my_key', $data, 3600); +$data = $cache->get('my_key', 120); // only if < 2 min old +``` + +Статический удобный API (обратная совместимость): + +```php +FileCache::setCache($key, $data) +FileCache::getCache($key, $maxAge = null) +``` + +Характеристики: + +- Сериализация: igbinary (если доступно) или резервный вариант PHP serialize. +- Блокировка: `LOCK_EX` при записи для предотвращения повреждения. +- Расположение файла: `{basePath}/{key}` (нет подкаталогов для основных ключей). +- Восстановление поврежденных файлов: обнаруживает поврежденные данные, автоматически удаляет поврежденные файлы. + +--- + +## Заново спрятать + +Файл: `src/Core/Cache/RedisCache.php` + +Дополнительная высокопроизводительная реализация. + +```php +$redis = new RedisCache('127.0.0.1', 6379, $password, 'prefix:'); +$redis->set($key, $data, 600); // 10-minute TTL via SETEX +$redis->getConnection(); // raw phpredis for sorted sets, pipelines +``` + +- Отложенное подключение: подключается при первой операции. +- Встроенная поддержка TTL через Redis `SETEX`. +- Используется в основном для `ConnectionTracker` (отсортированных наборов для текущего состояния соединения). + +--- + +## Redis Управление подключениями + +Файл: `src/Infrastructure/Redis/RedisManager.php` + +Жизненный цикл синглтона: + +```php +RedisManager::instance() // get active Redis or null +RedisManager::ensureConnected() // connect if not already +RedisManager::isConnected() // health check +RedisManager::closeInstance() // disconnect +``` + +Проверка работоспособности выдает сигнал Redis каждые 30 секунд (отключено). Автоматическое повторное подключение при сбое. Возвращает значение null при сбое соединения (постепенное ухудшение). + +Конфигурация: + +|Установка|Источник|По умолчанию| +| --- | --- | --- | +| `hostname` | `config.ini` |—| +| `port` |жестко запрограммированный| `6379` | +| `password` | `settings.redis_password` |—| +| `read_timeout` |жестко запрограммированный| `2.0s` | +| `tcp_keepalive` |жестко запрограммированный| `60s` | + +### Сохраняющиеся при отключении в режиме ожидания (долгоживущие демоны) + +Кратковременные запросы (PHP-FPM stream/admin) открывают новое соединение для каждого процесса +и на них не влияют тайм-ауты простоя. Демоны-долгожители — цикл `watchdog`, +`fanout_sync` — вместо этого удерживайте **одно** соединение через синглтон для их +весь срок службы, при котором возможны два режима сбоя на загруженном сервере или на межсерверном сервере +(LB → ГЛАВНАЯ) ссылка: + +- **Сервер простаивает -закрывается.** Redis закрывает любой клиент, который простаивает дольше своего `timeout` (`300s` +в комплекте `bin/redis/redis.conf`). затем phpredis прозрачно откроется снова. +сокет в следующей команде ** без повторного воспроизведения AUTH**, поэтому более поздняя команда +отвечает `NOAUTH` — или просто возвращает `false`. +- ** Устранен пробел в проверке работоспособности.** `instance()` пингуется только каждые 30 секунд, так что между +пингует, что сброшенное соединение еще не замечено. + +Охранники на месте: + +- `instance()` обрабатывает любой ответ, не связанный с`PONG` ping (беззвучное повторное подключение / `NOAUTH` +состояние) как отключенное соединение и вызывает полное, **повторно прошедшее проверку подлинности** повторное подключение +через `\XC_VM::redis_connect()` — не просто повторная попытка на уровне сокета. +- Вызывайте сайты, которые командами конвейера проверяют объект конвейера. Например +`ConnectionTracker::getCapacity()` проверяет, что `$redis->multi()` вернул +`\Redis` (сломанный сокет возвращает `false` и вызывает `zCard()` для этого bool +был бы фатальным вне пути повторного подключения) и выдает, чтобы его цикл повторных попыток снова подключился. + +Альтернативный вариант на стороне сервера (`timeout 0`) намеренно **не** используется — +вместо этого клиент становится устойчивым, и `tcp-keepalive` по-прежнему получает доступ к мертвым одноранговым узлам. + +--- + +## Заполнение кэша + +Файлы кэша генерируются двумя заданиями cron: + +### Облегченный кэш (CacheCronJob) + +Запускает каждый цикл cron. Восстанавливает быстро меняющиеся данные (~1 секунда): + +- `settings` — настройки панели +- `servers` — список серверов +- `bouquets` — пакеты каналов +- `categories` — категории потоков +- Блокирующие списки: `blocked_isp`, `blocked_ua`, `blocked_ips`, `blocked_servers` +- `allowed_ips`, `output_formats`, `hmac_keys`, `rtmp_ips` + +### Большой объем кэша (CacheEngineCronJob) + +Перестраивает потоковые, линейные и последовательные данные. Регулируется до одного раза в 5 минут с помощью `heavy_cache_built` marker: + +- `STREAMS_TMP_PATH/stream_{id}` — метаданные отдельного потока +- `LINES_TMP_PATH/line_i_{user_id}` — данные учетной записи пользователя +- `LINES_TMP_PATH/line_c_{username_password}` — имя пользователя → поиск идентификатора пользователя +- `LINES_TMP_PATH/line_t_{access_token}` — токен → поиск идентификатора пользователя +- `SERIES_TMP_PATH/series_{id}` — метаданные серии + +Режим обнаружения изменений (если `cache_changes` включен): сравнивает временную метку базы данных `updated` с файлом `mtime`, восстанавливает только измененные элементы. + +Режим полной перестройки: восстанавливает все записи. Регулируется параметром `cache_thread_count`. + +### Готовность кэша + +Файл `cache_complete` записывается после каждой полной сборки кэша. Путь к потоковой передаче проверяет наличие этого файла и завершает работу с ошибкой, если он отсутствует. + +### Безопасность холодного кэширования + +В потоковом загрузчике (`LegacyInitializer::initStreaming()`) отображается `servers`, +блок-листы и `proxy_servers` из файлового кэша. Перед первой сборкой +(новая загрузка, сброшенный tmp) эти файлы не существуют и `CacheReader::get()` +возвращает `null`, поэтому для каждого такого глобального массива по умолчанию используется пустой массив. Холодный кэш +поэтому ** ошибка закрыта** — запрос не находит серверов и показывает "не в эфире". — +вместо предупреждения `foreach(null)` или `in_array($x, null)` фатального (PHP 8) +вниз по течению. Действительно поврежденный кэш *сборка* все еще отображается отдельно с помощью +@предупреждение @0@@ о сбое записи, поэтому это значение по умолчанию маскирует только временный +окно холодного пуска, а не настоящий сбой. + +--- + +## Соглашения о ключах кэширования + +### Системные ключи (CACHE_TMP_PATH) + +|Ключ|Содержание| +| --- | --- | +| `settings` |массив настроек панели| +| `servers` |`array[server_id]` → конфигурация сервера| +| `bouquets` |`array[bouquet_id]` → bouquet определение| +| `categories` |`array[category_id]` → данные категории| +| `bouquet_map` |`array[stream_id]` → `array[bouquet_id]`| +| `category_map` |`array[bouquet_id]` → `array[category_id]`| +| `permissions_{group_id}` |набор групповых разрешений| +| `cache_complete` |`time()` временная метка последней полной сборки| + +### Ключи потока (STREAMS_TMP_PATH) + +|Ключ|Содержание| +| --- | --- | +| `stream_{id}` |информация о потоке + букеты + состояние каждого сервера| +| `channels_categories` |`array[stream_id]` → `array[category_id]`| + +### Линейные ключи (LINES_TMP_PATH) + +|Ключ|Содержание| +| --- | --- | +| `line_i_{user_id}` |полная запись о пользователе| +| `line_c_{username_password}` |user_id (поиск учетных данных)| +| `line_t_{access_token}` |user_id (поиск токена)| + +### Ключи серии (SERIES_TMP_PATH) + +|Ключ|Содержание| +| --- | --- | +| `series_{id}` |метаданные серии| +| `series_map` |`array[stream_id]` → идентификатор серии| +| `episodes_{series_id}` |`array[season_num]` → список эпизодов| + +--- + +## Шаблоны аннулирования + +|Спусковой крючок|Затронутые ключи|Механизм| +| --- | --- | --- | +|Администратор редактирует поток|`stream_{id}`, `bouquet_map`|сигнал → следующий `cron:cache_engine`| +|Администратор редактирует строку|`line_i_*`, `line_c_*`, `line_t_*`|следующий `cron:cache_engine`| +|Настройки изменены|`settings`, категории, блок-листы|`SettingsManager::clearCache()` + хрон| +|Обновлен список серверов|`servers`, `bouquet_map`|крон| +|Начало потока (FFprobe)| `{md5(source)}` |5-минутный TTL с помощью проверки mtime файла| +|Кнопка сброса администратора|все файлы в `CACHE_TMP_PATH`| `rm -rf` | + +--- + +## Потоковая передача против пути администратора + +### Путь потоковой передачи (`www/stream/*`) + +- `cached: true` по умолчанию. +- Считывает данные исключительно из файлового кэша (никаких запросов к базе данных). +- Необработанная двоичная десериализация: `igbinary_unserialize(file_get_contents(...))`. +- Если `cache_complete` отсутствует: завершите работу с ошибкой. + +### Путь администратора (`Public/Controllers/Admin/*`) + +- `cached: false` по умолчанию. +- Считывает данные из базы данных непосредственно через доменные службы. +- Дополнительный кратковременный кэш (пример из `BouquetService::getAll()`): + +```php +$rCache = FileCache::getCache('bouquets', 60); // only if < 60s old +if (!empty($rCache)) { + return $rCache; +} +// miss: query database and write cache +FileCache::setCache('bouquets', $rOutput); +``` + +--- + +## Расположение файла кэша + +```text +/home/xc_vm/tmp/cache/ +├── settings +├── servers +├── bouquets +├── categories +├── bouquet_map +├── category_map +├── cache_complete +├── heavy_cache_built +├── streams/ +│ ├── stream_{id} +│ └── channels_categories +├── lines/ +│ ├── line_i_{user_id} +│ ├── line_c_{username_password} +│ └── line_t_{access_token} +└── series/ + ├── series_{id} + ├── series_map + └── episodes_{series_id} +``` + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Core/Cache/CacheInterface.php` |контракт на кэширование| +| `src/Core/Cache/FileCache.php` |реализация кэша на основе файлов| +| `src/Core/Cache/RedisCache.php` |Redis реализация кэша| +| `src/Infrastructure/Redis/RedisManager.php` |Redis единичный элемент подключения| +| `src/Infrastructure/Cache/CacheReader.php` |устаревший мост для чтения кэша| +| `src/Cli/CronJobs/CacheCronJob.php` |облегченная генерация кэша| +| `src/Cli/CronJobs/CacheEngineCronJob.php` |генерация большого объема кэша (потоки, строки, серии)| +| `src/Domain/Bouquet/BouquetService.php` |пример кэширования пути администратора| +| `src/Domain/Stream/ConnectionTracker.php` |Redis отсортированные наборы для определения состояния соединения| diff --git a/docs/ru/development/event-system.md b/docs/ru/development/event-system.md new file mode 100644 index 00000000..177ec661 --- /dev/null +++ b/docs/ru/development/event-system.md @@ -0,0 +1,202 @@ +# Система событий + +XC_VM использует типизированный диспетчер событий в стиле PSR-14. Все события являются простыми PHP классами +отправлено и получено по имени. Диспетчер основан на экземпляре и хранится в +Откройте контейнер под ключом `events`. + +--- + +## Диспетчер событий + +`EventDispatcher` - это синглтон с мостом экземпляра. Статические методы делегируют +активный экземпляр, поэтому существующие сайты вызовов работают без изменений. + +```php +// bootstrap.php wires the canonical instance: +$dispatcher = new EventDispatcher(); +EventDispatcher::setInstance($dispatcher); +$container->set('events', $dispatcher); + +// Both paths reach the same listener store: +EventDispatcher::dispatch(new MyEvent(...)); // static call +$container->get('events')->dispatch(new MyEvent(...)); // instance call +``` + +В тестах изолируйте состояние для каждого теста с помощью: + +```php +protected function setUp(): void { + $dispatcher = new EventDispatcher(); + EventDispatcher::setInstance($dispatcher); +} + +protected function tearDown(): void { + EventDispatcher::resetInstance(); +} +``` + +--- + +## Диспетчеризация и прослушивание + +```php +// Dispatch +EventDispatcher::dispatch(new StreamStartedEvent($lineId, $streamId)); + +// Listen +EventDispatcher::listen(StreamStartedEvent::class, function (StreamStartedEvent $e): void { + // handle +}, priority: 10); + +// Remove a listener +EventDispatcher::unlisten(StreamStartedEvent::class, $myCallable); + +// Check +EventDispatcher::hasListeners(StreamStartedEvent::class); // bool +``` + +**Приоритет** — более высокое целое число = вызывается первым. По умолчанию `0`. + +--- + +## Регистрация слушателей в модуле + +### Вариант 1 — Получить массив eventsubscribers() + +```php +public function getEventSubscribers(): array { + return [ + StreamStartedEvent::class => [$this, 'onStreamStarted'], + StreamStartedEvent::class => [[$this, 'onStreamStarted'], 20], // with priority + ]; +} +``` + +### Вариант 2 — атрибут #[ListensTo] + +```php +use ListensTo; + +class MyModuleModule extends BaseModule { + + #[ListensTo(StreamStartedEvent::class, priority: 20)] + public function onStreamStarted(StreamStartedEvent $e): void { + // handle + } + + // IS_REPEATABLE — multiple attributes on the same method + #[ListensTo(StreamStartedEvent::class)] + #[ListensTo(StreamStoppedEvent::class)] + public function onStreamChange(object $e): void { + // handle both events + } +} +``` + +Оба механизма работают одновременно и могут сосуществовать в одном модуле. +`ModuleLoader::bootAll()` выполняет оба прохода для каждого загруженного модуля. + +--- + +## Останавливаемые события + +Наберите `AbstractEvent` и позвоните `$e->stopPropagation()`: + +```php +class MyGatingEvent extends AbstractEvent { + public bool $allowed = true; +} + +EventDispatcher::listen(MyGatingEvent::class, function (MyGatingEvent $e): void { + if (!$this->check()) { + $e->allowed = false; + $e->stopPropagation(); + } +}, priority: 100); +``` + +Прослушиватели пропускаются, как только `isPropagationStopped()` возвращает `true`. + +--- + +## Встроенные основные события + +|Класс события|Местоположение|Когда отправлено|Останавливаемый| +| ----------- | -------- | --------------- | :-------: | +| `ModuleLoadedEvent` | `Events/Module/` |После загрузки файла модуля|Нет| +| `ModuleBootedEvent` | `Events/Module/` |После вызова `boot()`|Нет| +| `PackageInstalledEvent` | `Events/Module/` |После установки marketplace|Нет| +| `UserAuthenticatedEvent` | `Events/Auth/` |После успешного входа в систему|Да| +| `UserLoggedOutEvent` | `Events/Auth/` |После выхода из системы|Нет| +| `StreamStartedEvent` | `Events/Stream/` |После начала трансляции|Нет| +| `StreamStoppedEvent` | `Events/Stream/` |После того, как поток прекратился|Нет| +| `SettingsChangedEvent` | `Events/Settings/` |После сохранения настроек|Нет| + +--- + +## Создание пользовательского события + +Простой класс — используйте свойства `readonly` для неизменяемых полезных нагрузок.: + +```php +get('unknown'); +} catch (NotFoundException $e) { + // PSR-11 NotFoundExceptionInterface +} +``` + +--- + +## Исключения из модулей + +|Класс|Когда его бросают| +| ----- | ----------- | +| `ModuleException` |База данных для всех отказов модулей| +| `ModuleNotFoundException` |Отсутствует необходимый модуль зависимостей| +| `ModuleLoadException` |Файл модуля не может быть загружен или класс не найден| +| `ModuleManifestException` |`module.json` отсутствует, неправильно сформирован или не прошел проверку| +| `ModuleCycleException` |Граф зависимостей имеет цикл| + +--- + +## Перехват по подсистемам + +```php +// Catch any XC_VM exception +try { + $loader->loadAll(); +} catch (XcVmException $e) { + logger()->error($e->getMessage()); +} + +// Catch only module-related failures +try { + $loader->loadAll(); +} catch (ModuleException $e) { + // ModuleNotFoundException | ModuleLoadException | ... +} + +// Catch container-specific failures +try { + $container->get('missing'); +} catch (ContainerException $e) { + // CircularDependencyException | NotFoundException | ... +} +``` + +--- + +## Местоположение + +``` +src/Core/Exception/ +├── XcVmException.php +├── Container/ +│ ├── ContainerException.php +│ ├── CircularDependencyException.php +│ ├── ServiceCreationException.php +│ └── NotFoundException.php +└── Module/ + ├── ModuleException.php + ├── ModuleNotFoundException.php + ├── ModuleLoadException.php + ├── ModuleManifestException.php + └── ModuleCycleException.php +``` + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/Core/Exception/` |Базовые классы исключений и иерархия проекта| diff --git a/docs/ru/development/http-request-handling.md b/docs/ru/development/http-request-handling.md new file mode 100644 index 00000000..cb7f3d43 --- /dev/null +++ b/docs/ru/development/http-request-handling.md @@ -0,0 +1,406 @@ +# Обработка HTTP-запросов + +Этот документ описывает, как обрабатываются HTTP-запросы в XC_VM, охватывая полный жизненный цикл от первоначального ввода до маршрутизации и отправки. В зависимости от типа запроса существует несколько путей выполнения. + +--- + +## Обзор + +Уровень HTTP построен из этих основных компонентов: + +|Компонент|Файл|Роль| +| --- | --- | --- | +| `RequestGuard` | `src/Core/Http/RequestGuard.php` |Безопасность перед маршрутизацией: защита от наводнений, проверка хоста, запуск регистратора| +| `InputValidator` | `src/Core/Validation/InputValidator.php` |Очистка входных данных (очистка глобальных объектов, повторный анализ)| +| `RequestManager` | `src/Core/Http/RequestManager.php` |Статический фасад, хранящий объединенные данные запроса GET+POST| +| `Request` | `src/Core/Http/Request.php` |Объектно-ориентированная оболочка запроса (существует, но не используется в основном производственном потоке)| +| `Router` | `src/Core/Http/Router.php` |Регистрация и отправка маршрута по странице и API| +| `Response` | `src/Core/Http/Response.php` |Помощники по статическому ответу (JSON, redirect, CORS и т.д.)| +| `LegacyInitializer` | `src/Core/Init/LegacyInitializer.php` |Устаревший bootstrap, который подключает очистку к `RequestManager`| +| `StreamingRequestBootstrap` | `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` |Облегченный bootstrap для конечных точек потоковой передачи| + +--- + +## Поток запросов: Страницы администратора/панели управления + +Точка входа: `src/Public/index.php` + +```text +nginx -> Public/index.php + -> URL parsing (scope + pageName) + -> XC_Bootstrap::boot(CONTEXT_ADMIN) + -> floodProtection() (block banned IPs) + -> hostVerification() (check allowed domains) + -> initSession() + -> initDatabase() + -> initLegacyCore() + -> LegacyInitializer::initCore() + -> InputValidator::cleanGlobals($_GET, $_POST, $_SESSION, $_COOKIE) + -> InputValidator::parseIncomingRecursively($_GET) -> $rInput + -> InputValidator::parseIncomingRecursively($_POST, $rInput) -> RequestManager::set() + -> initRedis() + -> initAdminAPI() + -> initTranslator() + -> Load routes from src/Public/routes/{scope}.php + -> Load routes from src/Public/routes/api.php + -> ModuleLoader::bootAll() (admin/reseller scope, with collision detection) + -> Router::dispatchApi($action) (checked first for "api" page) + -> Router::dispatch($pageName, $method) + -> Controller handler +``` + +### Ключевая деталь: санитарная обработка входных данных + +Рабочий процесс администрирования не использует `Request::capture()`. Вместо этого `LegacyInitializer::initCore()` управляет обработкой входных данных: + +1. `InputValidator::cleanGlobals()` вызывается на месте `$_GET`, `$_POST`, `$_SESSION`, и `$_COOKIE`, удаляя нулевые байты, последовательности обхода пути (`../`) и символы переопределения RTL. +2. `InputValidator::parseIncomingRecursively()` очищает ключи и значения (HTML-объекты, теги скриптов, разделители комментариев, окончания строк) и возвращает чистый массив. +3. Результат (объединяется с сообщением, сообщение имеет приоритет) сохраняется через `RequestManager::set()`. + +Во всей кодовой базе доступ к данным запроса осуществляется через `RequestManager::get($key)` и `RequestManager::getAll()`, а не через объект `Request`. + +--- + +## Поток запросов: REST API + +Точка входа: `src/Public/index.php` (короткое замыкание перед маршрутизатором) + +Когда `XC_SCOPE` равно `includes/api/admin` или `includes/api/reseller`: + +```text +nginx -> Public/index.php + -> XC_Bootstrap::boot(CONTEXT_ADMIN) + -> new AdminApiController() or new ResellerRestApiController() + -> $controller->index() + -> exit +``` + +Этот путь полностью обходит маршрутизатор. + +--- + +## Поток запросов: Потоковая передача + +Точка входа: `www/stream/*.php` конечные точки или `Public/index.php` для `player_api` + +```text +nginx -> StreamingRequestBootstrap::init($filename) + -> Load error codes, paths, config, binaries + -> Flood protection (check block_{IP} file) + -> Load settings from file cache + -> Host verification + -> Logger init + -> StreamingBootstrap::bootstrap($filename, $settings) + -> LegacyInitializer::initStreaming() + -> Request::cleanGlobals() on superglobals + -> Request::parseIncomingRecursively() -> $GLOBALS['rRequest'] + -> RequestManager::set($GLOBALS['rRequest']) +``` + +Путь потоковой передачи намеренно упрощен. Он не загружает маршрутизатор, EventDispatcher, транслятор или полный сервисный контейнер. Диспетчеризация маршрутов отсутствует; каждая конечная точка потоковой передачи имеет выделенную точку входа. + +--- + +## `RequestGuard` + +Файл: `src/Core/Http/RequestGuard.php` + +Процедурный защитный скрипт, включенный ранее в устаревший bootstrap. Выполняется только для HTTP-запросов (пропускается, если задано значение `$_SERVER['argc']`, указывающее CLI). + +### Обязанности + +1. **Защита от наводнений** -- Если файл `FLOOD_TMP_PATH/block_{IP}` существует, запрос отклоняется по протоколу HTTP 403. +2. **Загрузка кэша настроек** -- Считывает `$rSettings` из кэша файлов, сериализованных в igbinary, по адресу `CACHE_TMP_PATH/settings`. +3. **Проверка хоста** - Когда `$rSettings['verify_host']` имеет значение true, проверяется, что `HOST` отображается в кэшированном списке `allowed_domains`. Исключения: имя хоста `xc_vm` и любой действительный IP-адрес всегда разрешены. +4. **Флаг отображения ошибки** -- Устанавливает константу `PHP_ERRORS` вместо `$rSettings['debug_show_errors']`. +5. **Инициализация регистратора** -- Вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`. + +Примечание: В современном bootstrap (`XC_Bootstrap::boot()`) эти обязанности выполняются методами `floodProtection()` и `hostVerification()` напрямую, а не путем включения `RequestGuard.php`. + +--- + +## `InputValidator` + +Файл: `src/Core/Validation/InputValidator.php` + +Предоставляет статические методы для очистки входных данных и проверки на уровне действий. + +### Методы санитарной обработки + +|Метод|Описание| +| --- | --- | +| `cleanGlobals(&$data, $iteration)` |Удаление нулевых байтов на месте, обход пути (`../`) и переопределение символов RTL. Максимум 10 уровней рекурсии.| +| `parseIncomingRecursively(&$data, $input, $iteration)` |Возвращает новый обработанный массив. Очищаются как ключи, так и значения. Максимальное количество уровней рекурсии - 20.| +| `parseCleanKey($key)` |Наносим двойные точки, узоры `__wrapped__`, применяем `htmlspecialchars(urldecode())`.| +| `parseCleanValue($value)` |Удаляет теги ` STATUS_INVALID_INPUT, 'data' => $data]`, если недопустимо.| +| `confirmIDs($ids)` |Фильтрует массив только по целочисленным положительным идентификаторам.| + +--- + +## `RequestManager` + +Файл: `src/Core/Http/RequestManager.php` + +Статический интерфейс, в котором хранятся объединенные данные запроса GET+POST. Это основной шаблон доступа к данным запроса, используемый во всей базе кода. + +### Как данные попадают в + +`LegacyInitializer::initCore()` звонков: + +```php +$rInput = InputValidator::parseIncomingRecursively($_GET, array()); +RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $rInput)); +``` + +Параметры POST переопределяют параметры GET с помощью того же ключа (POST объединяется поверх GET). + +### интерфейс прикладного программирования + +|Метод|Описание| +| --- | --- | +| `set(array $request)` |Храните весь массив данных запроса.| +| `getAll()` |Извлеките все сохраненные данные запроса.| +| `get(string $key, $default = null)` |Извлеките одно значение по ключу.| +| `update(string $key, $value)` |Обновите один ключ в сохраненных данных.| + +### Использование + +```php +// Read a request parameter +$streamId = RequestManager::get('stream_id'); + +// Read all parameters +$allParams = RequestManager::getAll(); + +// Update a value (rare, used by some legacy handlers) +RequestManager::update('status', 'active'); +``` + +--- + +## `Request` + +Файл: `src/Core/Http/Request.php` + +Объектно-ориентированная оболочка запроса. Содержит статическую фабрику `capture()` и методы экземпляра для доступа к обработанным входным данным. Хотя класс существует и полностью функционален, в основном производственном потоке вместо него используется `InputValidator` + `RequestManager`. Методы статической очистки класса `Request` (`cleanGlobals`, `parseIncomingRecursively`) используются `LegacyInitializer::initStreaming()` для обеспечения обратной совместимости. + +### Строительство + +```php +// Static factory (singleton, not used in production admin flow) +$request = Request::capture(); + +// Direct construction +$request = new Request($_GET, $_POST, $_SERVER, $_COOKIE); +``` + +### Методы экземпляра + +|Метод|Подпись|Описание| +| --- | --- | --- | +| `input` | `input($key, $default = null)` |Получить из объединенных входных данных (приоритет POST над GET)| +| `get` | `get($key = null, $default = null)` |Получаем из строки запроса (`$_GET`). `null` ключ возвращает все.| +| `post` | `post($key = null, $default = null)` |Получаем данные из POST. `null` key возвращает все.| +| `all` | `all()` |Все объединенные входные данные| +| `has` | `has($key)` |Проверьте, существует ли ключ в объединенном вводе| +| `getInt` | `getInt($key, $default = 0)` |Получить значение в виде целого числа| +| `getBool` | `getBool($key, $default = false)` |Получить значение в виде логического значения (через `filter_var`)| +| `server` | `server($key, $default = null)` |Получаем `$_SERVER` значение| +| `cookie` | `cookie($key, $default = null)` |Получить значение файла cookie| +| `method` | `method()` |Строка HTTP-метода (GET, POST и т.д.)| +| `isPost` | `isPost()` |Проверьте, является ли метод POST| +| `isAjax` | `isAjax()` |Проверить `X-Requested-With: XMLHttpRequest`| +| `ip` | `ip()` |IP-адрес клиента (проверяет `X-Forwarded-For`, `X-Real-IP`, `REMOTE_ADDR`)| +| `uri` | `uri()` |URI запроса| +| `userAgent` | `userAgent()` |Заголовок пользовательского агента| +| `host` | `host()` |Заголовок узла (возвращается к `SERVER_NAME`)| +| `rawBody` | `rawBody()` |Необработанный текст сообщения от `php://input`| +| `json` | `json($assoc = true)` |Текст СООБЩЕНИЯ, декодированный в формате JSON| + +### Методы статической дезинфекции (обратная совместимость) + +Они отражают `InputValidator` и используются путем потоковой инициализации: + +|Метод|Описание| +| --- | --- | +| `cleanGlobals(&$data, $iteration)` |То же, что `InputValidator::cleanGlobals()`| +| `parseIncomingRecursively(&$data, $input, $iteration)` |То же, что `InputValidator::parseIncomingRecursively()`| +| `parseCleanKey($key)` |То же, что `InputValidator::parseCleanKey()`| +| `parseCleanValue($value)` |То же, что `InputValidator::parseCleanValue()`| + +--- + +## `Router` + +Файл: `src/Core/Http/Router.php` + +Одноэлементный маршрутизатор для отправки страниц и API. Заменяет устаревший шаблон `switch($rAction)`. + +### Регистрация маршрута + +|Метод|Подпись|Описание| +| --- | --- | --- | +| `get` | `get($route, $handler, $options = [])` |Зарегистрируйте маршрут получения страницы| +| `post` | `post($route, $handler, $options = [])` |Зарегистрируйте маршрут почтовой формы| +| `any` | `any($route, $handler, $options = [])` |Зарегистрируйте как GET, так и POST для одного и того же маршрута| +| `api` | `api($action, $handler, $options = [])` |Зарегистрируйте маршрут API (JSON, отправляемый по имени действия)| +| `group` | `group($prefix, $callback, $options = [])` |Группируйте маршруты под общим префиксом с общим промежуточным программным обеспечением/разрешениями| + +Параметр `$handler` принимает: +- `[ClassName::class, 'method']` -- создается через ServiceContainer (с возможностью возврата к `new`) +- Завершающий или вызываемый +- `[object, 'method']` + +Массив `$options` поддерживает: +- `'permission' => ['type', 'key']` -- проверяется с помощью `Authorization::check()` перед запуском обработчика +- `'middleware' => [callable, ...]` -- массив вызываемых объектов, выполняемых после проверки прав доступа, перед обработчиком + +### Примеры маршрутов + +```php +$router = Router::getInstance(); + +// Simple page routes +$router->get('streams', [StreamController::class, 'index']); +$router->post('stream/save', [StreamController::class, 'save']); + +// API route (JSON) +$router->api('deleteStream', [StreamController::class, 'apiDelete']); + +// Grouped routes with middleware and permissions +$router->group('watch', function (Router $r) { + $r->get('', [WatchController::class, 'index']); + $r->get('add', [WatchController::class, 'add']); + $r->post('settings', [WatchController::class, 'saveSettings']); + $r->api('enable', [WatchController::class, 'apiEnable']); +}, [ + 'permission' => ['admin', 'watch'], + 'middleware' => [$authCheck], +]); +``` + +### Нормализация маршрута + +Маршрутизатор нормализует устаревшие названия страниц, преобразуя символы подчеркивания в косые черты: + +|Ввод|Нормализованный| +| --- | --- | +| `watch` | `watch` | +| `watch_add` | `watch/add` | +| `settings_watch` | `settings/watch` | +| `plex_add.php` | `plex/add` | + +Эта нормализация применяется как во время регистрации (`buildRoute`), так и во время отправки (`normalizePage`), поэтому маршруты, зарегистрированные как `watch/add`, соответствуют названиям страниц, таким как `watch_add`. + +### Отправка + +```php +// Page dispatch (called from Public/index.php) +$router->dispatch($pageName, $method); // returns true if matched + +// API dispatch (called for action= parameter) +$router->dispatchApi($action); // returns true if matched +``` + +#### `dispatch($page, $method)` порядок исполнения + +1. Нормализовать `$page` (символы подчеркивания заменить косыми чертами, убрать `.php`). +2. Посмотрите в разделе POST routes (если используется метод POST) или GET routes. Если POST route не найден, вернитесь к GET routes. +3. **Проверка прав доступа ** через `checkPermission()`. Если отказано, наберите `denyAccess()` (переадресация или 403). +4. **Выполнение промежуточного программного обеспечения**. Вызывается каждый вызываемый объект в массиве `middleware`. Если какой-либо из них возвращает `false`, выполнение останавливается. +5. **Вызов обработчика** через `callHandler()`. + +#### `dispatchApi($action)` порядок исполнения + +1. Найдите в API маршруты по названию действия. +2. **Проверка прав доступа**. Если отказано, выводит `{"result": false}` и завершает работу. +3. **Вызов обработчика**. Промежуточное программное обеспечение не выполняется. + +Важно: `dispatchApi()` не запускает промежуточное программное обеспечение. Это намеренное отличие от отправки страницы. + +### Регистрация маршрута модуля + +Модули регистрируют маршруты через `ModuleInterface::registerRoutes()`. Маршрутизатор поддерживает безопасный режим регистрации, предотвращающий перезапись модулями основных маршрутов: + +```php +$router->beginModuleRegistration(); +// Module routes registered here -- duplicates are silently skipped +$moduleLoader->bootAll($container, $router); +$router->endModuleRegistration(); + +// Check for collisions (logged in development mode) +$collisions = $router->drainRouteCollisions(); +``` + +В режиме регистрации модуля (`preserveExistingRoutes = true`), если модуль пытается зарегистрировать маршрут, который уже существует, существующий маршрут сохраняется и регистрируется коллизия. `drainRouteCollisions()` возвращает и очищает собранные коллизии в виде массива `['type' => 'get'|'post'|'api', 'key' => 'route/path']`. + +### Самоанализ + +|Метод|Описание| +| --- | --- | +| `hasRoute($page)` |Проверьте, существует ли маршрут страницы (GET или POST).| +| `hasApiRoute($action)` |Проверьте, существует ли маршрут API| +| `getRoutes()` |Возвращает все зарегистрированные ключи маршрута как `['get' => [...], 'post' => [...], 'api' => [...]]`| + +--- + +## `Response` + +Файл: `src/Core/Http/Response.php` + +Статический помощник для отправки HTTP-ответов. Заменяет разрозненные шаблоны `header()` + `echo` + `exit()`. + +|Метод|Подпись|Описание| +| --- | --- | --- | +| `json` | `json($data, $statusCode = 200, $options = 0)` |Отправьте ответ в формате JSON и завершите работу| +| `jsonError` | `jsonError($message, $statusCode = 400, $extra = [])` |Отправьте сообщение об ошибке JSON и завершите работу| +| `redirect` | `redirect($url, $statusCode = 302)` |Отправьте перенаправление и завершите работу| +| `notFound` | `notFound($message = 'Not Found')` |Отправьте запрос 404 и выйдите| +| `header` | `header($name, $value)` |Установите один заголовок ответа| +| `cors` | `cors()` |Установить заголовки CORS (`Access-Control-Allow-Origin: *`)| +| `noCache` | `noCache()` |Установить заголовки без кэширования (используется для плейлистов HLS)| +| `raw` | `raw($content, $contentType, $statusCode)` |Отправьте необработанный контент с указанием типа контента и завершите работу| +| `empty` | `empty($statusCode = 204)` |Отправьте пустой ответ и завершите работу| + +--- + +## Контексты начальной загрузки + +`XC_Bootstrap::boot($context)` обеспечивает контекстно-зависимую инициализацию. Каждый контекст основывается на предыдущем: + +|Контекст|Что он инициализирует| +| --- | --- | +| `CONTEXT_MINIMAL` |Автозагрузка + константы + конфигурация + регистратор. Нет подключения к базе данных.| +| `CONTEXT_CLI` |+ База данных + `LegacyInitializer::initCore()` (очистка входных данных, настройки, пути FFmpeg). Необязательно Redis.| +| `CONTEXT_STREAM` |+ Только база данных (упрощенная, без `LegacyInitializer`). Конечные точки потоковой передачи используют `StreamingRequestBootstrap` вместо этого.| +| `CONTEXT_ADMIN` |+ Сессия + База данных + `LegacyInitializer::initCore()` + Redis + Admin API + Переводчик + глобальные настройки администратора. Полная инициализация.| + +Все HTTP-контексты (не CLI) также запускают защиту от наводнений и проверку хоста перед инициализацией, зависящей от контекста. + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Core/Http/RequestGuard.php` |Безопасность предварительной маршрутизации и инициализация регистратора (включая устаревшие)| +| `src/Core/Http/Request.php` |Оболочка запроса ООП с методами санитарной обработки| +| `src/Core/Http/Router.php` |Регистрация маршрута и отправка| +| `src/Core/Http/RequestManager.php` |Фасад данных статического запроса (шаблон основного доступа)| +| `src/Core/Http/Response.php` |Помощники по выводу ответов| +| `src/Core/Validation/InputValidator.php` |Очистка входных данных и проверка правильности действий| +| `src/Core/Init/LegacyInitializer.php` |Инициализация устаревшего ядра (очистка проводов в RequestManager)| +| `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` |Облегченный загрузчик конечной точки потоковой передачи| +| `src/Streaming/StreamingBootstrap.php` |Потоковое подключение к базе данных и устаревшая инициализация| +| `src/bootstrap.php` |Унифицированный bootstrap (`XC_Bootstrap` class)| +| `src/Public/index.php` |Внешний контроллер для администратора/реселлера/игрока/API| +| `src/Public/routes/admin.php` |Определения маршрутов на странице администратора| +| `src/Public/routes/reseller.php` |Определения маршрута на странице реселлера| +| `src/Public/routes/player.php` |Определения маршрута на странице игрока| diff --git a/docs/ru/development/modules.md b/docs/ru/development/modules.md new file mode 100644 index 00000000..b99d34ee --- /dev/null +++ b/docs/ru/development/modules.md @@ -0,0 +1,766 @@ +# Модульная система + +## Обзор + +Модуль - это изолированный каталог под `src/Modules/` с известным контрактом. Система +построен на принципах **Расширяемой платформы**: + +- Ядро (`Core/`) ничего не знает о модулях +- Модули могут зависеть от `Core/` и `Domain/`, но никогда друг от друга (кроме как через объявленные зависимости). +- Любой модуль можно отключить с помощью `config/modules.php`, не касаясь ядра +- Удаление каталога модуля не приводит к фатальным ошибкам + +--- + +## Структура каталогов модулей + +Имя каталога соответствует условию **`{name}_{hash5}`**, где `hash5` - это +первые 5 символов модуля `hash_id`. Логическое имя модуля (`module.json` +`name`, который никогда не содержит `_`) всегда разрешается из манифеста — никогда из +базовое имя каталога. Это позволяет двум модулям с **одинаковым именем** работать в разных +каталоги (`watch_2541a`, `watch_9f1c0`) и установить без столкновения с файловой системой. То +конфигурация, график зависимостей и пространство имен - все это зависит от канонического `name`, поэтому каталог +переименование не требует переноса данных. Каждый модуль ** должен ** иметь `hash_id`: загружаемые файлы. +без него получите новый идентификатор, сгенерированный и записанный в их `module.json` перед размещением, +таким образом, каталог без хэша никогда не создается. Устаревший пустой каталог `Modules/{name}/` из +более старое развертывание по-прежнему считывается, но оно ** автоматически переносится ** в `{name}_{hash5}` (генерируя +`hash_id`, если отсутствует) на следующем `console.php status` — макет без хэша удаляется, а не +держал. + +```text +src/Modules/my-module_9f1c0/ # {name}_{hash5}; canonical name is "my-module" +├── module.json # Metadata and manifest +├── MyModule.php # Module class (source of truth) +├── MyService.php # Business logic +├── MyController.php # Admin pages (optional) +├── MyCron.php # Cron logic (optional) +├── MyCronJob.php # CLI cron wrapper (optional) +├── database.sql # Master schema — full current CREATE/seed (optional) +├── database_drop.sql # Teardown — DROP every table the module owns (optional) +├── migrations/ # Forward version deltas (optional) +│ └── 1.1.0.sql # Applied only when upgrading a panel past 1.1.0 +└── views/ # Page templates (optional) + ├── my_page.php + └── my_page_scripts.php +``` + +Модуль владеет своей схемой через ** три роли, которые отражают ядро** (`bin/install/database.sql` ++ `migrations/`): + +|Файл|Роль|Работает на| +| ---- | ---- | ------- | +| `database.sql` |**Одна** главная схема — полная текущая `CREATE`/начальная|новая **установка**| +| `database_drop.sql` |**Одно** удаление — `DROP TABLE` для каждой таблицы, которой владеет модуль|**удалить**| +| `migrations/.sql` |**Папка** прямых переходов между версиями|**обновление ** для версий в `(installed, current]`| + +Правила: + +- **Новая установка выполняется только `database.sql`**, поэтому она всегда должна соответствовать последней версии. +схема (каждая дельта загнута внутрь). Записанный символ `installed_version` является водяным знаком — +ошибки никогда не воспроизводятся при новой установке. +- **Дельты доступны только в прямом направлении** (`ALTER`/`INSERT`), с именем `.sql` — разрыв - это +одинарный `database_drop.sql`, поэтому нет файлов для каждой версии `.down`. +- Сохраняйте дельты ** идемпотентными** (`ADD COLUMN IF NOT EXISTS`, `INSERT IGNORE`), чтобы повторные запуски были безопасными. +- Модуль без схемы не отправляет ни один из этих файлов. Модуль, работающий только с разницей (нет `database.sql`) +по-прежнему устанавливается путем повторного воспроизведения каждой дельты в ее версии. + +--- + +## модуль.json + +```json +{ + "name": "my-module", + "hash_id": "9f1c0b7e4d2a6538c1e0a4b7d6f39e21", + "description": "Short description", + "version": "1.0.0", + "requires_core": ">=2.0", + "environment": "main", + "priority": 0, + "dependencies": [], + "optional_dependencies": [], + "has_navbar": false, + "has_settings": false +} +``` + +### Поля манифеста + +|Поле|Тип|По умолчанию|Описание| +| ------ | ----- | :---: | ------------ | +| `name` | `string` |—|Каноническое имя модуля (в случае с kebab, без `_`). Каталог - это `{name}_{hash5}`, но код всегда использует это значение манифеста, а не базовое имя каталога.| +| `hash_id` | `string` |сгенерированный|**Постоянный** идентификатор модуля — случайный шестнадцатеричный код из 32 символов, генерируемый ОДИН раз и никогда не изменяющийся при изменении версии или переименовании. Его первые 5 символов образуют суффикс каталога `{name}_{hash5}`. Не редактируйте вручную.| +| `description` | `string` | `""` |Удобочитаемое описание| +| `version` | `string` |—|Предварительная версия (`1.0.0`)| +| `requires_core` | `string` |—|Минимальная версия ядра (`>=2.0`)| +| `environment` | `string` | `"main"` |`main`, `lb`, или `any`| +| `priority` | `int` | `0` |Приоритет загрузки — более высокие нагрузки раньше| +| `dependencies` | `array` | `[]` |Жесткие зависимости; если они недоступны, зависимый объект пропускается (см. ниже)| +| `optional_dependencies` | `array` | `[]` |Мягкие зависимости (загруженные ранее, если они есть)| +| `has_navbar` | `bool` | `false` |Регистрирует ли модуль элементы навигационной панели| +| `has_settings` | `bool` | `false` |Есть ли у модуля страница настроек| + +> **`hash_id` — постоянный идентификатор модуля.** Это случайное значение из 32 шестнадцатеричных чисел, +> сгенерированный ** один раз ** и ** никогда** впоследствии не изменявшийся — он должен пережить изменения версий +> и переименовывает (так что это случайное значение, а не производное от `name`/`version`). Сгенерируйте его с помощью +> `php -r 'echo bin2hex(random_bytes(16));'` и вставьте его в `module.json`, когда +> создайте новый модуль. Не создавайте его вручную и не используйте повторно другой модуль. Это придает модулям стабильную идентификацию, независимую от `name`, +> который является основой для перемещения модулей в отдельные репозитории и для создания +> явный источник обновления для каждого модуля ** — блок `update` manifest (смотрите ниже). + +**Жесткие и мягкие зависимости:** + +- `dependencies` — если какой—либо модуль недоступен (отсутствует на диске, отключен или находится в состоянии `failed`), зависимый модуль ** пропускается ** с записанным каскадным предупреждением (все, что зависит от него, также пропускается). Остальные модули, панель администратора и интерфейс командной строки продолжают работать; единственная неудовлетворенная зависимость больше не прерывает всю загрузку. +- `optional_dependencies` — загружается перед этим модулем, если присутствует, автоматически пропускается, если отсутствует + +> ** Защита от смещения.** Модуль, от которого зависят все еще включенные модули, не может быть `disabled` через панель / `ModuleManager::setState()` - операция отклоняется вместе со списком зависимых объектов (зеркальное отображение защиты `uninstallModule()`). Это предотвращает переход в состояние "`plex` включен, но его `watch` зависимость отключена". + +**Приоритет:** + +- Сначала при топологической сортировке учитывается график зависимостей, затем в пределах той же группы выполняется сортировка по `priority` по убыванию (большее число = загружено ранее), затем по алфавиту + +**Источник обновления (`update` block, необязательно):** + +Откуда модуль получает свои обновления. Отсутствует → `bundled` (файлы отправляются вместе с панелью и обновляются вместе с ней). + +```json +"update": { + "source": "bundled | platform | git | url", + "repository": "https://github.com/Vateron-Media/xc_vm-module-watch", + "channel": "stable", + "slug": "watch", + "url": "https://…/version.json" +} +``` + +- `source` — `bundled` ( с помощью панели), `platform` (магазин SaaS), `git` (репозитории), `url` (автономный хостинг). Неизвестные значения возвращаются к `bundled`. +- `repository` — git remote (для `git`); `slug` — store slug (для `platform`, по умолчанию используется значение `name`); `url` — URL версии/архива (для `url`); `channel` — `stable`/`beta` ( по умолчанию `stable`). + +Блок нормализуется с помощью `ModuleLoader` и отображается с помощью `ModuleManager::listModules()`. Еженедельный cron (`cron:module_updates`) проверяет `git`/`url` источники и записи `available_version`, что приводит к нажатию кнопки "Обновить" на X** (отображается только при наличии более новой версии). При нажатии кнопки "Обновить" запускается `ModuleManager::updateModuleFromSource()`: + +- `bundled` — файлы поступают вместе с панелью; Обновление просто запускает отложенные миграции. +- `platform` — делегирован потоку установки/обновления в магазине (откат + разветвление LB внутри). +- `git` — загружает ресурс выпуска **`module.tar.gz`** по тегу == новая версия (md5-проверяется с помощью выпуска `hashes.md5`, если он присутствует). +- `url` — перечитывает `version.json` для его `download` (https) + необязательный `md5`. + +Для `git`/`url` выбранный `module.json` **`hash_id` должен совпадать с установленным** (идентификация — репозиторий /URL-адрес не может выдавать себя за другой модуль), затем: резервное копирование → замена файлов → перенос → ** откат при любом сбое** → распространение в LB. + +**Стандартный набор и подготовка.** Модули, устанавливаемые панелью по умолчанию, перечислены в `config/bundled_modules.php`, а их ключ - в `hash_id` (неизменен для всех переименований). Сегодня все они имеют `bundled` (их файлы находятся в архиве панели). Когда модуль извлекается в свой собственный репозиторий, измените его запись на `git`/`url`/`platform` источник — `syncBundledModules()`, затем автоматически извлекает и устанавливает его с помощью `provisionStandardSet()` (не требуется, пока все находится в комплекте на диске). `ModuleManager::findModuleByHashId()` определяет модуль по его стабильному идентификатору независимо от каталога/имени. + +--- + +## Подинтерфейсы + +`ModuleInterface` разбивает площадь поверхности модуля на типизированные субдоговоры: + +```text +ModuleInterface +├── ServiceProviderInterface → boot(ServiceContainer) +├── RouteProviderInterface → registerRoutes(Router) +├── CommandProviderInterface → registerCommands(CommandRegistry) +└── NavbarProviderInterface → registerNavbar() +``` + +`StreamMiddlewareProviderInterface` является ** необязательным ** — он НЕ является частью `ModuleInterface`. +Реализуйте это только в том случае, если модулю необходимо внедрить себя в потоковый конвейер. + +```php +// Optional — not in ModuleInterface +class MyModule implements ModuleInterface, StreamMiddlewareProviderInterface { + public function getStreamMiddleware(): array { + return [new MyStreamMiddleware()]; + } +} +``` + +--- + +## Класс модуля + +Extend `BaseModule` — он не предоставляет значения по умолчанию для каждого необязательного метода, так что вы можете использовать только +переопределите то, что на самом деле использует модуль. Требуются только `getName()` и `getVersion()`. + +```php +set('my-module.service', function (ServiceContainer $c): MyModuleService { + return new MyModuleService($c->get('db')); + }); + } + + public function registerRoutes(Router $router): void { + $router->get('my_page', [MyModuleController::class, 'index'], [ + 'permission' => ['adv', 'my_module'], + ]); + } + + public function registerCommands(CommandRegistry $registry): void { + $registry->register(new MyModuleCronJob()); + } + + public function registerNavbar(): void { + NavbarRegistry::add( + (new NavbarItem('management.service_setup.my_module')) + ->parent('management.service_setup') + ->url('my_page') + ->label('my_module') + ->permissions(['my_module']) + ->order(60) + ); + } +} +``` + +> ** Совет:** модулю без маршрутов, элементов навигационной панели и команд CLI требуется только +> `getName()`, `getVersion()`, и `boot()`. +> Модуль изолированной подсистемы (его собственная точка входа и bootstrap, например, Ministra) обычно +> оставляет `boot()` и `registerRoutes()` унаследованными как недействительные. + +### Метод контракта + +|Метод|Интерфейс|Описание| +| ------- | ----------- | ---------- | +| `getName(): string` | `ModuleInterface` |Уникальное имя (соответствует каталогу)| +| `getVersion(): string` | `ModuleInterface` |Версия Semver| +| `boot(ServiceContainer)` | `ServiceProviderInterface` |Регистрация сервисов в контейнере DI| +| `registerRoutes(Router)` | `RouteProviderInterface` |Регистрация HTTP- и API-маршрутов| +| `registerCommands(CommandRegistry)` | `CommandProviderInterface` |Регистрация команд CLI и задач cron| +| `registerNavbar()` | `NavbarProviderInterface` |Регистрация элементов навигационной панели| +| `install(): void` | `ModuleInterface` |Запуск при установке модуля (миграции, начальный запуск)| +| `uninstall(): void` | `ModuleInterface` |Запуск при удалении модуля (очистка)| + +> **Важно — версия может храниться в двух местах.** Модуль объявляет свою версию +> **дважды**: поле `"version"` в поле `module.json` и возвращаемое значение +> `getVersion()` в классе module. **Сохраняйте их идентичными и изменяйте оба перед +> издательский.** Во время выполнения манифест `version` имеет приоритет — установка/обновление +> и водяной знак `installed_version` сначала читается как `module.json`, и только потом возвращается +> to `getVersion()` — таким образом, устаревший @@ 1@@ тихо выходит из строя и является +> распространенный источник ошибок типа "выполнена /не выполнена неправильная миграция". Если модуль отправляет файл +> миграции, `database.sql` (основная схема) и самый высокий `migrations/.sql` +> дельта также должна соответствовать этой версии. + +--- + +## PHP пространства имен + +Каждый модуль находится в выделенном пространстве имен PHP: `XcVm\Module\{Pascal}`, где `{Pascal}` - это +преобразование имени каталога модуля в PascalCase. + +``` +src/Modules/my-module/ → namespace XcVm\Module\MyModule; +src/Modules/watch/ → namespace XcVm\Module\Watch; +``` + +В главном файле модуля должно быть объявлено это пространство имен с расширением `BaseModule`: + +```php +set('my-module.service', fn () => new MyModuleService()); + } +} +``` + +**Правила:** + +- Имя файла основного класса модуля: `Module.php` — обязательно (соглашение с загрузчиком модулей) +- Все остальные имена файлов класса: `.php` +- Добавьте `use ClassName;` для каждого базового класса, на который ссылается ссылка (базовый модуль, ServiceContainer, маршрутизатор и т.д.) +- Никогда не импортируйте классы из других модулей — общайтесь через события или контейнер DI + +--- + +## Оформление контейнеров и сервизов DI + +Сервисы регистрируются в `boot()` через `ServiceContainer`. Контейнер поддерживает: + +- **`set(id, factory)`** — отложенный синглтон с помощью вызываемого или прямого значения +- **`factory(id, callable)`** — новый экземпляр для каждого `get()` +- **`decorate(id, callable, priority)`** — завершение существующей службы + +```php +// Decorate a service (adds behaviour around the original) +$container->decorate('stream.encoder', function (mixed $inner, ServiceContainer $c): MyEncoder { + return new MyEncoder($inner, $c->get('settings')); +}, priority: 20); +``` + +Декораторы объединены в цепочки по приоритету (самый высокий и самый внешний). Защищенные сервисы +(`db`, `settings`, `config`, `auth`) не удается оформить — при любой попытке генерируется `RuntimeException`. + +### Соответствие требованиям стандарта PSR-11 + +`ServiceContainer` реализует `ContainerInterface`: + +```php +public function get(string $id): mixed; // throws NotFoundException if missing +public function has(string $id): bool; +``` + +`NotFoundException` реализует `NotFoundExceptionInterface extends ContainerExceptionInterface`. + +--- + +## События PSR-14 + +События - это простые PHP классы. Отправляйте их через `EventDispatcher`: + +```php +// In any module +EventDispatcher::dispatch(new MyEvent($payload)); + +// Subscribe +EventDispatcher::listen(MyEvent::class, function (MyEvent $e): void { + // handle +}, priority: 10); +``` + +**Приоритет** — более высокое целое число = вызывается первым. По умолчанию `0`. + +**События, которые можно остановить ** — введите `AbstractEvent` и вызовите `$e->stopPropagation()`: + +```php +class MyGatingEvent extends AbstractEvent { + public bool $allowed = true; +} + +EventDispatcher::listen(MyGatingEvent::class, function (MyGatingEvent $e): void { + if (!$this->check()) { + $e->allowed = false; + $e->stopPropagation(); + } +}, priority: 100); +``` + +### Встроенные основные события + +|Класс события|Когда отправлено|Останавливаемый| +| --------------- | ---------------------- | :-----------: | +| `ModuleLoadedEvent` |После загрузки файла модуля|❌| +| `ModuleBootedEvent` |После вызова `boot()`|❌| +| `PackageInstalledEvent` |После установки marketplace|❌| +| `UserAuthenticatedEvent` |После успешного входа в систему|✅| +| `UserLoggedOutEvent` |После выхода из системы|❌| +| `StreamStartingEvent` |Перед началом трансляции|✅| +| `StreamStartedEvent` |После начала трансляции|❌| +| `StreamStoppedEvent` |После того, как поток прекратился|❌| +| `SettingsChangedEvent` |После сохранения настроек|❌| + +--- + +## Потоковое промежуточное программное обеспечение + +Модули могут внедрять промежуточное программное обеспечение в потоковый конвейер, реализуя +`StreamMiddlewareProviderInterface` (отдельно от `ModuleInterface`): + +```php +class MyStreamMiddleware implements StreamMiddlewareInterface { + + public function getPriority(): int { + return 50; + } + + public function handle(StreamContext $ctx, callable $next): StreamContext { + // before — read or set attributes + $ctx->set('my.key', 'value'); + $ctx = $next($ctx); + // after + return $ctx; + } +} +``` + +`StreamContext` - это набор атрибутов (`get`, `set`, `has`, `abort`, `isAborted`). `StreamPipeline` +выполняет промежуточное программное обеспечение, отсортированное по `getPriority()` убыванию. + +### Приоритеты трубопровода + +|Диапазон|Владелец| +| ---------- | ----------------- | +| `80–100` |Ядро (авторизация, разрешение, ограничение подключения)| +| `0–79` |Модули| + +### Зарезервированные слоты на панели навигации + +|Родительский узел|Гнезда для модулей| +| ------------------- | ------------------ | +| `management.service_setup` |`order` ≥ 60| +| `management.logs` |`order` ≥ 170| + +--- + +## Включение / выключение модулей + +Все обнаруженные модули загружаются по умолчанию. Используйте `src/config/modules.php` для переопределения состояния: + +```php +return [ + 'my-module' => ['state' => 'disabled'], // preferred + // or legacy boolean (still accepted): + 'my-module' => ['enabled' => false], +]; +``` + +Доступные значения `state` (подкрепленные `ModuleState` enum): + +|Ценность|Значение| +| ----- | ------- | +| `enabled` |Загрузка модуля (по умолчанию)| +| `disabled` |Модуль обнаружен, но пропущен| +| `installing` |Переходное состояние, заданное `ModuleManager` во время установки| +| `failed` |Ошибка установки; модуль пропущен (не загружен)| + +> **Панельная диагностика.** На странице "Модули" отображается желтый значок "Проблема с зависимостями" рядом со статусом модуля, когда требуемая зависимость отсутствует или не включена (например, `plex` означает `Enabled`, а `watch` - это `failed`). Во всплывающей подсказке к значку перечислены конкретные проблемы. Это поле `dependency_warnings` вычисляется с помощью `ModuleManager::listModules()`. + +Чтобы переопределить класс, разрешенный для модуля: + +```php +return [ + 'my-module' => ['class' => 'XcVm\\Module\\MyModuleV2\\MyModuleV2Module'], +]; +``` + +`config/modules.php` содержит только переопределения. Пустой или отсутствующий файл означает, что все обнаруженные +загружаются модули. + +--- + +## Как работает загрузка + +`ModuleLoader` выполняет эти действия при каждом запросе: + +1. Сканирование `src/Modules/*/module.json` +2. Применяет переопределения из `config/modules.php` +3. Фильтры по окружающей среде (`main` / `lb` / `any`) +4. Определяет порядок загрузки: + - `pruneUnsatisfiableModules()` удаляет модули, требуемые зависимости которых недоступны (каскадно, с зарегистрированным предупреждением), поэтому загрузка никогда не прерывается + - Топологическая сортировка (DFS) по графу зависимостей + - В пределах одной и той же группы зависимостей выполните сортировку по `priority` по убыванию, затем по алфавиту + - Выдает `RuntimeException` в циклах (циклические зависимости остаются фатальными) + - Отсутствующие необязательные зависимости автоматически пропускаются +5. Преобразует имя класса: `my-module` → ПОЛНОЕ имя `XcVm\Module\MyModule\MyModuleModule` +(kebab-case → PascalCase; может быть переопределен с помощью клавиши `class` в конфигурации) +6. Регистрирует автозагрузчик модуля PSR-4 (сопоставляет `XcVm\Module\` с каталогом модуля) +7. Создает экземпляр класса module + +В веб-контексте: + +- `bootAll($container, $router)` → вызовы `boot()`, `registerRoutes()`, `registerNavbar()`, +и подписывается на события для каждого загруженного модуля + +В контексте командной строки: + +- `registerAllCommands($registry)` → вызывает `registerCommands()` для каждого загруженного модуля + +--- + +## Marketplace: установка через расширение C + +Модули с платформы устанавливаются через `ModuleManager::downloadFromPlatform()`: + +```php +$manager->downloadFromPlatform(slug: 'my-module', version: '1.2.0', apiKey: $key); +``` + +Под капотом: + +1. @Расширение @0@@ — C загружает, расшифровывает и распаковывает файлы +2. `installModule($slug)` — запускает `install()` в модуле +3. `EventDispatcher::dispatch(new PackageInstalledEvent(...))` — отправляет событие +4. `hotReload($slug, $path)` — загружает модуль в текущем запросе **без перезапуска PHP-FPM** + +--- + +## Изолированные подсистемы + +Модуль может быть полностью изолированной подсистемой со своей собственной точкой входа и начальной загрузкой +(например, Ministra). Это ** соглашение**, а не маркерный интерфейс — он остается +обычный `ModuleInterface`/`BaseModule` модуль: + +```php +class MyModule extends BaseModule { + + public function getName(): string { + return 'my-module'; + } + + public function getVersion(): string { + return '1.0.0'; + } +} +``` + +Изоляция означает, что подсистема работает через свою собственную общедоступную точку входа (например, +`my-module/portal.php`, путь относительно `src/`, который обрабатывает свой собственный bootstrap) +с отдельным путем начальной загрузки. Он использует общую инфраструктуру (базу данных, кэш, конфигурацию), но +не участвует в основном `Router`, `ModuleLoader::bootAll()`, или +`NavbarRegistry`. Реализации `boot()` и `registerRoutes()`@ обычно являются +оставлено как унаследованное бездействие. + +--- + +## Контроллер + +```php +class MyController { + + protected string $viewsPath; + + public function __construct() { + $this->viewsPath = __DIR__ . '/views'; + require_once MAIN_HOME . 'Public/Views/layouts/admin.php'; + require_once MAIN_HOME . 'Public/Views/layouts/footer.php'; + } + + public function index(): void { + renderUnifiedLayoutHeader('admin', ['_TITLE' => 'My Module']); + include $this->viewsPath . '/my_page.php'; + renderUnifiedLayoutFooter('admin'); + include $this->viewsPath . '/my_page_scripts.php'; + } +} +``` + +|Правило| | +| --------- | -- | +| `__DIR__ . '/views'` |viewsPath — контроллер находится внутри каталога модуля| +|ПОЛУЧАТЬ страницы|позвоните `renderUnifiedLayoutHeader` перед просмотром, `renderUnifiedLayoutFooter` после| +|Действия API|нет макета — возвращаем JSON и выходим| + +--- + +## Задача Cron + +**Логика Cron** (`MyCron.php`) — только бизнес-логика, без подключения CLI. + +**Оболочка CronJob** (`MyCronJob.php`) — реализует `CommandInterface`, использует `CronTrait`: + +```php +class MyCronJob implements CommandInterface { + use CronTrait; + + public function getName(): string { return 'cron:my_task'; } + public function getDescription(): string { return 'Cron: my task'; } + + public function execute(array $rArgs): int { + if (!$this->assertRunAsXcVm()) { + return 1; + } + + require INCLUDES_PATH . 'admin.php'; + require_once __DIR__ . '/MyCron.php'; + + $this->initCron('XC_VM[MyTask]'); + MyCron::run(); + + return 0; + } +} +``` + +Регистрация в модуле: + +```php +public function registerCommands(CommandRegistry $registry): void { + $registry->register(new MyCronJob()); +} +``` + +Объявите запись crontab, переопределив `getCronEntries()` в классе module: + +```php +public function getCronEntries(): array { + return [ + '*/5 * * * *' => 'cron:my_task', + ]; +} +``` + +`ModuleLoader::collectCronEntries()` объединяет записи всех модулей и `StartupCommand` / +`StatusCommand` автоматически записывает их в системную crontab — никаких изменений в основных файлах не требуется. + +**Формат:** ключ = выражение cron, значение = имя консольной команды, зарегистрированное через `registerCommands()`. + +--- + +## Версионные миграции (MigratableInterface) + +> **Два механизма, оба аддитивные.** Схема на основе файлов**, описанная в разделе +> [Структура каталогов модулей](#module-directory-structure) (`database.sql` мастер + +> `database_drop.sql` teardown + `migrations/.sql` deltas) используется по умолчанию для +> простой DDL/seed. `MigratableInterface` ниже приведен программный путь для обновления. +> шаги, для которых требуется логика PHP (повторное заполнение данных, условные изменения). Модуль может использовать +> либо один из них, либо оба; `ModuleManager::updateModule()` сначала запускает файл delta, затем +> вызываемые миграции. + +Модули, для обновления которых требуется PHP логическая реализация `MigratableInterface`: + +```php +namespace XcVm\Module\MyModule; + +use BaseModule; +use MigratableInterface; +use ServiceContainer; + +class MyModuleModule extends BaseModule implements MigratableInterface { + + public function getMigrations(): array { + return [ + '1.1.0' => function (): void { + // runs when upgrading from any version < 1.1.0 to >= 1.1.0 + global $db; + $db->query("ALTER TABLE xc_my_table ADD COLUMN new_col INT DEFAULT 0"); + }, + '1.2.0' => function (): void { + // runs when upgrading from < 1.2.0 to >= 1.2.0 + }, + ]; + } +} +``` + +`ModuleManager::updateModule()` считывает `installed_version` из хранилища переопределений, фильтрует +сопоставляет только записи `> fromVersion && <= toVersion`, сортирует по параметрам и запускает каждую из них. +вызываемый в своей собственной транзакции базы данных. `installModule()` записи `installed_version` после +успешная установка; `uninstallModule()` очищает ее. + +**Основные правила:** + +- Ключи - это полустрочные строки (`'1.1.0'`, `'2.0.0'`) — `version_compare` используется упорядочение +- Каждая миграция выполняется в рамках своей собственной транзакции — сбой откатывает только этот шаг +- `BaseModule` предоставляет значение по умолчанию `getMigrations(): array { return []; }`, поэтому реализация +`MigratableInterface` необязательно + +--- + +## Composer обнаружение пакета + +Модули могут распространяться в виде пакетов Composer с `"type": "xcvm-module"`: + +```json +{ + "name": "vendor/my-xcvm-module", + "type": "xcvm-module", + "extra": { + "xcvm": { + "module-path": "src" + } + } +} +``` + +`ModuleLoader` автоматически сканирует `vendor/composer/installed.json` (Composer 1 и 2 +форматы) и обнаруживает все установленные пакеты `xcvm-module` вместе со встроенным +`src/Modules/` directory. Пакеты дедуплицируются — модуль как в `modules/`, так и в @@ +`vendor/` загружается только один раз. + +--- + +## Контрольный список модулей + +- [ ] Создать `src/Modules//` +- [ ] Добавьте `namespace XcVm\Module\;` в каждый файл класса +- [ ] Создать `module.json` с помощью `name`, `version`, `requires_core`, `priority`, `dependencies`, `optional_dependencies` +- [ ] Поставьте перманентную печать `hash_id` (`php -r 'echo bin2hex(random_bytes(16));'`; никогда не пишите это от руки) +- [ ] Создать `Module.php` расширение `BaseModule` +- [ ] Укажите версию в **обоих вариантах** `module.json` `"version"` и `getVersion()` — они должны совпадать (измените оба перед публикацией) +- [ ] Реализовать `boot()` для всех сервисов, предоставляемых модулем +- [ ] Реализовать `registerRoutes()` для конечных точек HTTP/API +- [ ] Внедрить `registerNavbar()` для элементов панели администратора (или оставить пустыми) +- [ ] (Если кроны) Создать `MyCron.php` + `MyCronJob.php`, зарегистрироваться в `registerCommands()` +- [ ] (Если crons) Переопределяет `getCronEntries()` в классе module (основной файл не изменяется) +- [ ] (Схема If) Отправка `database.sql` (мастер), `database_drop.sql` (демонтаж) и `migrations/.sql` дельты +- [ ] (При переносе PHP-логики) Реализовать `MigratableInterface::getMigrations()` +- [ ] (Если страницы) Создайте контроллер, используя `renderUnifiedLayoutHeader/Footer` +- [ ] (Если потоковое промежуточное программное обеспечение) Реализуйте `StreamMiddlewareProviderInterface` отдельно +- [ ] Проверить: `php -l src/Modules//Module.php` +- [ ] Проверьте: `php console.php --list` отображает команды модуля +- [ ] Проверьте: удаление каталога модуля не приводит к фатальной ошибке + +--- + +## часто задаваемые вопросы + +**Вопрос: Как мне отключить модуль?** +В `src/config/modules.php` добавьте `'module-name' => ['state' => 'disabled']`. +Устаревшая форма `'enabled' => false` также принята для обеспечения обратной совместимости. + +** Вопрос: Как мне объявить, что мой модуль зависит от другого?** +Используйте `dependencies` в `module.json` для жесткого удаления (должно присутствовать) или `optional_dependencies` +для мягкого удаления (загружается раньше вашего, если присутствует, и автоматически пропускается, если отсутствует). + +** Вопрос: Могу ли я украсить основную услугу?** +Да — используйте `$container->decorate('service-id', callable, priority)` в `boot()`. +Защищенные сервисы (`db`, `settings`, `config`, `auth`) не могут быть оформлены. + +**Вопрос: Как мне прослушивать основные события?** +Вызывайте `EventDispatcher::listen(EventClass::class, callable, priority)` в любом месте после начальной загрузки, +обычно внутри `boot()` или выделенного класса подписчиков. + +**Вопрос: Могу ли я отправлять пользовательские события из модуля?** +Да. Создайте простой класс или расширьте `AbstractEvent` и вызовите `EventDispatcher::dispatch(new MyEvent(...))`. + +** Вопрос: Для чего нужен `StreamMiddlewareProviderInterface`?** +Это позволяет модулю вводить `StreamMiddlewareInterface` в конвейер потоковой обработки +не изменяя `StreamProcess.php`. При необходимости применяйте его вместе с `ModuleInterface`. + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/Core/Module/ModuleLoader.php` |Обнаруживает, сортирует и загружает модули; PSR-4 распознаватель классов| +| `src/config/modules.php` |Конфигурация включения модуля / переопределения класса| +| `src/Modules/` |Каталоги модулей| +| `src/Core/Module/Contract/` |Подинтерфейсы модуля| diff --git a/docs/ru/development/navbar-rendering.md b/docs/ru/development/navbar-rendering.md new file mode 100644 index 00000000..73c1560c --- /dev/null +++ b/docs/ru/development/navbar-rendering.md @@ -0,0 +1,84 @@ +# Отображение навигационной панели на панели модуля + +Техническая документация по созданию и рендерингу навигационной панели в админ-панели. + +## Цель + +Навигационная панель создается декларативно из дерева `NavbarItem`, а не из жестко запрограммированных HTML-меню. + +Древовидные источники: + +1. Основные узлы из `CoreNavbarProvider::register()`. +2. Узлы модуля из `ModuleInterface::registerNavbar()`. + +## Жизненный цикл + +1. `ModuleLoader::bootAll()` вызывает `CoreNavbarProvider::register()`. +2. Затем для каждого загруженного модуля вызывается `registerNavbar()`. +3. В `Public/Views/admin/header.php` дерево отображается из `NavbarRegistry`. + +## Рендеринг в заголовке + +Рендеринг выполняется вспомогательными функциями: + +1. `_xc_nav_visible()` - фильтрация видимости для узла. +2. `_xc_nav_label()` - разрешение текстовой надписи. +3. `_xc_nav_children()` - рекурсивный рендеринг дочерних элементов. + +Узлы верхнего уровня берутся из `NavbarRegistry::getTopLevel()`, дочерние узлы берутся из `NavbarRegistry::getChildren($key)`. + +## Правила видимости + +Проверки выполняются в режиме `_xc_nav_visible()`: + +1. `desktopOnly`: скрывает узел на мобильном устройстве. +2. `settingDisabled`: скрывает узел, когда включен флаг настройки. +3. `permissions`: ИЛИ-проверить через `Authorization::check('adv', $permission)`. +4. Группа с `url='#'` отображается только в том случае, если виден хотя бы один дочерний элемент. +5. `divider` всегда передается и отображается как разделитель. + +## Особенности рендеринга + +1. `divider` отображается как разделитель без ссылки. +2. `submenuClass('megamenu')` включает отображение длинных списков в два столбца. +3. `noMobileSubmenu` отключает расширение дочернего подменю на мобильном устройстве. + +## Как модуль добавляет элемент прейскуранта + +Модуль добавляет элементы только через `registerNavbar()`: + +```php +public function registerNavbar(): void { + NavbarRegistry::add((new NavbarItem('management.service_setup.my_module')) + ->parent('management.service_setup') + ->url('my_module') + ->label('my_module') + ->permissions(['my_module']) + ->order(60)); + + NavbarRegistry::add((new NavbarItem('management.logs.my_module_log')) + ->parent('management.logs') + ->url('my_module_logs') + ->label('', 'My Module Logs') + ->permissions(['my_module']) + ->order(170)); +} +``` + +## Практические правила для модулей + +1. Используйте уникальные значения `key` в формате `section.group.item`. +2. Установите `parent` для существующего основного узла дерева или вашего собственного, уже добавленного узла. +3. Расположите элементы, используя `order` внутри одного родительского элемента. +4. Используйте `label('translation_key')` для перевода текста. +5. Используйте `label('', 'Literal Text')` для фиксированного буквального текста. +6. Если в модуле нет пунктов меню, оставьте `registerNavbar()` пустым. + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/Core/Module/NavbarRegistry.php` |Собирает элементы навигационной панели от поставщиков| +| `src/Core/Module/NavbarItem.php` |Объект значения элемента навигационной панели| +| `src/Core/Module/CoreNavbarProvider.php` |Встроенные основные пункты меню| +| `src/Public/Views/admin/header.php` |Визуализирует дерево навигационной панели| diff --git a/docs/ru/development/streaming-subsystem.md b/docs/ru/development/streaming-subsystem.md new file mode 100644 index 00000000..486f12f3 --- /dev/null +++ b/docs/ru/development/streaming-subsystem.md @@ -0,0 +1,496 @@ +# Подсистема потоковой передачи + +Подсистема потоковой передачи обрабатывает передачу в режиме реального времени, VOD и timeshift. +Это быстрый путь (~10-100 тыс. запросов в минуту, <50 мс p99), и он использует отдельный облегченный bootstrap, чтобы избежать загрузки всего стека администратора. + +--- + +## Поток запросов + +```text +client request + | +nginx rewrite (/auth/{token} -> /stream/live.php?token={token}) + | +StreamingRequestBootstrap::init() + | +StreamingBootstrap::bootstrap() + | +LegacyInitializer::initStreaming() + | +endpoint logic (live.php / vod.php / timeshift.php) + | +ShutdownHandler::handle() +``` + +nginx переписывает все URL-адреса потоковой передачи на PHP точки входа под `www/stream/`: + +|Шаблон URL-адреса|Точка входа|Цель| +| --- | --- | --- | +| `/auth/{token}` | `live.php` |Прямая трансляция| +| `/vauth/{token}` | `vod.php` |Доставка видео по запросу| +| `/tsauth/{token}` | `timeshift.php` |Архив/timeshift воспроизведение| +| `/hls/{token}` | `segment.php` |HLS сегментная поставка| +| `/key/{token}` | `key.php` |Ключ шифрования AES-128| +| `/subauth/{token}` | `subtitle.php` |Передача субтитров| + +--- + +## Расположение каталога + +``` +src/Streaming/ +├── StreamingBootstrap.php +├── AsyncFileOperations.php +├── TimeshiftClient.php +├── Auth/ +│ ├── StreamAuth.php +│ └── StreamAuthMiddleware.php +├── Balancer/ +│ └── ProxySelector.php +├── Codec/ +│ ├── FFmpegCommand.php +│ ├── FfmpegPaths.php +│ └── FFprobeRunner.php +├── Delivery/ +│ ├── HLSGenerator.php +│ ├── OffAirHandler.php +│ └── StreamRedirector.php +├── Health/ +│ └── ProcessChecker.php +├── Lifecycle/ +│ └── ShutdownHandler.php +└── Protection/ + └── ConnectionLimiter.php + +src/www/stream/ +├── init.php # Legacy bootstrap shim (deprecated) +├── auth.php # Token validation gateway +├── live.php # Live streaming delivery +├── vod.php # VOD delivery +├── timeshift.php # Archive/timeshift playback +├── segment.php # HLS segment delivery +├── key.php # Encryption key delivery +├── subtitle.php # Subtitle delivery +├── thumb.php # Thumbnail delivery +└── rtmp.php # RTMP publishing endpoint +``` + +--- + +## Конвейер начальной загрузки + +### 1. StreamingRequestBootstrap::init() + +Файл: `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` + +Действия в порядке: + +1. Загружайте коды ошибок, обработчик, пути, конфигурацию, двоичные файлы. +2. Защита от наводнений (только по протоколу HTTP): проверьте наличие `FLOOD_TMP_PATH . 'block_' . $rIP`. +3. Загрузите настройки из файлового кэша (`CACHE_TMP_PATH . 'settings'`). +4. Проверка хоста (только HTTP): проверка на соответствие `allowed_domains`. +5. Инициализируйте регистратор. +6. Аварийно закрытый шлюз: возвращает 404, если настройки отсутствуют (кроме `/status`). +7. Позвоните `StreamingBootstrap::bootstrap()`. + +### 2. Потоковый загрузчик::bootstrap() + +Файл: `src/Streaming/StreamingBootstrap.php` + +```php +public static function bootstrap($rFilename, $rSettings) +``` + +Классифицирует конечную точку: + +- **Конечные точки зондирования:** `probe`, `player_api` (небольшая нагрузка) +- **Конечные точки по умолчанию:** `live`, `thumb`, `subtitle`, `timeshift`, `vod`, `status` +- **Привилегированные конечные точки:** `rtmp`, `portal` + +Загружает `AsyncFileOperations.php` и `DatabaseHandler.php`, сохраняет настройки в `$GLOBALS['rSettings']` и получает доступ к данным в `$GLOBALS['rAccess']`, затем вызывает `LegacyInitializer::initStreaming()`. + +Возвращает экземпляр базы данных `$db` (используемый устаревшими точками входа). + +### 3. LegacyInitializer::Инициализация потока() + +Файл: `src/Core/Init/LegacyInitializer.php` + +Заполняет глобальные переменные из кэша: + +- `$GLOBALS['rSettings']`, `$GLOBALS['rServers']`, `$GLOBALS['rBouquets']` +- `$GLOBALS['rBlockedUA']`, `$GLOBALS['rBlockedISP']`, `$GLOBALS['rBlockedIPs']` +- `$GLOBALS['rAllowedIPs']`, `$GLOBALS['rProxies']`, `$GLOBALS['rSegmentSettings']` +- `$GLOBALS['rFFMPEG_CPU']`, `$GLOBALS['rFFMPEG_GPU']`, `$GLOBALS['rFFPROBE']` + +Подключается к базе данных/Redis на основе `$rSettings['redis_handler']`. + +> **Важно:** Путь к потоковой передаче считывается исключительно из файлового кэша. При обычной работе программа не запрашивает настройки в базе данных или запросы пользователей. + +--- + +## Аутентификация по токену + +Файл: `src/Streaming/Auth/StreamAuthMiddleware.php` + +```php +StreamAuthMiddleware::decryptToken($rToken, $rSettings, $rServers, $rIP): array +``` + +Содержимое токена: + +|Поле|Описание| +| --- | --- | +| `username` |Имя пользователя строки| +| `password` |Пароль к строке| +| `stream_id` |Идентификатор целевого потока| +| `expires` |Временная метка истечения срока действия токена| +| `channel_info` |Потоковые метаданные (on_demand, прокси, pid)| +| `user_info` |Разрешения пользователя (max_connections, is_restreamer)| +| `country_code` |GeoIP код страны| +| `video_codec` |Запрашиваемый видеокодек| + +Утверждение: + +1. Расшифруйте токен, используя `live_streaming_pass`. +2. Проверьте срок действия: `$rTokenData['expires'] < time() - $rServers[SERVER_ID]['time_offset']`. +3. Возвращает проанализированные данные токена или вызывает ошибку. + +Заголовки ответов задаются через `StreamAuthMiddleware::sendStreamHeaders()`: + +```text +Access-Control-Allow-Origin: * +X-XSS-Protection: 0 +X-Content-Type-Options: nosniff +Alt-Svc: h3-29, h3-T051, h3-Q050 (HTTP/3 hints) +``` + +--- + +## Потоковая доставка + +### Жить (live.php) + +Основная конечная точка доставки (~650 строк): + +1. Расшифруйте токен с помощью `StreamAuthMiddleware::decryptToken()`. +2. Разрешить работу сервера/прокси-сервера: `StreamAuth::checkAccess()` + `ProxySelector::availableProxy()`. +3. Установите ограничения на подключение: `StreamAuth::validateConnections()`. +4. Создайте запись о подключении: `ConnectionTracker::createConnection()`. +5. Передача вручную демону **`xc_fanout` (смотрите ниже): PHP выдает сообщение +`X-Accel-Redirect` и завершает байтовый путь — nginx передает байты в потоковом режиме. + - **ТС:** `X-Accel-Redirect: /xc_fanout/?c=&prebuffer=N` (nginx +перезаписывается в файл демона `/live/`). + - **HLS:** список воспроизведения указывает на выделенные сегменты; `segment.php` транслируется в прямом эфире +сегментирует только через демон (`/xc_fanout_hls/_`), иначе `404`. +6. При выходе: `ShutdownHandler::handle()` → закрыть запись подключения. + +### VOD (vod.php) + +Тот же процесс аутентификации, что и в live. Считывается из `VOD_PATH` вместо `STREAMS_PATH`. + +### Временной сдвиг (timeshift.php) + +Обслуживает архивные сегменты. Использует `TimeshiftClient` для разрешения архивного файла. + +### Демон доставки — `xc_fanout` + +Оперативная доставка клиентов (TS **и** HLS) доступна ** только для демонов **: PHP авторизует +средство просмотра, а затем полностью покидает байтовый путь, так что средство просмотра больше не закрепляет +PHP-FPM работник, отвечающий за жизнь потока. + +- **Разветвление.** `xc_fanout` (встроенный демон Go) извлекает каждый источник ** один раз** и +он доступен каждому пользователю через сокет unix с помощью встроенного в оперативную память сегментатора HLS. +PHP отсутствует в байтовом пути для каждого зрителя; старый цикл поиска и чтения +(`AsyncFileOperations::awaitFileExists()`) и `HLSGenerator::generateHLS()` +сервировочные дорожки были удалены при разделке. +- **Два сокета.** Клиентский сокет (ориентированный на nginx) обслуживает `/live/` и +`/hls/...`; управляющий сокет, предназначенный только для PHP, регистрирует исходные коды +(`PUT /streams/` / `/ingest/`), отвечает на вопросы о статусе выхода в эфир +(`GET /streams/`, `GET /probe/`) и предоставляет доступ к телеметрии. +- **Телеметрия / сверка.** `fanout_sync` опросов `GET /rates` (для каждого uuid +КБИТ/с → `lines_divergence`) и `GET /connections` (согласовывает `lines_live` +строк, поскольку PHP не может видеть разъединение в `X-Accel`). +- **Отключен.** Если демон сообщает об отсутствии данных (`has_data=false` / устаревшие), PHP +показывает страницу "не в эфире" вместо того, чтобы позволить зрителю зависнуть. +- **Сохранено на диске HLS** только для timeshift / миниатюр / `.analyse` / +`MonitorCommand` — не для доставки клиенту. + +#### Наложение отправленного сообщения + +Действие администратора "Отправить сообщение" отображает текстовый баннер на видео **одного** зрителя. +PHP отправляет его в сокет управления демоном +(`FanoutClient::sendSignal` → `POST /signal/`), и демон применяет +ffmpeg `drawtext` наложение на следующий сегмент этого просмотра HLS (или короткий фрагмент ~5 секунд). +окно), однократный запуск, максимальное усилие - сигнал никогда не прерывает воспроизведение. Демон должен +запускается с помощью ffmpeg, который на самом деле имеет фильтр `drawtext`, так что +`service` launcher выбирает сборку с поддержкой drawtext. + +--- + +## Управление подключениями + +### Средство отслеживания подключений + +Управляет текущим состоянием соединения. Серверная часть выбрана с помощью `$rSettings['redis_handler']`: + +**Redis (предпочтительно для масштаба):** + +- Соединения, хранящиеся в отсортированных наборах: + - `LINE#{identity}` — подключения для пользователя + - `STREAM#{stream_id}` — соединения для потоковой передачи + - `SERVER#{server_id}` — соединения на сервере + +**MySQL (резервный вариант):** + +- Таблица: `lines_live` с полями: `activity_id`, `user_id`, `stream_id`, `server_id`, `uuid`, `pid`, `hls_end` + +Ключевые методы: + +```php +ConnectionTracker::createConnection($data) +ConnectionTracker::updateConnection($connection, $changes, 'open'|'close') +ConnectionTracker::getConnection($uuid) +ConnectionTracker::getLineConnections($user_id) +ConnectionTracker::getCapacity() +``` + +### Ограничитель подключения + +Файл: `src/Streaming/Protection/ConnectionLimiter.php` + +Устанавливает ограничения на подключение для каждого пользователя при превышении значения `max_connections`: + +|Приоритет|Критерий|Действие| +| --- | --- | --- | +|2|Тот же IP + тот же пользовательский агент|Убей первым| +|1|Тот же IP-адрес (любой UA)|Убей следующего| +|0|Какая-либо связь|Убить в качестве запасного варианта| + +Настройки: + +- `disallow_2nd_ip_con` — принудительно использовать один IP-адрес для каждого пользователя +- `ip_subnet_match` — соответствует подсети /24 вместо точного IP-адреса +- `restrict_same_ip` — возвращает ошибку при несоответствии IP-адресов вместо уничтожения + +### Устройство для выключения + +Файл: `src/Streaming/Lifecycle/ShutdownHandler.php` + +Зарегистрирован через `register_shutdown_function()`. На выходе из PHP процесса: + +1. Закройте запись о подключении с помощью `lines_live` или Redis. +2. Удалите tmp-файлы по адресу `CONS_TMP_PATH . $uuid`. +3. Удалите поток по требованию из очереди, если это применимо. + +--- + +## балансировка нагрузки + +### Выбор сервера (StreamAuth::checkAccess) + +Файл: `src/Streaming/Auth/StreamAuth.php` + +```php +public static function checkAccess($rUserInfo, $rUserIP, $rCountryCode, $rUserISP = ''): int|false +``` + +Алгоритм: + +1. Получите доступные серверы: `server_online == true`, `server_type == 0`, `online_clients < total_clients`. +2. Сортировка по вместимости (по возрастанию) — сначала загружается наименее загруженный. +3. Применить маршрутизацию GeoIP (если `enable_geoip == 1`): + - Точное соответствие стране → выберите немедленно. + - `geoip_type == 'strict'` → исключить несоответствие. + - В противном случае → присвоить приоритетный вес. +4. Примените маршрутизацию через интернет-провайдера (если `enable_isp == 1`): та же логика, что и GeoIP. +5. Верните сервер с наименьшей пропускной способностью из группы с наивысшим приоритетом. + +### Выбор прокси-сервера (ProxySelector::Доступный прокси) + +Файл: `src/Streaming/Balancer/ProxySelector.php` + +```php +public static function availableProxy($rProxies, $rCountryCode, $rUserISP = ''): int|null +``` + +Тот же алгоритм, что и `StreamAuth::checkAccess()`, но примененный к списку прокси-серверов. + +--- + +## Ограничение скорости и защита от наводнений + +Три слоя: + +### 1. nginx (уровень подключения) + +```nginx +limit_req_zone $binary_remote_addr zone=one:30m rate=20r/s; +limit_req zone=one burst=8; +``` + +20 запросов в секунду на IP-адрес с пакетом из 8 запросов. 30-минутное скользящее окно. + +### 2. StreamingRequestBootstrap (IP-блокировка) + +```php +if (file_exists(FLOOD_TMP_PATH . 'block_' . $rIP)) { + http_response_code(403); + exit(); +} +``` + +IP-блокировка на основе файлов. Файлы блоков создаются с помощью вышестоящей логики обнаружения наводнений. + +### 3. Ограничитель подключений (для каждого пользователя) + +Применяется после проверки токена. Ограничивает одновременные потоки для каждого пользователя на основе `max_connections`. + +--- + +## HLS Шифрование + +Файл: `src/Streaming/Delivery/HLSGenerator.php` + +```php +public static function generateHLS($rSettings, $rM3U8, $rUsername, $rPassword, + $rStreamID, $rUUID, $rIP, ...): string|false +``` + +Когда `encrypt_hls == true`: + +1. Сгенерируйте ключевой токен AES-128 из IP + StreamID + salt. +2. Замените IV содержимым из `STREAMS_PATH . $rStreamID . '_.iv'`. +3. Зашифруйте ссылку на каждый сегмент: `IP/StreamID/Segment/UUID/SERVER_ID/VideoCodec/OnDemand`. +4. Замените названия сегментов на `/hls/{encrypted_token}`. + +Доставка ключей происходит через `key.php` с использованием того же механизма токенов. + +--- + +## Представление + +Ключевые проектные решения, касающиеся пропускной способности и задержки: + +|Особенность|Механизм| +| --- | --- | +|Ожидание неблокирующего файла|`AsyncFileOperations::awaitFileExists()` использует inotify (Linux) или оптимизированный опрос| +|Нулевой режим работы процессора|`time_nanosleep()` через `AsyncFileOperations::efficientSleep()`| +|nginx буферизация|128 буферов по 32 КБАЙТ на запрос| +|Объединение подключений в пул|Redis (предпочтительно) или постоянный MySQL| +|Чтение только из кэша|Настройки и пользовательские данные считываются из файлового кэша без запросов к базе данных| +|Ранний выход|Отслеживает `connection_status()` каждые 5 секунд, чтобы обнаружить отключение клиента| +|Обновление настроек|Каждые 5 минут (300 секунд) для отслеживания изменений конфигурации без перезапуска| + +--- + +## Пути к файловой системе + +```text +STREAMS_PATH = /home/xc_vm/www/stream/ +CONS_TMP_PATH = /home/xc_vm/tmp/ +CACHE_TMP_PATH = /home/xc_vm/tmp/cache/ +FLOOD_TMP_PATH = /home/xc_vm/tmp/flood/ +SIGNALS_PATH = /home/xc_vm/tmp/signals/ +VIDEO_PATH = /home/xc_vm/www/video/ +ARCHIVE_PATH = /home/xc_vm/www/archive/ +VOD_PATH = /home/xc_vm/www/vod/ +``` + +--- + +## Диагностика и оснастка + +Два инструмента проверяют правильность доставки потока — что сегменты поступают по порядку +и очередь на доставку не прерывается. + +### `tools/stream_queue_check.py` (только для Python, stdlib) + +Автономный монитор **целостности сегмента/очереди пакетов** с дополнительным ** функцией live +панель управления буфером**. Автоматически определяет HLS против MPEG-TS. + +```bash +python3 tools/stream_queue_check.py "" --duration 30 # batch check +python3 tools/stream_queue_check.py "" --json # cron / monitoring +python3 tools/stream_queue_check.py "" --live --duration 0 # live dashboard +``` + +Что означает "неповрежденная очередь" для каждого типа потока: + +|Течение|Проверка очереди| +| --- | --- | +|HLS (`.m3u8`)|`EXT-X-MEDIA-SEQUENCE` монотонный и непрерывный (без удаленных или перемотанных сегментов), без `EXT-X-DISCONTINUITY`, каждый вновь появляющийся сегмент доступен для загрузки. Основные плейлисты отображаются в их первом варианте.| +|MPEG-TS (`.ts`, `/play//ts`)|per-PID `continuity_counter` (потерянные / дублированные / переупорядоченные пакеты = разрыв очереди), потеря байта синхронизации, индикатор транспортной ошибки и остановка доставки.| + +Основные параметры: + +|Флаг|Цель| +| --- | --- | +| `--duration N` |секунды для наблюдения (`0` = до нажатия Ctrl-C в `--live`)| +| `--tolerance N` |разрешить N временных разрывов очереди, прежде чем сообщать о `BROKEN` (игнорирует редкие сбои в источнике, передаваемые `-c copy`)| +| `--stall-timeout S` |перерыв в доставке засчитывается как задержка; не превышайте продолжительность сегмента (по умолчанию 15).| +| `--live` |цветная приборная панель TUI (внизу)| +|`--prebuffer S` / `--buffer-target S`|live: предварительный буфер для виртуального игрока и масштаб буферного графика| +|`--json` / `--no-color`|машинный вывод / отключение ANSI| + +Код выхода: `0` исправен, `2` проблема с очередью или задержка, `1` использование. + +#### Оперативная панель мониторинга (`--live`) + +Моделирует виртуального проигрывателя: проигрыватель перемещается со скоростью настенных часов, в то время как содержимое +"получено". Для **TS** полученная временная шкала берется из **PCR** (часы потока).; +для **HLS** из длительностей сегментов `EXTINF`. Буферизованное время воспроизведения ("кэш") = +получено − воспроизведено; если значение достигает нуля, то начало воспроизведения зависает (событие отмены буферизации). + +```text + STREAM QUEUE / BUFFER MONITOR TS up 00:22 + cache buffer (s), last 60s: + ▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▄▄▄▇▇▇▆▆▆▅▅▅▄▄▇▇▇▆▆▆▅ <- burst-then-drain = delivery sawtooth + IN CACHE : [█████████████████░░░░░░░░░░░░░] 11.6s / 20s + PLAYING : PLAYING head 00:18 received 00:29 + rate 1000 kbit/s received 4.1 MB last data 7.0s ago + QUEUE OK cc:0 sync:0 gaps:0 disc:0 rebuffers:0 +``` + +График буфера и индикатор окрашены в зеленый (исправный) / желтый (низкий) / красный цвета +(голодает). Для HLS строка блоков показывает сегменты, которые все еще находятся в кэше перед началом +плейхед. + +### `console.php stream:check` (PHP, компаньон) + +Проверяет URL-адрес источника и с помощью `--decode` извлекает и декодирует медиафайл для перехвата +неработающие/поврежденные сегменты. HLS проверяется по сегментам; значение TS для одного сокета +конечная точка фиксируется с помощью cURL и декодируется в автономном режиме (ffmpeg в режиме реального времени `-i` зависает +it). Источник: `src/Cli/Commands/StreamCheckCommand.php`. + +```bash +console.php stream:check "" # metadata probe (type, codecs) +console.php stream:check "" --decode=30 --json +``` + +> **Примечание — темп доставки.** Цикл доставки TS в режиме реального времени в `live.php` истощается +> доступные данные без регулирования и приостанавливаются только при достижении ffmpeg's +> заголовок записи. Более ранняя версия отключалась на одну секунду после каждого чтения, ограничивая +> пропускная способность на уровне `read_buffer_size` в секунду и голодающие клиенты; +> `stream_queue_check.py --live` визуализирует результирующее поведение буфера. + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Streaming/StreamingBootstrap.php` |основной загрузчик потоковой передачи| +| `src/Infrastructure/Bootstrap/StreamingRequestBootstrap.php` |Инициализация на уровне HTTP| +| `src/Streaming/Auth/StreamAuth.php` |выбор сервера и проверка подключения| +| `src/Streaming/Auth/StreamAuthMiddleware.php` |расшифровка токенов и заголовки ответов| +| `src/Streaming/Balancer/ProxySelector.php` |выбор прокси-сервера| +| `src/Streaming/Protection/ConnectionLimiter.php` |ограничения на подключение для каждого пользователя| +| `src/Streaming/Delivery/HLSGenerator.php` |Генерация плейлиста M3U8| +| `src/Streaming/Delivery/StreamRedirector.php` |доступность потока и маршрутизация сервера| +| `src/Streaming/AsyncFileOperations.php` |неблокирующие утилиты для файловой системы| +| `src/Streaming/Lifecycle/ShutdownHandler.php` |очистка соединения при выходе| +| `src/Domain/Stream/ConnectionTracker.php` |состояние соединения в Redis/MySQL| +| `src/Core/Init/LegacyInitializer.php` |настройка глобальной переменной для потоковой передачи| +| `src/Cli/Commands/StreamCheckCommand.php` |`stream:check` — проверка/декодирование потока на наличие фрагментарных сегментов| +| `tools/stream_queue_check.py` |мониторинг целостности очереди + панель мониторинга динамического буфера| diff --git a/docs/ru/guides/authentication-and-sessions.md b/docs/ru/guides/authentication-and-sessions.md new file mode 100644 index 00000000..c10bf8b6 --- /dev/null +++ b/docs/ru/guides/authentication-and-sessions.md @@ -0,0 +1,422 @@ +# Аутентификация и сеансы + +XC_VM поддерживает три контекста аутентификации - администратора, реселлера и игрока - каждый с изолированными сеансовыми ключами, отдельными потоками входа в систему и независимой логикой проверки. В этом документе описан полный жизненный цикл аутентификации от входа в систему до проверки сеанса и обеспечения безопасности. + +--- + +## Обзор процесса входа в систему + +Все три контекста следуют схожему шаблону высокого уровня, с зависящими от контекста различиями в проверке и хранении сеанса. + +### Взаимодействие администратора и реселлера + +```text +POST request with credentials + -> BruteforceGuard checks (flood / brute-force) + -> Optional reCAPTCHA verification + -> Credential lookup via UserRepository::getAuthUserByCredentials() + -> Access code / group validation + -> Permission check (is_admin or is_reseller) + -> User status check (enabled/disabled) + -> Password re-hash + session write + login log +``` + +### Поток игроков + +```text +POST request with credentials + -> UserRepository::getUserInfo() lookup + -> Line type checks (reject E2, MAG, Stalker) + -> Expiration date check + -> admin_enabled / enabled status checks + -> IP allowlist, country restriction, user agent, ISP checks + -> Session write + redirect + -> BruteforceGuard::checkFlood() on any failure +``` + +--- + +## Аутентификатор + +Файл: `src/Core/Auth/Authenticator.php` + +### `Authenticator::login(array $data, bool $bypassRecaptcha = false): array` + +Способ входа в систему администратора. Шаги по порядку: + +1. Проверка повторной проверки (если параметр `recaptcha_enable` включен и не пропущен). +2. Поиск учетных данных с помощью `UserRepository::getAuthUserByCredentials()`. +3. Проверка группы кодов доступа - имя пользователя `member_group_id` должно входить в разрешенные группы текущего кода доступа, в противном случае кодов доступа не должно существовать. +4. Проверка прав доступа - `is_admin` должно иметь значение true для группы пользователя. +5. Проверка состояния - `$rUserInfo['status'] == 1` (включено). +6. В случае успеха: повторно хэширует пароль, обновляет `last_login` и `ip` в базе данных, записывает сеансовые ключи, регистрирует логин. + +Значения сеанса, записанные в зависимости от успеха: + +```php +$_SESSION['hash'] = $rUserInfo['id']; // User ID +$_SESSION['ip'] = $rIP; // Client IP at login +$_SESSION['code'] = AuthRepository::getCurrentCode(); // Current access code +$_SESSION['verify'] = md5($rUserInfo['username'] . '||' . $rCrypt); // Verification hash +``` + +### `Authenticator::resellerLogin(array $data): array` + +Метод входа в систему реселлера. Структура идентична `login()` с некоторыми отличиями: + +- Повторная проверка всегда проверяется, когда она включена (параметр обхода отсутствует). +- Для проверки разрешений требуется `is_reseller` вместо `is_admin`. +- Возвращает `STATUS_NOT_RESELLER`, если у пользователя нет разрешения реселлера. +- Журналы входа в систему записываются с типом `RESELLER` вместо `ADMIN`. + +Значения сеанса, записанные в зависимости от успеха: + +```php +$_SESSION['reseller'] = $rUserInfo['id']; // User ID +$_SESSION['rip'] = $rIP; // Client IP at login +$_SESSION['rcode'] = AuthRepository::getCurrentCode(); // Current access code +$_SESSION['rverify'] = md5($rUserInfo['username'] . '||' . $rCrypt); // Verification hash +``` + +### Константы статуса входа в систему + +Определено в `src/bootstrap.php` через `XC_Bootstrap::defineStatusConstants()`: + +|Постоянный|Ценность|Значение| +| --- | --- | --- | +| `STATUS_FAILURE` |0|Общий сбой (неверные учетные данные или универсальная ошибка)| +| `STATUS_SUCCESS` |1|Вход в систему был успешным| +| `STATUS_DISABLED` |5|Учетная запись отключена| +| `STATUS_NOT_ADMIN` |6|У пользователя нет прав администратора| +| `STATUS_INVALID_CAPTCHA` |12|Не удалось выполнить проверку reCAPTCHA| +| `STATUS_INVALID_CODE` |13|Несоответствие кода доступа/ группы| +| `STATUS_NOT_RESELLER` |35|У пользователя нет разрешения торгового посредника| + +### Хэширование паролей + +```php +Authenticator::hashPassword(string $password, ?string $salt = null, int $rounds = 20000): string +``` + +Использует `crypt()` с SHA-512 (`$6$`). Формат salt - это `$6$rounds=20000$$`, где `` - это 16 шестнадцатеричных символов, производных от `openssl_random_pseudo_bytes(16)`. Пароли повторно хэшируются при каждом успешном входе в систему, что меняет значение. + +```php +Authenticator::checkPassword(string $password, string $storedHash): string +``` + +Проверяет пароль в виде открытого текста на соответствие сохраненному хэшу, используя `crypt($password, $storedHash)`, с возможностью сравнения по времени с помощью `hash_equals()`. Сохраненный хэш содержит алгоритм, раунды и соль, поэтому `crypt()` воспроизводит правильный хэш для сравнения. + +--- + +## Аутентификация игрока + +Файл: `src/Public/Controllers/Player/PlayerLoginController.php` + +Процедура входа игрока в систему принципиально отличается от процедуры администратора/реселлера. Она аутентифицирует "линии" конечного пользователя (подписки на IPTV), а не операторов панели. + +### Процесс входа в систему + +`PlayerLoginController::processLogin()` выполняет эти проверки в порядке: + +1. **Поиск учетных данных** -- `UserRepository::getUserInfo()` (отличается от `getAuthUserByCredentials`, используемого администратором/реселлером). +2. **Отклонение типа линии** - Линии E2, MAG и Stalker отклоняются с определенными кодами ошибок. +3. **Проверка истечения срока действия** -- `exp_date` должно быть равно null или в будущем. +4. **Проверка с поддержкой администратора ** -- `admin_enabled == 0` возвращает `CLIENT_BANNED`. +5. **Проверка, включенная пользователем ** -- `enabled == 0` возвращает `CLIENT_DISABLED`. +6. **Список разрешенных IP-адресов** -- Если для пользователя задан `allowed_ips`, IP-адрес клиента должен совпадать (разрешен с помощью `gethostbyname`). +7. **Ограничение по стране ** - Два режима: + - Для каждого пользователя: если задано значение `forced_country`, а не `ALL`, то страна GeoIP должна совпадать. + - Глобальный: если нет переопределения для каждого пользователя, проверяется глобальный параметр `allow_countries` (если он не содержит `ALL`). +8. **Проверка агента пользователя** - Если для пользователя задано значение `allowed_ua`, пользовательский агент HTTP должен соответствовать. +9. **Проверка провайдера** -- флаг `isp_violate` отклоняет соединение. +10. **Проверка сервера интернет-провайдера** -- Если `isp_is_server` имеет значение true и пользователь не является рестримером, соединение будет отклонено. + +Каждый сбой запускает `BruteforceGuard::checkFlood()` перед возвратом кода ошибки. + +### Коды ошибок проигрывателя + +|Постоянный|Ценность|Значение| +| --- | --- | --- | +| `CLIENT_INVALID` |0|Неверное имя пользователя или пароль| +| `CLIENT_IS_E2` |1|Линии Enigma запрещены| +| `CLIENT_IS_MAG` |2|МАГНИТНЫЕ линии не допускаются| +| `CLIENT_IS_STALKER` |3|Линии преследования запрещены| +| `CLIENT_EXPIRED` |4|Срок действия строки истек| +| `CLIENT_BANNED` |5|Строка заблокирована (admin_enabled = 0)| +| `CLIENT_DISABLED` |6|Линия отключена (включено = 0)| +| `CLIENT_DISALLOWED` |7|Не удалось выполнить проверку IP/страны/UA/провайдера| + +### Ключи к сеансу игрока + +```php +$_SESSION['phash'] = $rUserInfo['id']; +$_SESSION['pverify'] = md5($rUserInfo['username'] . '||' . $rUserInfo['password']); +``` + +В контексте player хранятся только два сеансовых ключа. В отличие от ключей администратора и реселлера, здесь нет ключей `activity`, `ip`, или `code`. Это означает, что в сеансе player нет таймаута бездействия и не обнаруживается изменение IP-адреса на уровне сеанса. + +--- + +## Проверка сеанса при загрузке страницы + +После первоначального входа в систему при каждой загрузке страницы, прошедшей проверку подлинности, выполняется повторная проверка сеанса. Это происходит в файлах начальной загрузки, а не в `SessionManager`. + +### Проверка сеанса администратора + +Файл: `src/Public/Views/admin/functions.php` + +Если задано значение `$_SESSION['hash']`, при каждой загрузке страницы выполняются следующие проверки: + +1. **Поиск пользователя** -- `UserRepository::getRegisteredUserById($_SESSION['hash'])`. Если пользователь больше не существует, сеанс завершается. +2. **Проверка разрешений** -- `AuthRepository::getPermissions()` должен возвращать допустимый набор с `is_admin == true`. +3. **Проверка IP-адреса** -- Сравнивает текущий IP-адрес с `$_SESSION['ip']`: + - Если включена настройка `ip_subnet_match`: сравниваются только первые три октета (например, `192.168.1.*` совпадает с `192.168.1.*`). + - Если `ip_subnet_match` отключен: требуется точное совпадение IP-адресов. + - Если IP-адрес не совпадает и включена настройка `ip_logout`, сеанс завершается. + - Если IP-адрес не совпадает и `ip_logout` отключен, `$_SESSION['ip']` автоматически обновляется до нового IP-адреса. +4. **Проверьте хэш-код ** -- `$_SESSION['verify']` должно быть равно `md5($rUserInfo['username'] . '||' . $rUserInfo['password'])`. Это гарантирует, что сеанс будет аннулирован в случае изменения пароля. + +Если какая-либо проверка завершается неудачей, сеанс очищается с помощью `SessionManager::clearContext('admin')` и пользователь перенаправляется на индексную страницу. + +### Проверка сеанса работы с реселлером + +Файл: `src/Infrastructure/Bootstrap/reseller_functions.php` + +Логика идентична проверке администратора, но используются сеансовые ключи реселлера: + +- Проверяет `$_SESSION['reseller']` на наличие идентификатора пользователя. +- Использует `$_SESSION['rip']` для сравнения IP-адресов. +- Использует `$_SESSION['rverify']` для проверки хэша. +- Проверяет разрешение `is_reseller` вместо `is_admin`. + +Соответствие IP-подсети и поведение при выходе из системы по IP-адресу такое же, как у администратора. + +### Тайм-аут сеанса администрирования + +Файл: `src/Public/Views/admin/session.php` + +Для сеансов администрирования выполняется отдельная проверка тайм-аута сеанса. Если заданы значения `$_SESSION['hash']` и `$_SESSION['last_activity']`, а с момента `last_activity` прошло более 60 минут, ключи сеанса (`hash`, `ip`, `code`, `verify`, `last_activity`) не заданы. При каждом действительном запросе `$_SESSION['last_activity']` обновляется, и сеанс закрывается для записи. + +### Проверка сеанса игрока + +Контекст проигрывателя не выполняет проверку IP-адреса, соответствие подсети или тайм-аут активности на уровне сеанса. Сохраняются только `phash` и `pverify`@, и проверка выполняется на прикладном уровне для повторной проверки этих значений по базе данных. + +--- + +## Менеджер сеанса + +Файл: `src/Core/Auth/SessionManager.php` + +Унифицированный сеансовый API, который абстрагирует различные имена сеансовых ключей в разных контекстах. Предназначен для замены устаревших файлов `admin/session.php` и `reseller/session.php`. + +### Контекстная ключевая карта + +|Логический ключ|Администратор `$_SESSION` ключ|Реселлер `$_SESSION` ключ|Игрок `$_SESSION` ключ| +| --- | --- | --- | --- | +| `auth` | `hash` | `reseller` | `phash` | +| `activity` | `last_activity` | `rlast_activity` |--| +| `ip` | `ip` | `rip` |--| +| `code` | `code` | `rcode` |--| +| `verify` | `verify` | `rverify` | `pverify` | + +Контекст игрока намеренно не содержит сопоставлений `activity`, `ip`, и `code`. + +### Методы + +**`start(string $context, int $timeout = 60): void`** + +Запускает сеанс PHP (если он еще не запущен), устанавливает активный контекст и запускает `checkTimeout()` для завершения устаревших сеансов. Контекст должен быть `'admin'`, `'reseller'`, или `'player'`. + +**`requireAuth(?string $loginUrl = null): void`** + +Проверяет наличие аутентифицированного сеанса. Если запрос направлен напрямую `session.php`, возвращает ответ в формате JSON `{"result": true/false}` (используется для опроса сеанса AJAX). В противном случае перенаправляет не прошедших проверку пользователей на страницу входа в систему. В случае успеха вызывает `touch()`, чтобы обновить временную метку действия. + +**`isAuthenticated(): bool`** + +Проверка на отсутствие блокировки. Возвращает значение `true`, если сеанс был запущен и установлен ключ `auth`. + +**`getUser(): ?string`** + +Возвращает значение, хранящееся в ключе сеанса `auth` (идентификатор пользователя для администратора/реселлера или идентификатор строки для игрока), или `null`, если не пройдена проверка подлинности. + +**`getValue(string $name): mixed`** + +Возвращает значение сеанса по его логическому имени (`auth`, `activity`, `ip`, `code`, `verify`). Логическое имя сопоставляется с фактическим ключом `$_SESSION` в зависимости от текущего контекста. + +**`setValue(string $name, mixed $value): void`** + +Устанавливает значение сеанса по логическому имени. + +**`login(string $hash, ?string $ip = null): void`** + +Создает аутентифицированный сеанс, задавая значения `auth` и `activity`. При необходимости сохраняет IP-адрес клиента. + +**`destroy(): void`** + +Очищает все ключи сеанса для текущего контекста. Если никакой другой контекст не активен (проверяет противоположный контекст администратора/реселлера), уничтожает весь сеанс PHP. + +**`clearContext(string $context): void`** + +Очищает все ключи сеанса для определенного контекста, не разрушая сеанс. Простая замена устаревшего `destroySession($type)`. + +**`touch(): void`** + +Обновляет `$_SESSION[$activityKey]` до текущей временной метки и вызывает `session_write_close()`, чтобы снять блокировку сеанса. + +**`getContext(): ?string`** + +Возвращает текущую строку контекста (`'admin'`, `'reseller'`, `'player'`) или `null`, если значение не задано. + +### Поведение по истечении времени ожидания + +`SessionManager::DEFAULT_TIMEOUT` равно 60 минутам. Метод `checkTimeout()` (автоматически вызываемый `start()`) сравнивает время, прошедшее с момента `last_activity`. Если тайм-аут превышен, все ключи сеанса, зависящие от контекста, сбрасываются, что фактически приводит к выходу пользователя из системы. + +Поскольку контекст игрока не содержит ключа `activity` в карте ключей, проверка тайм-аута не применяется к сессиям игрока. + +--- + +## Охранник с применением грубой силы + +Файл: `src/Core/Auth/BruteforceGuard.php` + +Централизованное ограничение скорости и защита от перебора. Все методы используют файловое состояние, хранящееся по адресу `FLOOD_TMP_PATH` (`/home/xc_vm/tmp/flood/`). Разрешенные IP-адреса (IP-адреса сервера) и IP-адреса, указанные в параметре `flood_ips_exclude`, всегда исключаются. + +### `checkFlood(?string $ip = null, bool $useCachedMode = false): null` + +Скорость - ограничивает количество запросов по IP-адресу в пределах настраиваемого временного интервала. + +- **Настройки:** `flood_limit` (максимальное количество запросов), `flood_seconds` (размер окна). +- **Файл состояния:** `FLOOD_TMP_PATH . $ip` - хранит объект JSON с `requests` count и `last_request` timestamp. +- **Поведение:** Отслеживает количество запросов в пределах временного интервала. Если количество превышает `flood_limit`, IP-адрес блокируется (заносится в таблицу `blocked_ips` или передается через Redis в режиме кэширования/потоковой передачи). Файл состояния удаляется после блокировки. +- ** Используется: ** Логином игрока (вызывается при каждой неудачной попытке входа в систему), конечными точками потоковой передачи. + +### `checkBruteforce(?string $ip = null, ?string $mac = null, ?string $username = null, bool $useCachedMode = false): null` + +Обнаруживает атаки методом перебора на основе количества уникальных MAC-адресов или имен пользователей, обнаруженных с одного IP-адреса. + +- **Настройки:** `bruteforce_mac_attempts`, `bruteforce_username_attempts` (максимальное количество уникальных значений), `bruteforce_frequency` (временной интервал в секундах). +- **Файл состояния:** `FLOOD_TMP_PATH . $ip . '_mac'` или `FLOOD_TMP_PATH . $ip . '_user'` - сохраняет попытки в виде пар `{term: timestamp}`. +- ** Поведение:** Попытки с истекшим сроком действия (за пределами частотного диапазона) отсекаются с помощью `truncateAttempts()`. Если количество уникальных запросов превышает допустимое, IP-адрес блокируется. +- **Используется:** Конечными точками потоковой аутентификации. + +### `checkAuthFlood(array $user, ?string $ip = null): null` + +Ограничивает скорость запросов на аутентификацию для конкретной комбинации пользователь +IP. Предназначен для ограничения повторных попыток авторизации без полной блокировки. + +- ** Настройки:** `auth_flood_limit` (максимальное количество попыток), `auth_flood_seconds` (окно), `auth_flood_sleep` (задержка в секундах при блокировке). +- **Файл состояния:** `FLOOD_TMP_PATH . $userId . '_' . $ip` - сохраняет попытки в виде индексированных временных меток плюс необязательную временную метку `block_until`. +- ** Поведение:** Когда количество попыток превышает допустимое значение, устанавливается временная метка `block_until`. Последующие запросы в течение периода блокировки задерживаются на `auth_flood_sleep` секунд (через `sleep()`). IP-адрес не блокируется навсегда. Пользователи Restreamer (`is_restreamer`) освобождаются от ответственности. +- **Используется:** Потоковой аутентификацией. + +### `truncateAttempts(array $attempts, int $frequency, bool $list = false): array` + +Отфильтровывает просроченные попытки из массива отслеживания. Когда `$list` равно `true`, массив обрабатывается как индексированный (для `checkAuthFlood`); в противном случае как ассоциативный, определяемый по термину (для `checkBruteforce`). + +### Блокирующий механизм + +Когда IP-адрес заблокирован: + +- **Обычный режим:** Выполняет вставку в таблицу базы данных `blocked_ips` с указанием причины (`FLOOD ATTACK` или `BRUTEFORCE MAC/USER ATTACK`) и обновляет кэш `BlocklistService`. +- **Режим кэширования/потоковой передачи (`$useCachedMode = true`):** Устанавливает сигнал Redis (`bruteforce_attack/$ip` или `flood_attack/$ip`) через `RedisManager::setSignal()` для блокировки контекста потоковой передачи без записи в базу данных. +- В обоих режимах выполняется касание файла-маркера `FLOOD_TMP_PATH . 'block_' . $ip` для быстрой проверки на уровне файловой системы. + +--- + +## Безопасность сеанса + +### Настройка файлов cookie + +В контексте начальной загрузки администратора сеансовые файлы cookie устанавливаются в соответствии со строгой политикой SameSite: + +```php +$params['samesite'] = 'Strict'; +session_set_cookie_params($params); +session_start(); +``` + +### Проверка хэша + +Проверяющий хэш (`$_SESSION['verify']` / `$_SESSION['rverify']` / `$_SESSION['pverify']`) вычисляется следующим образом: + +```php +md5($username . '||' . $hashedPassword) +``` + +Это значение сравнивается с текущими значениями базы данных при каждой загрузке страницы. Если администратор изменяет пароль пользователя (что изменяет сохраненный хэш), все существующие сеансы для этого пользователя автоматически становятся недействительными, поскольку проверяемый хэш больше не будет совпадать. + +Для входа в систему администратора и торгового посредника пароль повторно хэшируется при входе в систему, поэтому используется `$rCrypt` (новый хэш). Для входа в систему игроков используется существующий сохраненный хэш `$rUserInfo['password']`. + +### Обработка изменений IP-адреса + +Два параметра управляют поведением при изменении IP-адреса: + +|Установка|Эффект| +| --- | --- | +| `ip_logout` |Когда этот параметр включен, сеанс завершается при изменении IP-адреса клиента (точного или из подсети, в зависимости от `ip_subnet_match`).| +| `ip_subnet_match` |Когда этот параметр включен, сравниваются только первые три октета IP-адреса, вместо того чтобы требовать точного совпадения. Позволяет пользователям с динамическими IP-адресами в пределах одной подсети сохранять сеанс связи.| + +Когда `ip_logout` отключен и IP-адрес изменяется, сохраненный IP-адрес сеанса автоматически обновляется до нового IP-адреса. + +--- + +## Регистрация входа в систему + +Когда параметр `save_login_logs` включен, все попытки входа в систему (успешные и неудачные) записываются в таблицу `login_logs`: + +```sql +INSERT INTO `login_logs`(`type`, `access_code`, `user_id`, `status`, `login_ip`, `date`) +VALUES($type, $codeId, $userId, $status, $ip, $timestamp); +``` + +|Колонка|Описание| +| --- | --- | +| `type` |`ADMIN` или `RESELLER`| +| `access_code` |Идентификатор текущего кода доступа| +| `user_id` |Идентификатор пользователя (0 для неверных учетных данных)| +| `status` |`SUCCESS`, `INVALID_LOGIN`, `INVALID_CODE`, `NOT_ADMIN`, `DISABLED`| +| `login_ip` |IP-адрес клиента| +| `date` |Временная метка Unix| + +Логины игроков не записываются на `login_logs`. + +--- + +## Авторизация (после входа в систему) + +После аутентификации два дополнительных уровня авторизации определяют, к чему пользователь может получить доступ: + +### `Authorization` + +Файл: `src/Core/Auth/Authorization.php` + +Авторизация на уровне объекта. Проверяет, есть ли у текущего пользователя разрешение на доступ к определенному ресурсу (пользователь, поток и т.д.) на основе иерархии владельцев реселлеров и групповых разрешений. + +- `Authorization::hasResellerPermissions($type)` -- устанавливает флажок единственного разрешения для `$rPermissions`. +- `Authorization::check($type, $id)` -- проверяет доступ к определенному ресурсу по типу и идентификатору. + +### `PageAuthorization` + +Файл: `src/Core/Auth/PageAuthorization.php` + +Управление доступом на уровне страницы. Определяет, разрешают ли групповые права текущего пользователя доступ к определенной странице панели администратора или торгового посредника. + +- `PageAuthorization::checkResellerPermissions($page)` -- сопоставляет названия страниц с флагами требуемых разрешений и возвращает, разрешен ли доступ. + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Core/Auth/Authenticator.php` |Логика входа администратора и реселлера в систему, хэширование паролей| +| `src/Core/Auth/SessionManager.php` |Унифицированный сеансовый API с сопоставлением контекстных ключей| +| `src/Core/Auth/BruteforceGuard.php` |Ограничение скорости и защита от перебора| +| `src/Core/Auth/Authorization.php` |Проверки авторизации на уровне объекта| +| `src/Core/Auth/PageAuthorization.php` |Управление доступом на уровне страницы| +| `src/Public/Controllers/Player/PlayerLoginController.php` |Процесс входа игрока в систему с проверкой безопасности| +| `src/Public/Views/admin/functions.php` |Проверка сеанса администратора при каждой загрузке страницы| +| `src/Public/Views/admin/session.php` |Тайм-аут сеанса администратора и проверка сеанса AJAX| +| `src/Infrastructure/Bootstrap/reseller_functions.php` |Проверка сеанса реселлера при каждой загрузке страницы| +| `src/Domain/User/UserRepository.php` |Поиск учетных данных (`getAuthUserByCredentials`)| +| `src/bootstrap.php` |Определения констант состояния, контексты начальной загрузки| +| `src/Core/Config/Paths.php` |`FLOOD_TMP_PATH` определение| diff --git a/docs/ru/guides/cli-tools.md b/docs/ru/guides/cli-tools.md new file mode 100644 index 00000000..babd1ec4 --- /dev/null +++ b/docs/ru/guides/cli-tools.md @@ -0,0 +1,520 @@ +# Инструменты CLI и обновления баз данных + +Справочник по интерфейсу командной строки XC_VM, системным инструментам и процессу обновления базы данных после обновления версии. Охватывает ежедневные операции, экстренный доступ и создание новых этапов обновления базы данных. + +--- + +## Точка входа в консоль + +Все команды CLI выполняются через `console.php`: + +```bash +/home/xc_vm/console.php [args...] +``` + +Консоль поддерживает три типа команд: + +|Тип|Рассчитывать|Описание| +| --- | --- | --- | +|**Команды**|28|Одноразовые операции (обновление, статус, инструменты и т.д.)| +|**Закадычные друзья**|25|Запланированные задачи (автоматически вызываемые crontab)| +|**Демоны**|8|Длительно выполняющиеся фоновые процессы (команды с использованием `DaemonTrait`)| + +> **Примечание:** Демоны - это обычные команды, которые используют `DaemonTrait`. Отдельного каталога `Daemons/` не существует. + +Чтобы просмотреть все доступные команды: + +```bash +/home/xc_vm/console.php list +``` + +--- + +## Полный реестр команд + +### Служебные команды + +|Команда|Класс|Описание|Пользователь| +| --- | --- | --- | --- | +| `status` | `StatusCommand` |Состояние системы, обновления базы данных, проверка конфигурации|корень| +| `update` | `UpdateCommand` |Обновление системы (update / после обновления)|xc_vm| +| `service` | `ServiceCommand` |Управление службой XC_VM: запуск, остановка, перезапуск, перезагрузка|корень| +| `tools` | `ToolsCommand` |Утилиты технического обслуживания (см. раздел команды "Инструменты")|корень/xc_vm| +| `certbot` | `CertbotCommand` |Сгенерируйте SSL-сертификат с помощью certbot|корень| +| `binaries` | `BinariesCommand` |Обновите двоичные файлы и базу данных GeoLite с GitHub|xc_vm| +| `startup` | `StartupCommand` |Инициализация системы: daemons.sh, crontab, кэш|корень| +| `monitor` | `MonitorCommand` |Отслеживайте поток по идентификатору (запуск/перезапуск/отслеживание)|xc_vm| +| `thumbnail` | `ThumbnailCommand` |Создание рамок миниатюр для потока|xc_vm| +| `plex_item` | `PlexItemCommand` |Обработать один элемент Plex (фильм/сериал)|xc_vm| +| `watch_item` | `WatchItemCommand` |Обработать один элемент наблюдения (поиск/обновление в базе данных TMDB)|xc_vm| +| `migrate` | `MigrateCommand` |Передача данных из базы данных `xc_vm_migrate`|xc_vm| +| `db:migrate` | `DbMigrateCommand` |Применить ожидающие переноса базы данных из каталога `migrations/`|xc_vm| +| `server:install` | `ServerInstallCommand` |Установка/настройка сервера (Proxy/LB) через SSH|корень| +| `server:diagnose` | `ServerDiagnoseCommand` |Диагностируйте, почему прокси-узел/LB-узел не подключен к основному (сердцебиение, доступность, iptables, сервис).|корень| + +> Команды, помеченные **необязательно**, условно регистрируются с помощью `file_exists()` guard: `cache_handler`, `server:install`, `migrate`. + +### Команды демона (постоянные процессы) + +Эти команды используют `DaemonTrait` и выполняются непрерывно через циклы `while(true)`: + +|Команда|Класс|Описание| +| --- | --- | --- | +| `signals` | `SignalsCommand` |Обрабатывать сигналы уничтожения/кэширования из базы данных и Redis| +| `watchdog` | `WatchdogCommand` |Мониторинг системы: процессор, подключения, обновления сервера| +| `queue` | `QueueCommand` |Обрабатывать фоновые задачи в очереди| +| `scanner` | `ScannerCommand` |Поиск новых потоков/устройств| +| `cache_handler` | `CacheHandlerCommand` |Обрабатывать операции с кэшем (необязательно)| + +### Команды потоковой обработки + +|Команда|Класс|Описание| +| --- | --- | --- | +| `proxy` | `ProxyCommand` |MPEG-TS потоковое проксирование через сокеты| +| `archive` | `ArchiveCommand` |Телевизионный архив — запись потока на сегменты| +| `created` | `CreatedCommand` |Созданный канал — создание канала из исходных текстов| +| `delay` | `DelayCommand` |Задержка HLS воспроизведения потока| +| `loopback` | `LoopbackCommand` |Получать MPEG-TS с другого сервера| +| `llod` | `LlodCommand` |Потоковый процессор с низкой задержкой по требованию| +| `record` | `RecordCommand` |Запись потока в формате MP4| +| `ondemand` | `OndemandCommand` |Прерывать трансляции без активных зрителей| + +### Задания Cron (всего 26: 22 ядра + 4 модуля) + +Все имена заданий cron имеют префикс `cron:`. Они используют `CronTrait` и вызываются системной crontab. + +**Основные задания cron** (в `src/Cli/CronJobs/`): + +|Команда|Класс|Описание| +| --- | --- | --- | +| `cron:activity` | `ActivityCronJob` |Импорт журналов действий пользователей в базу данных| +| `cron:backups` | `BackupsCronJob` |Управление резервными копиями (необязательно)| +| `cron:cache` | `CacheCronJob` |Управление кэшем| +| `cron:cache_engine` | `CacheEngineCronJob` |Генерировать кэш для строк, потоков, серий, групп (необязательно)| +| `cron:certbot` | `CertbotCronJob` |Обновление SSL-сертификата| +| `cron:cleanup` | `CleanupCronJob` |Очистка временных файлов и журналов| +| `cron:epg` | `EpgCronJob` |EPG загрузка и обработка (необязательно)| +| `cron:errors` | `ErrorsCronJob` |Журналы ошибок процесса| +| `cron:lines_logs` | `LinesLogsCronJob` |Импорт журналов клиентских запросов в базу данных| +| `cron:maxmind` | `MaxMindCronJob` |Обновление баз данных MaxMind GeoIP (только по вторникам; `--force` запускать вручную)| +| `cron:providers` | `ProvidersCronJob` |Поставщики обновлений (необязательно)| +| `cron:root_mysql` | `RootMysqlCronJob` |Обслуживание базы данных (root, необязательно)| +| `cron:root_signals` | `RootSignalsCronJob` |Сигналы обработки, iptables, nginx, управление службами (root)| +| `cron:series` | `SeriesCronJob` |Обновление данных серии (необязательно)| +| `cron:servers` | `ServersCronJob` |Контролируйте сервер, запускайте демонов, обновляйте статистику| +| `cron:stats` | `StatsCronJob` |Вычислять и хранить статистику| +| `cron:streams` | `StreamsCronJob` |Проверка и обновление статуса потока| +| `cron:streams_logs` | `StreamsLogsCronJob` |Импорт журналов потоков| +| `cron:tmp` | `TmpCronJob` |Очистка временных файлов| +| `cron:update` | `UpdateCronJob` |Проверять и применять обновления (необязательно)| +| `cron:users` | `UsersCronJob` |Управление подключениями пользователей, Redis синхронизация, расхождение| +| `cron:vod` | `VodCronJob` |Обрабатывать VOD содержимое| + +**Модуль cron заданий ** (зарегистрирован через `ModuleInterface::registerCommands()`): + +|Команда|Класс|Модуль|Описание| +| --- | --- | --- | --- | +| `cron:plex` | `PlexCronJob` |сплетение|Обрабатывать обновления Plex| +| `cron:tmdb` | `TmdbCronJob` |тмдб|Получение метаданных TMDB (необязательно)| +| `cron:tmdb_popular` | `TmdbPopularCronJob` |тмдб|Выборка популярного содержимого TMDB (необязательно)| +| `cron:watch` | `WatchCronJob` |часы|Обрабатывать обновления библиотеки отслеживания| + +> Необязательные задания cron (зарегистрированные условно): `cron:backups`, `cron:cache_engine`, `cron:epg`, `cron:providers`, `cron:root_mysql`, `cron:series`, `cron:tmdb`, `cron:tmdb_popular`, `cron:update`. + +--- + +## Регистрация новой команды + +Все команды CLI реализуют `CommandInterface`. Основные команды автоматически обнаруживаются из `src/Cli/` через отражение в `console.php`. Команды модуля регистрируются через `ModuleLoader::registerAllCommands()`. + +### Командный интерфейс + +```php +interface CommandInterface { + public function getName(): string; // Unique command name (used in CLI) + public function getDescription(): string; // One-line help text (shown in `list`) + public function execute(array $rArgs): int; // Entry point, returns exit code +} +``` + +### Шаг 1. Создайте класс + +Создайте новый файл в `src/Cli/Commands/` (или `src/Cli/CronJobs/` для заданий cron).: + +```php +register(new MyNewCommand()); + +// Or conditionally (for optional features) +if (file_exists(CLI_PATH . 'Commands/MyNewCommand.php')) { + $rRegistry->register(new MyNewCommand()); +} +``` + +### Шаг 3. Добавить в Makefile (если LB-исключен) + +Если команда не должна включаться в сборки балансировщика нагрузки, добавьте ее путь к `LB_FILES_TO_REMOVE` в поле `Makefile`. + +### Шаг 4. Тестирование + +```bash +# Verify it appears in the list +/home/xc_vm/console.php list + +# Run it +/home/xc_vm/console.php my_command +``` + +--- + +## Команда инструментов + +Команда `tools` предоставляет утилиты для обслуживания системы. + +```bash +/home/xc_vm/console.php tools +``` + +### Подкоманды (запускаются как `root`) + +|Подкомандование|Описание| +| --- | --- | +| `rescue` |Создайте временный код аварийного доступа для доступа к панели экстренной помощи. Введите URL-адрес. **Удалите этот код после использования!**| +| `recaptcha` |Отключите reCAPTCHA (`recaptcha_enable = 0`), чтобы восстановить вход в панель администратора при сбое проверки captcha.| +| `access` |Восстановите все настройки кода доступа nginx и перезагрузите nginx. Печатает URL-адреса для всех кодов панели администратора.| +| `ports` |Восстановите настройки портов nginx (HTTP, HTTPS, RTMP) из базы данных и перезагрузите nginx.| +| `migration` |Очистите промежуточную базу данных (`xc_vm_migrate`) и при необходимости восстановите в ней резервную копию `.sql`.| +| `user` |Создайте пользователя rescue admin со случайными учетными данными. Введите имя пользователя и пароль. **Удалите этого пользователя после использования!**| +| `mysql` |Повторно авторизуйте привилегии MySQL для всех серверов load balancer.| +| `database` |Восстановите пустую базу данных XC_VM из `database.sql`. **Удаляет ВСЕ данные!** Требуется флаг `--confirm`.| +| `flush` |Очистить все заблокированные IP—адреса - очищает правила iptables, удаляет файлы блокировки и обрезает таблицу `blocked_ips`.| + +### Подкоманды (запускаются как `xc_vm`) + +|Подкомандование|Описание| +| --- | --- | +| `images` |Загрузите отсутствующие изображения потоковых передач/фильмов/сериалов из базы данных TMDB. Сканирует базу данных в поисках URL-адресов изображений и загружает отсутствующие файлы.| +| `duplicates` |Найдите и удалите повторяющиеся потоки VOD. Группируйте по идентичному источнику, сначала сохраняйте, остальные удаляйте. **Разрушительный!**| +| `bouquets` |Удалите устаревшие ссылки из букетов. Удаляет идентификаторы, которые больше не существуют в базе данных.| + +### Примеры + +```bash +# Emergency panel access (root) +sudo /home/xc_vm/console.php tools rescue + +# Disable reCAPTCHA to recover admin login (root) +sudo /home/xc_vm/console.php tools recaptcha + +# Regenerate access codes (root) — required after nginx template changes +sudo /home/xc_vm/console.php tools access + +# Regenerate port configuration (root) +sudo /home/xc_vm/console.php tools ports + +# Clear staging database (root) +sudo /home/xc_vm/console.php tools migration + +# Clear staging database and restore a backup (root) +sudo /home/xc_vm/console.php tools migration /path/to/backup.sql + +# Create rescue admin user (root) +sudo /home/xc_vm/console.php tools user + +# Reauthorise MySQL privileges on all servers (root) +sudo /home/xc_vm/console.php tools mysql + +# Restore blank database (root) — DESTRUCTIVE! +sudo /home/xc_vm/console.php tools database --confirm + +# Flush all blocked IPs (root) +sudo /home/xc_vm/console.php tools flush + +# Download missing images (xc_vm) +su - xc_vm -c '/home/xc_vm/console.php tools images' + +# Remove duplicate VOD entries (xc_vm) +su - xc_vm -c '/home/xc_vm/console.php tools duplicates' + +# Clean orphaned bouquet references (xc_vm) +su - xc_vm -c '/home/xc_vm/console.php tools bouquets' +``` + +- ❗️ ** Внимание: ** `duplicates` безвозвратно удаляет трансляции и все связанные с ними данные (журналы, статистику, эпизоды, записи). Всегда создавайте резервную копию перед запуском. +- ❗️ **Внимание: ** `database --confirm` удаляет всю базу данных и заменяет ее пустой схемой. Это необратимо. +- 💡 ** Совет:** После запуска `rescue` всегда удаляйте код через панель администратора или запустив `tools access`, как только вы восстановите доступ. +- 💡 ** Совет:** После запуска `user` немедленно измените пароль и по завершении удалите пользователя для восстановления. + +--- + +## Обновления базы данных после Обновления Версии + +XC_VM использует файловую систему обновления базы данных для управления изменениями схемы между версиями. Обновления базы данных выполняются автоматически во время обновлений и проверок состояния системы. + +### как это работает + +- SQL-файлы для обновлений базы данных хранятся в `/home/xc_vm/migrations/`. + +- Каждому файлу присваивается имя с префиксом последовательного номера, например: + +```text +001_drop_watch_folders_plex_token.sql +002_panel_logs_add_file_env.sql +003_drop_settings_segment_type.sql +``` + +- Применяемые шаги обновления базы данных отслеживаются в таблице `migrations` базы данных. Каждый шаг выполняется **ровно один раз** - если шаг уже был применен, он пропускается. + +- Обновления базы данных выполняются автоматически: + - `console.php update post-update` - после обновления панели + - `console.php status` - во время проверки состояния системы (только на ГЛАВНОМ сервере) + +### Поток выполнения обновления базы данных + +```text +[ MigrationRunner::run() — DB update execution ] + │ + ▼ +[ CREATE TABLE IF NOT EXISTS `migrations` ] + │ + ▼ +[ Read all *.sql files from migrations/ ] + │ + ▼ +[ For each file not in `migrations` table: ] + ├── Execute SQL statements + ├── Record in `migrations` table + └── Output [OK] or [WARN] +``` + +--- + +## Создание нового шага обновления базы данных + +Когда вам нужно изменить схему базы данных (добавить столбцы, создать таблицы, вставить данные и т.д.), создайте новый SQL-файл для этапа обновления базы данных. + +### Шаг 1. Выберите имя файла + +Используйте следующий порядковый номер и описательное название: + +```text +NNN_short_description.sql +``` + +**Правила форматирования:** + +- Префикс номера: 3 цифры, дополненные нулем (например, `006`, `007`) +- Разделитель: символ подчеркивания `_` +- Название: нижний регистр, подчеркивание, описывающее, что делает шаг обновления +- Добавочный номер: `.sql` + +**Примеры:** + +```text +006_add_user_timezone.sql +007_create_audit_log_table.sql +008_insert_default_codec_settings.sql +``` + +### Шаг 2. Напишите SQL-код + +Поместите в файл необработанные инструкции SQL. Несколько инструкций разделяются символами `;`. + +**Правила для этапов обновления базы данных SQL:** + +- **Используйте `IF EXISTS` / `IF NOT EXISTS`**, чтобы сделать шаги обновления базы данных идемпотентными: + +```sql +-- Adding a column (safe) +ALTER TABLE `settings` ADD COLUMN IF NOT EXISTS `timezone` VARCHAR(64) DEFAULT 'UTC'; + +-- Dropping a column (safe) +ALTER TABLE `settings` DROP COLUMN IF EXISTS `old_column`; + +-- Creating a table (safe) +CREATE TABLE IF NOT EXISTS `audit_log` ( + `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `action` VARCHAR(255) NOT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8; +``` + +- **Используйте условное обозначение `INSERT`**, чтобы избежать дублирования: + +```sql +INSERT INTO `streams_arguments` (argument_key, argument_name, argument_cmd) +SELECT 'my_key', 'My Argument', '-my_flag %s' +FROM DUAL +WHERE NOT EXISTS (SELECT 1 FROM `streams_arguments` WHERE argument_key = 'my_key'); +``` + +- **Не смешивайте DDL и DML**, которые зависят друг от друга, в одном файле. Если вам нужно добавить столбец и затем заполнить его, используйте два файла шага обновления базы данных. + +- **Комментарии** поддерживаются с префиксом `--` (они пропускаются во время выполнения). + +### Шаг 3. Поместите файл + +Скопируйте SQL-файл для шага обновления базы данных в: + +```text +/home/xc_vm/migrations/ +``` + +> 💡 В хранилище исходных текстов это `src/migrations/`. + +### Шаг 4. Подтвердите обновление базы данных + +Запустите `db:migrate`, чтобы применить ожидающие обновления БД шаги: + +```bash +su - xc_vm -c '/home/xc_vm/console.php db:migrate' +``` + +Или через `status first-run` (также запускает миграции): + +```bash +sudo /home/xc_vm/console.php status first-run +``` + +Ожидаемый результат: + +```text +Migrations +------------------------------ + [OK] 006_add_user_timezone.sql + +``` + +Если инструкция не выполняется, шаг все равно будет записан, но покажет `[WARN]` — просмотрите SQL и устраните любые проблемы. + +--- + +## Общие операции CLI + +### Проверка состояния + +```bash +sudo /home/xc_vm/console.php status +``` + +Проверяет, запущен ли XC_VM, подключается к базе данных, выполняет ожидающие обновления шаги, исправляет разрешения и проверяет конфигурацию nginx. Требуется после установки или восстановления. + +С аргументом `first-run` пропускает текущую проверку, используемую для начальной настройки: + +```bash +sudo /home/xc_vm/console.php status first-run +``` + +### Управление услугами + +```bash +sudo /home/xc_vm/console.php service start|stop|restart|reload +``` + +### Обновление вручную + +```bash +sudo -u xc_vm /home/xc_vm/console.php update update +``` + +Загружает и применяет последнее обновление с GitHub. Обычно запускается автоматически через веб-панель. + +### Диагностика потока + +```bash +sudo -u xc_vm /home/xc_vm/console.php monitor +``` + +Запускает поток вручную и отображает все ошибки. Полезно для диагностики сбоев при запуске потока. + +### Диагностика сервера (узла) + +```bash +# On the MAIN — remote-probe a node by its server id +sudo /home/xc_vm/console.php server:diagnose + +# On the LB/proxy node itself — local self-diagnosis (no arguments) +sudo /home/xc_vm/console.php server:diagnose +``` + +Выясняет, почему прокси—узел/LB—узел отображается в автономном режиме на панели: проверяет частоту сердечных сокращений, доступность (ICMP/TCP/HTTP `/api`), перекос часов, очередь сигналов и - локально на узле - выполняет ли узел брандмауэр на главном IP-адресе в своем собственные iptables, запущена ли служба `xc_vm`/nginx, запущен ли демон `watchdog` heartbeat и находится ли `cron:servers` в `xc_vm` crontab. Доступен только для чтения; код выхода `0` = проблем не обнаружено, `2` = указаны вероятные причины. Подробности смотрите в [Руководстве по диагностике сервера](../administration/server-diagnostics.md). + +### SSL-сертификат + +```bash +sudo /home/xc_vm/console.php certbot +``` + +### Применяйте перенос базы данных вручную + +```bash +su - xc_vm -c '/home/xc_vm/console.php db:migrate' +``` + +Применяет все ожидающие обновления файлы `.sql` из `/home/xc_vm/migrations/`. Используйте это, когда вам нужно выполнить миграцию без полного обновления системы. + +### Обновление базы данных данными из других систем + +```bash +/home/xc_vm/console.php migrate +``` + +Передает данные из промежуточной базы данных `xc_vm_migrate`. Подробности см. в [Руководстве по обновлению базы данных](../info/migration_guide.md). + +--- + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `src/console.php` |Точка входа CLI + обнаружение команды FQCN| +| `src/Cli/Commands/` |Консольные команды| +| `src/Cli/CronJobs/` |Классы заданий Cron| +| `src/migrations/` |Миграция баз данных| diff --git a/docs/ru/guides/dev-workflow.md b/docs/ru/guides/dev-workflow.md new file mode 100644 index 00000000..7e8082d0 --- /dev/null +++ b/docs/ru/guides/dev-workflow.md @@ -0,0 +1,141 @@ +# Рабочий процесс разработки + +Как настроить проект локально, выполнить проверку качества и развернуть код на сервере разработки. + +--- + +## Локальная настройка + +Зафиксированный `src/vendor/` предназначен ** только для производства **, поэтому инструменты разработки (PHPStan, +PHP-CS-Fixer) отсутствуют в дереве. Установите их один раз из зафиксированной блокировки: + +```bash +make dev-tools # = cd src && composer install +``` + +Это добавит пакеты `require-dev` в `src/vendor/`. **Никогда не фиксируйте их** — +зарегистрированный поставщик должен оставаться доступным только для производства (`composer install --no-dev`). +`.gitignore` не допускает попадания пакетов разработчиков в `git add` и CI-шлюз +(`check-vendor-prod-only`) завершается сбоем сборки, если она когда-либо была зафиксирована. + +## Проверка качества + +Запустите их до того, как push — CI запустит тот же набор: + +|Команда|Проверки| +| --- | --- | +| `make phpstan` |Статический анализ по сравнению с зафиксированным базовым уровнем (сбой происходит только при появлении НОВЫХ проблем)| +| `make cs` |Стиль кода — импорт/гигиена пространства имен (PHP-CS-Fixer, предварительный запуск)| +| `make cs-fix` |Примените исправления стиля на месте| +| `make gates` |PSR-4 регрессионные параметры (ниже)| +| `php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist` |Модульные тесты| + +@Для @0@@ и `make cs` нужны инструменты разработчика — сначала запустите `make dev-tools`. + +`make gates` связывает трех охранников: + +- **проверка-процедурное использование ** - процедурные файлы / файлы просмотра импортируют каждый перенесенный класс, который они используют (PHP импортируется позиционно, поэтому `use` должен предшествовать использованию); +- **verify-lb-archive** — сборка балансировщика нагрузки исключает привилегированный код (контроллеры администратора/реселлера, домен пользователя/устройства, команды установки/root).; +- **только для проверки поставщика** - ни один пакет `require-dev` не зафиксирован в соответствии с `src/vendor/`. + +## Развертывание кода в VDS через SFTP + +Для ежедневной разработки мы рекомендуем [расширение SFTP](https://marketplace.visualstudio.com/items?itemName=Natizyskunk.sftp) для VS Code — редактировать локально, автоматически загружать при сохранении. + +### Установка + +Создать `.vscode/sftp.json`: + +```json +[ + { + "name": "My Dev VDS", + "host": "YOUR_VDS_IP", + "protocol": "sftp", + "port": 22, + "username": "root", + "remotePath": "/home/xc_vm", + "useTempFile": false, + "uploadOnSave": true, + "openSsh": false, + "watcher": { + "files": "**/*", + "autoUpload": false, + "autoDelete": true + }, + "ignore": [ + ".vscode", + ".git", + ".gitattributes", + ".gitignore", + "update", + "*pycache/", + "*.gitkeep", + "bin/", + "config/", + "tmp/" + ], + "context": "./src/", + "profiles": {} + }, + { + "name": "My Dev VDS Tests", + "host": "YOUR_VDS_IP", + "protocol": "sftp", + "port": 22, + "username": "root", + "remotePath": "/home/xc_vm/tests", + "useTempFile": false, + "uploadOnSave": true, + "openSsh": false, + "watcher": { + "files": "**/*", + "autoUpload": false, + "autoDelete": true + }, + "ignore": [ + ".vscode", + ".git", + ".gitattributes", + ".gitignore", + "tmp/", + ".cache/" + ], + "context": "./tests/", + "profiles": {} + } +] +``` + +### Основные настройки + +- **`context: "./src/"`** — сопоставляет местное `src/` с удаленным `/home/xc_vm/` +- **`context: "./tests/"`** — сопоставляет местное `tests/` с удаленным `/home/xc_vm/tests/` +- **`uploadOnSave: true`** — каждое сочетание клавиш Ctrl+S мгновенно переносит файл в VDS +- **`ignore`** — защищает файлы, зависящие от сервера. (`bin/`, `config/`, `tmp/`) + +> ** Безопасность:** Используйте SSH-ключи вместо пароля. Каталог `.vscode/` находится в `.gitignore`, поэтому учетные данные не попадут в git. + +### Как синхронизировать папку с тестами + +1. Добавьте вторую запись SFTP с `context: "./tests/"` и `remotePath: "/home/xc_vm/tests"`. +2. Сохраняйте файлы под `tests/` локально. +3. Расширение загрузит их отдельно из `src/` в `/home/xc_vm/tests`. +4. Это необходимо, поскольку тесты хранятся за пределами `src/` и не будут загружены основной записью. + +### Рабочий процесс + +1. Откройте проект в VS Code +2. Отредактируйте любой файл под `src/` +3. Если вы добавляете тест, отредактируйте файл в разделе `tests/` +4. Сохранить — соответствующая запись SFTP загружает файл в VDS +5. Запустите соответствующий тест на VDS +6. Фиксация в git как обычно + +## Связанные файлы + +|Файл|Роль| +| --- | --- | +| `.vscode/sftp.json` |Локальная настройка → Настройка синхронизации VDS (gitignored)| +| `Makefile` |`make dev-tools`, `make phpstan`, `make cs`, `make gates`| +| `src/composer.json` |Зависимости + PSR-4 автозагрузка| diff --git a/docs/ru/guides/diagnostics-api.md b/docs/ru/guides/diagnostics-api.md new file mode 100644 index 00000000..6f597297 --- /dev/null +++ b/docs/ru/guides/diagnostics-api.md @@ -0,0 +1,92 @@ +# Служба диагностики + +`DiagnosticsService` предоставляет помощников по оперативной диагностике: + +- Проверка SSL-сертификата +- проверка совместимости кодеков +- экспорт/отправка журнала панели +- обнаружение IP-адреса внешнего API + +--- + +## Методы + +### `getCertificateInfo(?string $certificate = null): ?array` + +Возвращает сведения о сертификате (`serial`, `expiration`, `subject`, `path`). + +Если `$certificate` равно null, путь к сертификату определяется автоматически из конфигурации nginx SSL. +Возвращает `null`, если файл отсутствует или при анализе OpenSSL произошел сбой. + +--- + +### `checkCompatibility(array|string $data, bool $allowHEVC = false): bool` + +Проверяет, совместимы ли медиакодеки с ограничениями проигрывателя. +Принимает полезную нагрузку FFProbe в виде массива или строки JSON. + +Принятые по умолчанию кодеки включают: + +- видео: `h264`, `vp8`, `vp9`, `ogg`, `av1` +- аудио: `aac`, `libfdk_aac`, `opus`, `vorbis`, `pcm_s16le`, `mp2`, `mp3`, `flac` + +С `$allowHEVC=true`, позволяет `hevc`/`h265` и `ac3`. + +--- + +### `downloadPanelLogs(object $db): array` + +Извлекает до 1000 последних строк из `panel_logs` (исключая `epg`), очищает поля вывода и строки журнала после экспорта. + +Возвращаемая форма: + +```php +[ + 'errors' => [...], + 'version' => '...' +] +``` + +Выдает `Exception` при сбое запроса к базе данных. + +--- + +### `submitPanelLogs(object $db): string|false` + +Отправляет журналы панели в конечную точку удаленной диагностики и очищает отправленные журналы. +Возвращает строку ответа в случае успеха, `false` в случае сбоя (например, не удается разрешить IP-адрес API). +Используется для поддержки /телеметрии рабочих процессов. + +--- + +## `panel_logs` Форма (используемые поля) + +|Колонка|Тип|Значение| +| --- | --- | --- | +| `type` |строка|тип события| +| `log_message` |строка|основное сообщение| +| `log_extra` |строка|дополнительный контекст| +| `line` |инт|исходная строка| +| `date` |инт|Временная метка UNIX| + +--- + +### `getPIDs(int $rServerID): array` + +Анализирует информацию о системном процессе из ответа API сервера. Возвращает структурированные данные процесса для мониторинга. + +--- + +### `getNVENCProcesses(int $rServerID): array` + +Извлекает данные процесса GPU (NVENC) из информации о графическом процессоре сервера. Используется для диагностики кодирования на графическом процессоре. + +--- + +## Связанные файлы + +|Файл|Цель| +| --- | --- | +| `src/Core/Diagnostics/DiagnosticsService.php` |методы диагностики| +| `src/Core/Logging/Logger.php` |записывает журналы| +| `src/Core/Config/AppConfig.php` |метаданные версии| diff --git a/docs/ru/guides/error-handling.md b/docs/ru/guides/error-handling.md new file mode 100644 index 00000000..f10770f3 --- /dev/null +++ b/docs/ru/guides/error-handling.md @@ -0,0 +1,380 @@ +# Модель обработки ошибок + +XC_VM обработка ошибок состоит из трех уровней: + +- **Коды ошибок** -- в чем произошел сбой (централизованный реестр именованных строк ошибок) +- **Обработчики ошибок** -- как генерируется HTTP-ответ клиента (`generateError()`, `generate404()`) +- **Подсистема ведения журнала** - фиксация во время выполнения ошибок PHP, неперехваченных исключений и фатальных сбоев + +--- + +## Обзор потока + +```text +Application code + | + +-- generateError('CODE') // deliberate error response + | -> debug mode: styled HTML page with code + description + | -> production: generate404() or explicit HTTP code + | + +-- PHP warning / notice / error // runtime errors + | -> Logger::handleError() + | -> maps errno to level (ERROR, WARNING, NOTICE, INFO) + | -> writes base64-encoded JSON to error_log.log + | -> optionally displays on screen + | + +-- Uncaught exception // unhandled Throwable + | -> Logger::handleException() + | -> logs as EXCEPTION with full chained trace + | + +-- Fatal error at shutdown // E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR + -> Logger::handleFatal() + -> logs as FATAL (no stack trace available) +``` + +--- + +## Реестр кодов ошибок + +Все коды объявлены в `src/Core/Error/ErrorCodes.php` как глобальный массив `$rErrorCodes`. + +Формат кода: + +- Ключ: строка в верхнем регистре (пример: `INVALID_CREDENTIALS`) +- Значение: понятное для человека описание на английском языке + +Используйте только централизованные определения кода. Не следует жестко кодировать текст ошибки в обработчиках конечных точек. + +### Полный список кодов + +|Код|Описание| +| --- | --- | +| `API_IP_NOT_ALLOWED` |IP-адрес не разрешен для доступа к API.| +| `ARCHIVE_DOESNT_EXIST` |Для этого идентификатора потока отсутствуют архивные файлы.| +| `ASN_BLOCKED` |ASN был заблокирован.| +| `BANNED` |Линия была заблокирована.| +| `BLOCKED_USER_AGENT` |Пользовательский агент был заблокирован.| +| `CACHE_INCOMPLETE` |Генерируется кэш...| +| `DEVICE_NOT_ALLOWED` |Устройствам MAG и Enigma не разрешен доступ к этому файлу.| +| `DISABLED` |Линия была отключена.| +| `DOWNLOAD_LIMIT_REACHED` |Достигнут лимит одновременной загрузки.| +| `E2_DEVICE_LOCK_FAILED` |Проверка блокировки устройства не удалась.| +| `E2_DISABLED` |Устройство было отключено.| +| `E2_NO_TOKEN` |Токен не был указан.| +| `E2_TOKEN_DOESNT_MATCH` |Токен не соответствует записям.| +| `E2_WATCHDOG_TIMEOUT` |Истек лимит времени.| +| `EMPTY_USER_AGENT` |Пустые пользовательские агенты запрещены.| +| `EPG_DISABLED` |EPG был отключен.| +| `EPG_FILE_MISSING` |Кэшированные файлы EPG отсутствуют.| +| `EXPIRED` |Срок действия строки истек.| +| `FORCED_COUNTRY_INVALID` |Страна не совпадает с принудительной страной.| +| `GENERATE_PLAYLIST_FAILED` |Не удалось создать список воспроизведения.| +| `HLS_DISABLED` |HLS был отключен.| +| `HOSTING_DETECT` |Обнаружен хостинг-сервер.| +| `INVALID_API_PASSWORD` |Неверный пароль API.| +| `INVALID_CREDENTIALS` |Имя пользователя или пароль неверны.| +| `INVALID_HOST` |Доменное имя не распознано.| +| `INVALID_STREAM_ID` |Идентификатор потока не существует.| +| `INVALID_TYPE_TOKEN` |Токены не могут быть использованы для этого типа потока.| +| `IP_BLOCKED` |IP-адрес был заблокирован.| +| `IP_MISMATCH` |Текущий IP-адрес не соответствует исходному IP-адресу подключения.| +| `ISP_BLOCKED` |Провайдер был заблокирован.| +| `LB_TOKEN_INVALID` |Токен AES не может быть расшифрован.| +| `LEGACY_EPG_DISABLED` |Устаревший epg.php доступ был отключен.| +| `LEGACY_GET_DISABLED` |Устаревший get.php доступ был отключен.| +| `LEGACY_PANEL_API_DISABLED` |Устаревший panel_api.php доступ был отключен.| +| `LINE_CREATE_FAIL` |Не удалось вставить строку в базу данных.| +| `NO_CREDENTIALS` |Учетные данные не были указаны.| +| `NO_SERVERS_AVAILABLE` |В настоящее время серверы для этого потока не доступны.| +| `NO_TIMESTAMP` |Временная метка архива не указана.| +| `NO_TOKEN_SPECIFIED` |Зашифрованный токен AES не был указан.| +| `NOT_ENIGMA_DEVICE` |Линия - это не загадочное устройство.| +| `NOT_IN_ALLOWED_COUNTRY` |Нет в списке разрешенных стран.| +| `NOT_IN_ALLOWED_IPS` |Его нет в списке разрешенных IP-адресов.| +| `NOT_IN_ALLOWED_UAS` |Отсутствует в списке разрешенных пользовательских агентов.| +| `NOT_IN_BOUQUET` |У Line нет доступа к этому идентификатору потока.| +| `PLAYER_API_DISABLED` |API плеера был отключен.| +| `PROXY_ACCESS_DENIED` |Вы не можете получить прямой доступ к этому потоку, пока включен прокси-сервер.| +| `PROXY_DETECT` |Обнаружен прокси-сервер.| +| `PROXY_NO_API_ACCESS` |Не удается получить доступ к API через прокси.| +| `RESTREAM_DETECT` |Обнаружен повторный поток.| +| `STALKER_CHANNEL_MISMATCH` |Идентификатор потока не совпадает с токеном stalker.| +| `STALKER_DECRYPT_FAILED` |Не удалось расшифровать токен сталкера.| +| `STALKER_INVALID_KEY` |Недействительный ключ сталкера.| +| `STALKER_IP_MISMATCH` |IP-адрес не соответствует токену stalker.| +| `STALKER_KEY_EXPIRED` |Срок действия жетона сталкера истек.| +| `STREAM_OFFLINE` |Трансляция в данный момент отключена.| +| `SUBTITLE_DOESNT_EXIST` |Файл субтитров не существует.| +| `THUMBNAIL_DOESNT_EXIST` |Файл миниатюр не существует.| +| `THUMBNAILS_NOT_ENABLED` |Миниатюра не включена для этого потока.| +| `TOKEN_ERROR` |Токен AES содержит неполные данные.| +| `TOKEN_EXPIRED` |Срок действия токена AES истек.| +| `TS_DISABLED` |MPEG-TS был отключен.| +| `USER_ALREADY_CONNECTED` |Линия уже подключена с другого IP-адреса.| +| `USER_DISALLOW_EXT` |Добавочный номер отсутствует в списке разрешенных.| +| `VOD_DOESNT_EXIST` |VOD файл не существует.| +| `WAIT_TIME_EXPIRED` |Время начала трансляции истекло, запустить не удалось.| + +Коды, относящиеся к потоковой передаче данных (`CACHE_INCOMPLETE`, `SUBTITLE_DOESNT_EXIST`, `NO_SERVERS_AVAILABLE`, `PROXY_ACCESS_DENIED`) были перенесены из `stream/init.php` в централизованный реестр. + +--- + +## Обработчики ошибок + +Определено в `src/Core/Error/ErrorHandler.php`. Это простые функции (не методы класса), загружаемые в начале начальной загрузки. + +### `generateError(string $rError, bool $rKill = true, ?int $rCode = null)` + +Выдает ответ об ошибке HTTP. Поведение зависит от параметра `debug_show_errors`.: + +```text +if debug_show_errors === true + render styled HTML page showing error key + description + if $rKill -> exit() +else (production) + if $rKill + if $rCode is set -> http_response_code($rCode) + exit() + else -> generate404() + // if !$rKill, does nothing in production mode +``` + +Параметры: + +|Параметр|Тип|По умолчанию|Значение| +| --- | --- | --- | --- | +| `$rError` | `string` |--|Ключ от `$rErrorCodes`| +| `$rKill` | `bool` | `true` |Завершить работу скрипта после вывода| +| `$rCode` |`инт\|нулевой`| `null` |Явный код ответа HTTP (обходит 404 в рабочей среде)| + +Примеры: + +```php +generateError('INVALID_CREDENTIALS'); // production: 404 + exit +generateError('API_IP_NOT_ALLOWED', true, 403); // production: 403 + exit +generateError('STREAM_OFFLINE', false); // production: no output, no exit +``` + +### `generate404(bool $rKill = true)` + +Возвращает страницу `404 Not Found` в стиле nginx и устанавливает HTTP 404. HTML-код содержит комментарии с дополнениями, чтобы скрыть страницы ошибок, отображаемые в браузере MSIE и Chrome. + +```php +generate404(); // 404 + exit +generate404(false); // 404, continue execution +``` + +--- + +## Подсистема регистратора + +Определен в `src/Core/Logging/Logger.php`. Класс `final`, который регистрирует три глобальных обработчика PHP, чтобы фиксировать все ошибки во время выполнения и записывать их в файл. + +### Инициализация + +```php +Logger::init(bool $showErrors, string $logFile): void +``` + +Регистры: + +1. `set_error_handler([Logger::class, 'handleError'])` -- PHP предупреждения, извещения, ошибки +2. `set_exception_handler([Logger::class, 'handleException'])` -- не перехвачено `Throwable` +3. `register_shutdown_function([Logger::class, 'handleFatal'])` -- неустранимые ошибки при завершении работы + +Также настраивает `error_reporting(E_ALL & ~E_NOTICE & ~E_DEPRECATED)` и устанавливает `display_errors` / `display_startup_errors` на основе `$showErrors`. + +### Где вызывается функция Logger::init() + +Регистратор инициализируется в двух местах, в зависимости от пути запроса: + +|Путь входа|Файл|Как| +| --- | --- | --- | +|Bootstrap (все контексты)| `src/bootstrap.php` |`XC_Bootstrap::loadConstants()` звонков `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`| +|Конечные точки потоковой передачи| `src/Core/Http/RequestGuard.php` |Загружает настройки из файлового кэша, определяет `PHP_ERRORS`, затем вызывает `Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log')`| + +В обоих случаях `PHP_ERRORS` отражает настройку `debug_show_errors` (по умолчанию используется `false`, когда настройки недоступны). + +### Отображение уровня ошибок + +`Logger::handleError()` сопоставляет PHP константы ошибок со строками уровня журнала через `mapErrorLevel()`: + +|PHP константа(ы)|Уровень регистрации| +| --- | --- | +|`E_ERROR`, `E_CORE_ERROR`, `E_COMPILE_ERROR`| `ERROR` | +|`E_WARNING`, `E_USER_WARNING`| `WARNING` | +|`E_NOTICE`, `E_USER_NOTICE`| `NOTICE` | +|Все остальные значения `errno`| `INFO` | + +Обработчик завершения работы (`handleFatal()`) проверяет `error_get_last()` на наличие этих фатальных типов и регистрирует их как `FATAL`: + +|PHP постоянные значения при выключении|Уровень регистрации| +| --- | --- | +|`E_ERROR`, `E_PARSE`, `E_CORE_ERROR`, `E_COMPILE_ERROR`| `FATAL` | + +Неперехваченные исключения, зарегистрированные с помощью `handleException()`, всегда используют уровень `EXCEPTION`. + +Ошибки, подавленные с помощью оператора `@`, игнорируются (обработчик проверяет `error_reporting() & $errno`). + +### Формат журнала + +Каждая запись в журнале записывается в виде одной строки: `base64_encode(json_encode($data))`, за которой следует новая строка. Это предотвращает искажение строк в многострочных сообщениях. + +Декодированная структура JSON: + +```json +{ + "type": "WARNING", + "log_message": "Undefined variable $foo", + "file": "/home/xc_vm/Domain/Stream/StreamService.php", + "line": 142, + "log_extra": "#0 /home/xc_vm/...(line): function()\n#1 ...", + "time": 1716220800, + "env": "fpm-fcgi" +} +``` + +|Поле|Содержание| +| --- | --- | +| `type` |Уровень регистрации: `ERROR`, `WARNING`, `NOTICE`, `INFO`, `EXCEPTION`, или `FATAL`| +| `log_message` |Текст сообщения об ошибке/исключении| +| `file` |Абсолютный путь к исходному файлу| +| `line` |Номер строки, в которой произошла ошибка| +| `log_extra` |Трассировка стека (форматированная строка). Пусто для неустранимых ошибок.| +| `time` |Временная метка Unix| +| `env` |PHP имя SAPI (`cli`, `fpm-fcgi`, и т.д.)| + +### Расположение файла журнала + +Путь по умолчанию: `LOGS_TMP_PATH . 'error_log.log'` + +Если каталог журнала не существует, Logger создает его с правами доступа `0775`. При запуске от имени root (распространенного в контейнерах) файлу присваивается имя `xc_vm:xc_vm` с режимом `0664`. + +### Вывод на экран + +Когда `$showErrors` равно `true`, регистратор также отображает ошибки напрямую: + +- **CLI:** клеммный выход с цветовой кодировкой (красный - НЕИСПРАВИМОСТЬ/ОШИБКА, желтый - ПРЕДУПРЕЖДЕНИЕ, синий - УВЕДОМЛЕНИЕ) +- **Веб:** встроенный `
` с моноширинным шрифтом, красной рамкой и трассировкой стека в блоке `
`
+
+---
+
+## Конвейер ведения журнала: Передача файла в базу данных
+
+Регистратор записывает данные в `error_log.log` на диске. Отдельная подсистема считывает этот файл и сохраняет записи в таблице базы данных `panel_logs`:
+
+1. **Регистратор** записывает строки JSON в кодировке base64 в `error_log.log`
+2. **FileLogger** (`src/Core/Logging/FileLogger.php`) предоставляет дополнительный интерфейс ведения журнала, используемый кодом приложения (ошибки PDO, EPG и т.д.), который записывает данные в тот же файл в том же формате
+3. Записи заносятся в таблицу `panel_logs`
+4. **Диагностический сервис** (`src/Core/Diagnostics/DiagnosticsService.php`) считывает данные из `panel_logs` для:
+   - `downloadPanelLogs()` -- извлекает до 1000 последних ошибок, не связанных с EPG, затем обрезает таблицу
+   - `submitPanelLogs()` -- отправляет логи на центральный сервер API для анализа
+5. Панель администратора отображает эти журналы в разделе **Управление > Журналы > Ошибки панели**
+
+### Фильтрация шума файлового регистратора
+
+`FileLogger::log()` пропускает записи, которые соответствуют:
+
+- Сообщения, содержащие `panel_logs` в дополнительном поле (предотвращает рекурсивное ведение журнала)
+- Совпадающие сообщения `timeout exceeded`, `lock wait timeout`, или `duplicate entry` (ошибки с помехами MySQL)
+
+---
+
+## Другие лесорубы
+
+Каталог `src/Core/Logging/` содержит дополнительные специализированные регистраторы:
+
+|Класс|Файл|Цель|
+| --- | --- | --- |
+| `Logger` | `Logger.php` |Глобальный PHP обработчик ошибок/исключений/фатальных исходов (описанный выше)|
+| `FileLogger` | `FileLogger.php` |Ведение журнала на уровне приложения (ошибки PDO, EPG и т.д.) до `error_log.log`|
+| `DatabaseLogger` | `DatabaseLogger.php` |Клиентская потоковая передача событий запроса в `client_request.log` (вводится в таблицу `client_logs`)|
+| `UpdateLogger` | `UpdateLogger.php` |Операции обновления системы до `MAIN_HOME/update.log` (обычный текст, а не base64)|
+
+Все регистраторы, кроме `UpdateLogger`, реализуют `LoggerInterface` и записывают JSON в кодировке base64.
+
+---
+
+## Типы исключений в кодовой базе
+
+В кодовой базе определено небольшое количество пользовательских классов исключений. Все неперехваченные исключения перехватываются командой `Logger::handleException()`, которая регистрирует всю цепочку исключений (включая `getPrevious()`).
+
+|Класс исключений|Базовый класс|Местоположение|
+| --- | --- | --- |
+| `DropboxException` | `Exception` | `src/Core/Storage/DropboxClient.php` |
+| `M3uParser\Exception` | `\Exception` | `src/Core/Parsing/M3uParser/src/Exception.php` |
+| `DataBuildingException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Parser/DataBuildingException.php` |
+| `DefinitionException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Definition/DefinitionException.php` |
+| `DumpingException` | `\RuntimeException` | `src/Core/Parsing/PhpM3u8/src/Dumper/DumpingException.php` |
+
+В большинстве прикладных программ используются общие ошибки `Exception` или используется встроенная система ошибок PHP. Обработчик исключений регистратора принимает любые `Throwable`.
+
+---
+
+## Отладка против производства
+
+### Производство (по умолчанию: `debug_show_errors = false`)
+
+- `generateError()` возвращает общую страницу 404 (или явный HTTP-код), скрывая внутреннюю причину сбоя
+- Регистратор по-прежнему записывает все ошибки в `error_log.log` на диск
+- `display_errors` и `display_startup_errors` имеют значение `'0'`
+- Ошибки видны только через панель администратора (страница ошибок панели) или файлы журналов
+
+### Отладка (`debug_show_errors = true`)
+
+- `generateError()` показывает стилизованную страницу с ключом ошибки и сопоставленным описанием
+- Регистратор дополнительно отображает ошибки на экране (вывод CLI с цветовой кодировкой или встроенный HTML).
+- `display_errors` и `display_startup_errors` имеют значение `'1'`
+
+Не включайте отображение отладки на рабочих узлах.
+
+---
+
+## Регистрация обработчика ошибок Bootstrap
+
+Инфраструктура обработки ошибок загружается на ранней стадии загрузки:
+
+1. `bootstrap.php` определяет `MAIN_HOME` и регистрирует Composer автозагрузчик
+2. `XC_Bootstrap::loadConstants()` загружает (по порядку):
+   - `Core/Error/ErrorCodes.php` -- заполняет `$rErrorCodes`
+   - `Core/Error/ErrorHandler.php` -- определяет `generateError()` и `generate404()`
+   - Путь и конфигурационные файлы
+   - `Core/Logging/Logger.php` -- определение класса
+3. @вызывается @0@@, регистрирующий три глобальных обработчика
+4. С этого момента фиксируются все PHP ошибки, неперехваченные исключения и фатальные сбои
+
+Для конечных точек потоковой передачи, которые обходят полную загрузку, `RequestGuard.php` выполняет шаги 2-3 независимо: загружает настройки из файлового кэша, определяет `PHP_ERRORS` и вызывает `Logger::init()`.
+
+---
+
+## Добавление нового кода ошибки
+
+1. Добавьте новый ключ к `src/Core/Error/ErrorCodes.php`:
+
+```php
+'MY_NEW_ERROR' => 'Human-readable description.',
+```
+
+2. Используйте это в коде:
+
+```php
+generateError('MY_NEW_ERROR');
+```
+
+Описания должны быть на английском языке для приведения в соответствие с существующим реестром.
+
+---
+
+## Связанные файлы
+
+|Файл|Цель|
+| --- | --- |
+| `src/Core/Error/ErrorCodes.php` |Централизованная карта кодов ошибок (`$rErrorCodes`)|
+| `src/Core/Error/ErrorHandler.php` |@функции @0@@ и `generate404()`|
+| `src/Core/Logging/Logger.php` |Глобальные обработчики ошибок, исключений и фатальных исходов PHP|
+| `src/Core/Logging/LoggerInterface.php` |Интерфейс контракта ведения журнала|
+| `src/Core/Logging/FileLogger.php` |Ведение журнала файлов на уровне приложения (PDO, EPG и т.д.)|
+| `src/Core/Logging/DatabaseLogger.php` |Ведение журнала событий потокового запроса клиента|
+| `src/Core/Logging/UpdateLogger.php` |Ведение журнала операций обновления системы|
+| `src/Core/Http/RequestGuard.php` |Путь потоковой передачи: защита от наводнений, проверка хоста, запуск регистратора|
+| `src/Core/Diagnostics/DiagnosticsService.php` |Считывает таблицу `panel_logs` для отображения администратором и отправки API|
+| `src/bootstrap.php` |Включает уровень ошибок и регистратор во всех контекстах начальной загрузки|
diff --git a/docs/ru/guides/feature-flags.md b/docs/ru/guides/feature-flags.md
new file mode 100644
index 00000000..08ce2699
--- /dev/null
+++ b/docs/ru/guides/feature-flags.md
@@ -0,0 +1,82 @@
+# Флаги функций разработки
+
+XC_VM использует константы и флаги, зависящие от настроек, для управления поведением среды.
+
+Константы приложения хранятся в `src/Core/Config/AppConfig.php`.
+
+---
+
+## Активный флаг времени выполнения
+
+### `PHP_ERRORS`
+
+```php
+define('PHP_ERRORS', $rShowErrors); // derived from $rSettings['debug_show_errors']
+```
+
+`PHP_ERRORS` управляет PHP/детализирует детализацию и выводит данные с экрана регистратора:
+
+```php
+Logger::init(PHP_ERRORS, LOGS_TMP_PATH . 'error_log.log');
+```
+
+### `DB_ACCESS_ENABLED`
+
+```php
+define('DB_ACCESS_ENABLED', false); // enables phpMiniAdmin tab/page in admin panel
+```
+
+`DB_ACCESS_ENABLED` управляет доступом к phpMiniAdmin только из пользовательского интерфейса администратора.
+Это не блокирует подключения к базе данных основного приложения.
+
+---
+
+## Флаги, зависящие от настроек (`$rSettings`)
+
+Загружается из кэша настроек и используется в точках принятия решений во время выполнения.
+
+|Ключ|Тип|Значение|
+| --- | --- | --- |
+| `debug_show_errors` | `bool` |показывать подробные результаты ошибок/отладки|
+| `recaptcha_enable` | `bool` |включите reCAPTCHA v2 при входе в систему|
+| `verify_host` | `bool` |принудительная проверка списка разрешений хоста|
+| `save_login_logs` | `bool` |постоянные попытки входа в систему в `login_logs`|
+
+Эти значения загружаются из `CACHE_TMP_PATH/settings` защитниками запросов.
+
+---
+
+## Статические константы приложения
+
+От `src/Core/Config/AppConfig.php`:
+
+```php
+define('DB_ACCESS_ENABLED', false);
+define('XC_VM_VERSION', '2.2.1');
+define('GIT_OWNER', 'Vateron-Media');
+define('GIT_REPO_MAIN', 'XC_VM');
+define('GIT_REPO_UPDATE', 'XC_VM_Update');
+define('GIT_REPO_BIN', 'XC_VM_Binaries');
+define('MONITOR_CALLS', 3);
+define('OPENSSL_EXTRA', '...');
+```
+
+---
+
+## Добавление новых флагов
+
+Используйте статические константы в `AppConfig.php` для фиксированных констант инфраструктуры/среды выполнения.
+Используйте настройки (`$rSettings`) для значений, которыми необходимо управлять из пользовательского интерфейса панели.
+
+Избегайте определения одного и того же поведения в обоих местах.
+
+---
+
+## Связанные файлы
+
+|Файл|Цель|
+| --- | --- |
+| `src/Core/Config/AppConfig.php` |статические константы приложения|
+| `src/Core/Http/RequestGuard.php` |загружает `$rSettings`, устанавливает `PHP_ERRORS`|
+| `src/Core/Error/ErrorHandler.php` |использует `debug_show_errors` поведение|
+| `src/Core/Logging/Logger.php` |поведение при отладке/детализации|
diff --git a/docs/ru/guides/geoip-and-device-detection.md b/docs/ru/guides/geoip-and-device-detection.md
new file mode 100644
index 00000000..bd87b191
--- /dev/null
+++ b/docs/ru/guides/geoip-and-device-detection.md
@@ -0,0 +1,296 @@
+# GeoIP и обнаружение устройства
+
+XC_VM использует базы данных MaxMind GeoIP2/GeoLite2 для определения геолокации и интернет-провайдера, а также библиотеку Mobile_Detect для анализа пользовательского агента.
+Эти системы интегрированы в потоковую аутентификацию для контроля доступа, географической маршрутизации и ведения журнала действий.
+
+---
+
+## GeoIP Поиск
+
+Два класса обеспечивают поиск GeoIP:
+
+### GeoIP (полезность)
+
+Файл: `src/Core/Util/GeoIP.php`
+
+```php
+GeoIP::getCountry($ip): array|false    // GeoLite2-City lookup
+GeoIP::getISP($ip): array|false        // GeoIP2-ISP lookup
+GeoIP::isISPBlocked($ispName, $blockedISPs): int
+GeoIP::isASNBlocked($asn, $blockedServers): bool
+```
+
+Результаты кэшируются в виде файлов по адресам `CONS_TMP_PATH/{md5(ip)}_geo2` и `CONS_TMP_PATH/{md5(ip)}_isp`.
+
+### Геоипсервис (высокого уровня)
+
+Файл: `src/Core/GeoIP/GeoIPService.php`
+
+```php
+GeoIPService::getIPInfo($rIP): array|false      // city-level lookup
+GeoIPService::getISP($rIP): array|false         // ISP + ASN data
+GeoIPService::matchCIDR($rASN, $rIP): array|null  // hosting/proxy detection
+```
+
+`matchCIDR()` проверяет соответствие IP-адреса блокам CIDR, хранящимся в ASN по адресу `CIDR_TMP_PATH/{asn}`. Возвращает флаги для определения хостинга и прокси-сервера.
+
+---
+
+## Обнаружение интернет-провайдеров и ASN
+
+Когда `show_isps` включен, каждый потоковый запрос разрешает клиентский провайдер:
+
+```php
+$rISPLock = GeoIPService::getISP($rIP);
+// returns: ['isp' => 'Comcast', 'autonomous_system_number' => 7922, ...]
+```
+
+Данные интернет-провайдера хранятся в записи пользователя:
+
+|Поле|Описание|
+| --- | --- |
+| `isp_desc` |имя заблокированного интернет-провайдера|
+| `as_number` |заблокированный ASN|
+| `con_isp_name` |текущее подключение провайдера (временное)|
+| `is_isplock` |включить привязку к интернет-провайдеру (0/1)|
+| `isp_violate` |Флаг нарушения со стороны провайдера — блокирует доступ|
+
+Нарушение блокировки интернет-провайдера происходит, когда:
+
+```text
+con_isp_name != isp_desc
+AND is_isplock = 1
+AND is_stalker = 0
+AND enable_isp_lock = 1
+```
+
+---
+
+## Обнаружение устройств
+
+### Мобильное обнаружение
+
+Файл: `src/Core/Device/MobileDetect.php`
+
+Библиотека (версия 2.8.45) для анализа пользовательского агента:
+
+```php
+$detect = new Mobile_Detect();
+$detect->isMobile();    // phones
+$detect->isTablet();    // tablets
+$detect->isAndroid();   // brand-specific
+```
+
+Используется в `src/bootstrap.php` для определения мобильных устройств по адаптивному интерфейсу администратора.
+
+### Телевизионные приставки
+
+**Энигмасервис** (`src/Domain/Device/EnigmaService.php`):
+
+Управляет учетными записями Enigma2 STB. Блокирует поля: `token`, `lversion`, `cpu`, `enigma_version`, `modem_mac`, `local_ip`.
+
+**Магсервис** (`src/Domain/Device/MagService.php`):
+
+Управляет учетными записями MAG STB. Блокирует поля: `ver`, `device_id2`, `device_id`, `hw_version`, `image_version`, `stb_type`, `sn`.
+
+Обе поддержки:
+
+- `lock_device` — аппаратная блокировка
+- `is_isplock` — Привязка к провайдеру
+- `forced_country` — принудительный перевод пользователя в определенную страну
+
+---
+
+## Проверки контроля доступа
+
+Все проверки выполняются в `src/www/stream/auth.php` во время проверки токена:
+
+### 1. Валидация в стране
+
+```text
+if forced_country is set:
+    country_code must match forced_country
+    error: FORCED_COUNTRY_INVALID
+
+if allow_countries whitelist exists:
+    country_code must be in whitelist
+    error: NOT_IN_ALLOWED_COUNTRY
+```
+
+### 2. Принудительное применение блокировки интернет-провайдером
+
+```text
+if is_isplock = 1 and is_stalker = 0:
+    con_isp_name must match isp_desc
+    error: ISP_BLOCKED
+```
+
+### 3. Блокировка ASN
+
+```text
+if block_svp = 1:
+    check ASN against blocked servers
+    error: ASN_BLOCKED
+```
+
+### 4. Обнаружение хостинга и прокси-серверов
+
+```text
+GeoIPService::matchCIDR($asn, $ip)
+    flag[3] = hosting → error: HOSTING_DETECT
+    flag[4] = proxy → error: PROXY_DETECT
+```
+
+Также проверяет заголовок `X-XC_VM-DETECT` на предмет обнаружения повторного потока.
+
+### 5. Блокировка пользовательского агента
+
+```text
+check against BlocklistService::checkBlockedUAs()
+error: BLOCKED_USER_AGENT
+
+if user has allowed_ua set:
+    user_agent must match one entry
+    error: NOT_IN_ALLOWED_UAS
+```
+
+### 6. Проверка типа устройства
+
+```text
+MAG device flag must match token
+error: DEVICE_NOT_ALLOWED or TOKEN_EXPIRED
+```
+
+---
+
+## Географический маршрут
+
+GeoIP выбор сервера для передачи данных и прокси-сервера:
+
+### Выбор сервера (StreamAuth::checkAccess)
+
+Когда `enable_geoip == 1`:
+
+- Точное соответствие стране → немедленно выберите этот сервер.
+- `geoip_type == 'strict'` → исключить несоответствующие серверы.
+- В противном случае → назначьте приоритетный вес (1 для низкого, 2 для нормального).
+
+### Выбор прокси-сервера (ProxySelector::Доступный прокси)
+
+Та же логика применима к списку прокси-серверов. Для маршрутизации используется как код страны, так и имя провайдера.
+
+```php
+ProxySelector::availableProxy(
+    array_keys($rProxies),
+    $rCountryCode,
+    $rUserInfo['con_isp_name']
+);
+```
+
+---
+
+## Обновления базы данных
+
+Файл: `src/Core/GeoIP/MaxMindUpdater.php`
+
+```php
+$updater = MaxMindUpdater::fromSettings($settings);
+$updater->update();  // downloads and extracts all configured editions
+```
+
+Поддерживаемые выпуски:
+
+- `GeoLite2-Country`, `GeoLite2-City`, `GeoLite2-ASN` ( бесплатно)
+- `GeoIP2-Country`, `GeoIP2-City`, `GeoIP2-ISP`, `GeoIP2-Anonymous-IP` ( оплаченный)
+
+Загружается через MaxMind API с использованием `maxmind_account_id` и `maxmind_license_key`.
+Извлекает файлы `.mmdb` из tar.gz архивов в `BIN_PATH/maxmind/`.
+
+Пути к файлам базы данных (определены в `src/Core/Config/Binaries.php`):
+
+```text
+GEOLITE2_BIN  = BIN_PATH/maxmind/GeoLite2-Country.mmdb
+GEOLITE2C_BIN = BIN_PATH/maxmind/GeoLite2-City.mmdb
+GEOISP_BIN    = BIN_PATH/maxmind/GeoIP2-ISP.mmdb
+```
+
+### Автоматическое обновление
+
+Базы данных обновляются с помощью задания `cron:maxmind` cron (`src/Cli/CronJobs/MaxMindCronJob.php`).
+Он запускается ** только по вторникам** — в день, когда MaxMind публикует новые версии. Логика разветвляется на настройки панели:
+
+- если `maxmind_account_id` + `maxmind_license_key` + `maxmind_editions` задано, базы данных извлекаются непосредственно из MaxMind API (`MaxMindUpdater`, загружаются только настроенные версии).;
+- если учетные данные MaxMind не заданы, они возвращаются к версиям GitHub GeoLite2 (бесплатные базы данных).
+
+### Ручное (принудительное) обновление
+
+Чтобы немедленно обновить базы данных `.mmdb` на рабочей панели, запустите задание cron вручную **от имени пользователя root** с флагом `--force` (это снимает ограничение "Только по вторникам").:
+
+```bash
+/home/xc_vm/bin/php/bin/php /home/xc_vm/console.php cron:maxmind --force
+```
+
+Выходные статусы:
+
+- `[OK]` — база данных обновлена;
+- `[SKIP]` — уже обновлено;
+- `[WARN]` / `[ERROR]` — с подробной информацией (неверные учетные данные, ошибка HTTP, сеть недоступна).
+
+> ➡️ Путь к API MaxMind отправляет заголовок `If-Modified-Since`, поэтому уже обновленная база данных возвращает HTTP 304 и статус `[SKIP]`. Чтобы принудительно выполнить повторную загрузку, сначала удалите (или переименуйте) соответствующий `.mmdb` в `BIN_PATH/maxmind/`, чтобы заголовок не отправлялся. Резервный вариант GitHub не имеет такого поведения — он сравнивает md5 и повторно загружает при несоответствии.
+
+---
+
+## Ведение журнала действий
+
+Все сеансы потоковой передачи записываются в журнал GeoIP, а данные устройства - в журнал `lines_live`:
+
+|Колонка|Источник|
+| --- | --- |
+| `geoip_country_code` | `GeoIPService::getIPInfo()` |
+| `isp` |`con_isp_name` из `GeoIPService::getISP()`|
+| `external_device` |идентификатор типа устройства|
+| `user_agent` |Заголовок HTTP User-Agent|
+| `user_ip` |IP-адрес клиента|
+
+Вошел в систему `live.php`, `vod.php`, `timeshift.php`, и `rtmp.php`.
+
+Периодически архивируется с `lines_live` на `lines_activity` с помощью `ActivityCronJob`.
+
+---
+
+## Конфигурация
+
+### Настройки
+
+|Установка|Тип|Описание|
+| --- | --- | --- |
+| `show_isps` | `0/1` |включить обнаружение интернет-провайдера|
+| `enable_isp_lock` | `0/1` |включить принудительную привязку к провайдеру|
+| `block_svp` | `0/1` |заблокировать VPN/прокси/сервер (проверка ASN)|
+| `block_streaming_servers` | `0/1` |блокировать IP-адреса центров обработки данных|
+| `block_proxies` | `0/1` |блокировать IP-адреса прокси-провайдеров|
+| `county_override_1st` | `0/1` |автоматическое назначение forced_country при первом подключении|
+| `allow_countries` | `array` |белый список разрешенных кодов стран|
+| `detect_restream_block_user` | `0/1` |автоматическое отключение пользователя при обнаружении повторного потока|
+| `disallow_empty_user_agents` | `0/1` |отклонять запросы без использования User-Agent|
+| `maxmind_account_id` | `string` |Учетная запись MaxMind API|
+| `maxmind_license_key` | `string` |API-ключ MaxMind|
+| `maxmind_editions` | `JSON` |множество загруженных изданий|
+
+---
+
+## Связанные файлы
+
+|Файл|Цель|
+| --- | --- |
+| `src/Core/Util/GeoIP.php` |низкоуровневый поиск GeoIP с кэшированием файлов|
+| `src/Core/GeoIP/GeoIPService.php` |соответствие высокого уровня GeoIP + CIDR|
+| `src/Core/GeoIP/MaxMindUpdater.php` |Загрузчик баз данных MaxMind|
+| `src/Core/Config/Binaries.php` |GeoIP константы пути к файлу базы данных|
+| `src/Core/Device/MobileDetect.php` |Библиотека Mobile_Detect|
+| `src/Domain/Device/EnigmaService.php` |Управление STB Enigma2|
+| `src/Domain/Device/MagService.php` |Управление MAG STB|
+| `src/Domain/User/UserRepository.php` |GeoIP обогащение записей пользователей|
+| `src/www/stream/auth.php` |потоковая авторизация со всеми проверками местоположения / устройства|
+| `src/Streaming/Auth/StreamAuth.php` |Выбор сервера с поддержкой GeoIP|
+| `src/Streaming/Balancer/ProxySelector.php` |Выбор прокси-сервера с поддержкой GeoIP|
diff --git a/docs/ru/guides/input-validation.md b/docs/ru/guides/input-validation.md
new file mode 100644
index 00000000..8a54fb79
--- /dev/null
+++ b/docs/ru/guides/input-validation.md
@@ -0,0 +1,320 @@
+# Проверка и санитарная обработка входных данных
+
+XC_VM использует двухуровневую защиту для входящих данных запроса. Во-первых, программа глобальной очистки удаляет опасный контент со всех PHP суперглобальных ресурсов во время начальной загрузки, перед запуском любого кода приложения. Во-вторых, уровень проверки на уровне действия проверяет наличие обязательных полей перед выполнением бизнес-логики.
+
+Оба уровня реализованы в `src/Core/Validation/InputValidator.php`.
+
+---
+
+## Глобальный поток санитарной обработки
+
+Очистка выполняется автоматически во время начальной загрузки. Когда вызывается `LegacyInitializer::initCore()` (в `src/Core/Init/LegacyInitializer.php`), перед любым кодом контроллера или службы выполняются следующие действия:
+
+```
+LegacyInitializer::initCore()
+  |
+  +-- InputValidator::cleanGlobals($_GET)
+  +-- InputValidator::cleanGlobals($_POST)
+  +-- InputValidator::cleanGlobals($_SESSION)
+  +-- InputValidator::cleanGlobals($_COOKIE)
+  |
+  +-- $input = InputValidator::parseIncomingRecursively($_GET)
+  +-- RequestManager::set(InputValidator::parseIncomingRecursively($_POST, $input))
+```
+
+После выполнения этой последовательности все необработанные суперглобальные данные были обработаны на месте, и объединенные/очищенные данные GET+POST доступны через `RequestManager`.
+
+Контекст потоковой передачи (`LegacyInitializer::initStreaming()`) выполняет ту же последовательность очистки, используя класс `Request`, который предоставляет эквивалентные методы для пути начальной загрузки потоковой передачи.
+
+### Чистые глобальные значения(&$rData, $rIteration = 0)
+
+Рекурсивно обходит заданный суперглобальный массив и удаляет опасное содержимое. Применяется к `$_GET`, `$_POST`, `$_SESSION`, и `$_COOKIE`.
+
+Извлекает следующее из каждого скалярного значения:
+
+|Угроза|Узор удален|Замена|
+| --- | --- | --- |
+|Ввод нулевого байта|`\0` (chr 0)|удаленный|
+|Обход пути| `../` |`../` (в HTML-кодировке)|
+|Переопределение RTL (подмена пользовательского интерфейса)| `‮` |удаленный|
+
+Рекурсия ограничена 10 уровнями, чтобы предотвратить исчерпание стека из-за глубоко вложенных входных данных.
+
+### Синтаксический анализ выполняется рекурсивно(&$rData, $rInput, $rIteration = 0)
+
+Рекурсивно выполняет обработку данных GET и POST, применяя очистку ключей и значений к каждому листу. Для массивов выполняется рекурсия на глубину до 20 уровней. Для скалярных значений применяется `parseCleanKey()` к ключу и `parseCleanValue()` к значению.
+
+Объединенный результат (сначала ПОЛУЧИТЬ, затем опубликовать с наложением) сохраняется в `RequestManager` для использования на протяжении всего жизненного цикла запроса.
+
+### parseCleanKey($rKey)
+
+Очищает ключи массива, чтобы предотвратить внедрение с помощью имен ключей:
+
+1. URL-расшифровывает, а HTML-экранирует ключ (`htmlspecialchars(urldecode(...))`)
+2. Удаляет последовательности с двумя точками (`..` -> `''`)
+3. Удаляет маркеры стиля `__dunder__` с помощью регулярного выражения
+4. Проверяет соответствие допустимому набору символов: символы word, точки, дефисы, подчеркивания
+
+### Значение parseCleanValue($rValue)
+
+Выполняет очистку скалярных значений за несколько проходов:
+
+|Шаг|Что он делает|
+| --- | --- |
+|Неэкранированный пейзаж|`stripslashes()` и восстановите ` ` в исходное положение|
+|Нормализовать новые строки|Преобразовать `\r\n`, `\n\r`, `\r` в `\n`|
+|Защита HTML-комментариев|`` становится `-->`|
+|Нейтрализация тегов скрипта|` STATUS_INVALID_INPUT, 'data' => $data]
+    return $error;
+}
+```
+
+### Подтверждающие идентификаторы($ids)
+
+```php
+InputValidator::confirmIDs(array $ids): array
+```
+
+Фильтрует массив, чтобы он содержал только целые положительные идентификаторы. Любое значение, в котором `intval($id) <= 0` не используется. Широко используется в кодовой базе (более 30 сайтов для звонков) везде, где необходимо очистить списки идентификаторов, предоставленные пользователем, перед запросами к базе данных.
+
+```php
+$safeIds = InputValidator::confirmIDs($userSuppliedIds);
+// [1, 42, 7] -- negative, zero, and non-numeric values removed
+```
+
+---
+
+## Ссылка на действие проверки
+
+В методе `validate()` вместо названия действия используется оператор `switch`. Ниже действия сгруппированы по функциональным областям.
+
+### контент-менеджмент
+
+#### Потоки и каналы
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processStream` |`stream_display_name` ИЛИ `review` флаг ИЛИ `$_FILES['m3u_file']`|Любой из трех параметров удовлетворяет требованиям проверки|
+| `processChannel` |`stream_display_name` ИЛИ `review` флаг ИЛИ `$_FILES['m3u_file']`|Те же правила, что и в processStream|
+| `processRadio` |`stream_display_name` ИЛИ `review` флаг ИЛИ `$_FILES['m3u_file']`|Те же правила, что и в processStream|
+
+#### Фильмы / VOD
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processMovie` |`stream_display_name` ИЛИ `review` флаг ИЛИ `$_FILES['m3u_file']`|Те же правила, что и в processStream|
+
+#### Сериалы и эпизоды
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processSeries` | `title` |Требуется указать название серии|
+| `processEpisode` |`series` (непустой) И `season_num` (числовой) И (`multi` флаг ИЛИ `episode` числовой)|Сложная многопутевая проверка|
+
+### Организация
+
+#### Букеты
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processBouquet` | `bouquet_name` |Требуемый скаляр|
+| `reorderBouquet` | `stream_order_array` |Необходимо декодировать в массив JSON|
+| `sortBouquets` | `bouquet_order_array` |Необходимо декодировать в массив JSON|
+
+#### Категории
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processCategory` |`category_name`, `category_type`|Оба необходимых|
+| `orderCategories` | `categories` |Необходимо декодировать в массив JSON|
+
+#### Группы и коды
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processGroup` | `group_name` |Требуемый скаляр|
+| `processGroupLegacy` | `group_name` |То же, что и processGroup|
+| `processCode` | `code` |Требуемый скаляр|
+| `processPackage` | `package_name` |Требуемый скаляр|
+
+### EPG
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processEPG` |`epg_name`, `epg_file`|Оба необходимых|
+
+### Устройства и линии
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processMAG` | `mac` |Требуется MAC-адрес|
+| `processEnigma` | `mac` |Требуется MAC-адрес|
+| `setChannelOrder` | `stream_order_array` |Необходимо декодировать в массив JSON|
+
+### Профили
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processProfile` | `profile_name` |Требуемый скаляр|
+
+### Поставщики услуг
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processProvider` |`ip`, `port`, `username`, `password`, `name`|Все пять необходимых|
+| `processISP` | `isp` |Требуемый скаляр|
+| `processUA` | `user_agent` |Требуемый скаляр|
+
+### Безопасность
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `blockIP` | `ip` |Требуется IP-адрес|
+| `processRTMPIP` | `ip` |Требуется IP-адрес|
+
+### Управление сервером
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `processServer` |`server_name`, `server_ip`|Оба необходимых|
+| `processProxy` |`server_name`, `server_ip`|То же, что и processServer|
+| `installServer` |`ssh_port`, `root_password`|Оба необходимых|
+| `moveStreams` |`content_type`, `source_server`, `replacement_server`|Все три необходимых|
+| `replaceDNS` |`old_dns`, `new_dns`|Оба необходимых|
+| `orderServers` | `server_order` |Необходимо декодировать в массив JSON|
+
+### Папки с записями и просмотром
+
+|Действие|Обязательные для заполнения поля|Записи|
+| --- | --- | --- |
+| `scheduleRecording` |`title`, `source_id`|Оба необходимых|
+| `processWatchFolder` |`folder_type`, `selected_path`, `server_id`|Все три необходимых|
+
+### Массовые операции (полезная нагрузка в виде массива JSON)
+
+Для всех массовых операций в указанном поле требуется массив в кодировке JSON. Поле должно быть преобразовано в допустимый массив PHP.
+
+|Действие|Поле JSON|
+| --- | --- |
+| `massEditEpisodes` | `streams` |
+| `massEditMovies` | `streams` |
+| `massEditRadios` | `streams` |
+| `massEditStreams` | `streams` |
+| `massEditChannels` | `streams` |
+| `massDeleteStreams` | `streams` |
+| `massEditSeries` | `series` |
+| `massDeleteSeries` | `series` |
+| `massEditLines` | `users_selected` |
+| `massEditUsers` | `users_selected` |
+| `massEditMags` | `devices_selected` |
+| `massEditEnigmas` | `devices_selected` |
+| `massDeleteMovies` | `movies` |
+| `massDeleteLines` | `lines` |
+| `massDeleteUsers` | `users` |
+| `massDeleteStations` | `radios` |
+| `massDeleteMags` | `mags` |
+| `massDeleteEnigmas` | `enigmas` |
+| `massDeleteEpisodes` | `episodes` |
+
+---
+
+## Аварийное поведение по умолчанию
+
+Действия, явно не указанные в инструкции `switch`, попадают в `return true`, что означает, что они всегда проходят проверку. Это сделано намеренно - эти действия либо не содержат обязательных полей на уровне gate, либо выполняют свою собственную проверку на более глубоком уровне бизнес-логики.
+
+**Явно выполняемые сквозные действия** (перечислены в переключателе с `return true`):
+
+- `processUser`
+- `processLine`
+- `processHMAC`
+- `editAdminProfile`
+- `editSettings`
+- `editBackupSettings`
+- `editCacheCron`
+- `editPlexSettings`
+- `editWatchSettings`
+- `processPlexSync`
+- `processLogin`
+- `submitTicket`
+
+**Неявно передаваемые действия** (вообще не в переключателе, перехватываются по умолчанию `return true`):
+
+Любая строка действия, не соответствующая `case`, также вернет `true`. Если для нового действия требуется стробирование ввода, необходимо явно добавить регистр.
+
+---
+
+## Используемые шаблоны проверки
+
+Метод `validate()` последовательно использует небольшой набор шаблонов:
+
+|Шаблон|Цель|Пример|
+| --- | --- | --- |
+| `!empty($rData['field'])` |Обязательное скалярное поле (ненулевое, непустое, ненулевое значение)| `!empty($rData['bouquet_name'])` |
+| `is_numeric($rData['field'] ?? null)` |Числовая проверка с резервным копированием, безопасным для null| `is_numeric($rData['season_num'] ?? null)` |
+| `is_array(json_decode($rData['field'] ?? '', true))` |Строка JSON, которую необходимо декодировать в массив| `is_array(json_decode($rData['streams'] ?? '', true))` |
+| `isset($rData['field'])` |Проверка наличия поля (значение может быть пустым/ложным)| `isset($rData['review'])` |
+| `isset($_FILES['field'])` |Проверка наличия загружаемого файла| `isset($_FILES['m3u_file'])` |
+|ИЛИ условия|Многопутевая проверка (удовлетворяет любой из путей)|`!пусто($rData['имя_потока']) \|\|isset($rData['обзор']) \|\|isset($_FILES['m3u_file'])`|
+
+---
+
+## Добавление проверки для нового действия
+
+Добавьте `case` к `switch` в `src/Core/Validation/InputValidator.php`:
+
+```php
+case 'myNewAction':
+    return !empty($rData['required_field'])
+        && is_numeric($rData['numeric_field'] ?? null);
+```
+
+Методические рекомендации:
+
+- На этом уровне проверяйте только минимально необходимые входные данные. Сохраняйте правила, относящиеся к предметной области (проверка формата, бизнес-ограничения, проверка уникальности), на уровне сервиса.
+- Используйте `!empty()` для требуемых скаляров, `is_numeric()` для числовых полей и `is_array(json_decode(..., true))` для полезной нагрузки массива JSON.
+- Для действий, которые принимают загрузку файлов в качестве альтернативы полям формы, укажите `isset($_FILES['field'])` в качестве условия ИЛИ.
+- Если действие не требует проверки на уровне шлюза, добавьте его в явный сквозной блок с `return true`, чтобы будущие разработчики знали, что это упущение является намеренным, а не случайным.
+
+---
+
+## Связанные файлы
+
+|Файл|Цель|
+| --- | --- |
+| `src/Core/Validation/InputValidator.php` |Вся логика санитарной обработки и проверки|
+| `src/Core/Init/LegacyInitializer.php` |Вызывающий элемент Bootstrap, запускающий очистку с помощью `initCore()`|
+| `src/Core/Http/RequestManager.php` |Хранит обработанные, объединенные данные GET+POST|
+| `src/Public/Controllers/` |Контроллеры, которые вызывают `validate()` / `validateOrFail()` перед бизнес-логикой|
diff --git a/docs/ru/guides/ministra-browser-emulation.md b/docs/ru/guides/ministra-browser-emulation.md
new file mode 100644
index 00000000..a7a7c4a7
--- /dev/null
+++ b/docs/ru/guides/ministra-browser-emulation.md
@@ -0,0 +1,136 @@
+# Ministra: Эмуляция STB-интерфейса браузера
+
+Краткое руководство по запуску Ministra в обычном браузере (без реальной телевизионной приставки MAG) и диагностике ошибки `your device is not active`.
+
+---
+
+## Когда использовать
+
+- Вам нужно подтвердить `index.html` загрузку и `portal.php` вызовы с помощью пути к токену.
+- Вам нужно быстро отладить handshake/get_profile в браузере.
+- Вам нужно понять, почему устройство получает статус неактивного.
+
+---
+
+## Поддерживаемые URL-адреса
+
+Обычно используются два варианта (в зависимости от nginx config):
+
+- `http://HOST/ACCESS_CODE/`
+- `http://HOST/c/`
+
+Для эмуляции браузера важно, чтобы `index.html` открывался, а шаги API переходили к `portal.php` внутри того же префикса.
+
+Для эмулятора STB точкой входа также должен быть базовый префикс (или `portal.php` без параметров запроса), а не предварительно созданный запрос `action=handshake`.
+
+---
+
+## Черный экран в Эмуляторе STB
+
+Если эмулятор настроен с использованием URL-адреса, подобного:
+
+```text
+http://HOST/ACCESS_CODE/portal.php?type=stb&action=handshake&mac=...&token=&prehash=...&JsHttpRequest=1-xml
+```
+
+это ошибка конфигурации. Такой URL-адрес возвращает JSON-файл для подтверждения связи и не является экраном портала, поэтому эмулятор обычно показывает черный экран.
+
+Используйте один из этих вариантов:
+
+- `http://HOST/ACCESS_CODE/`
+- `http://HOST/c/`
+- `http://HOST/ACCESS_CODE/portal.php` (только если эмулятору требуется прямой путь `portal.php`, но без `type/action/token/prehash` в URL)
+
+Параметры `type`, `action`, `JsHttpRequest`, `token`, и `prehash` должны генерироваться самим клиентом (эмулятором) на каждом шаге API.
+
+---
+
+## Необходимые параметры ПОЛУЧЕНИЯ
+
+Если включена проверка устройств, явно передавайте идентификаторы:
+
+|Параметр|Цель|
+| --- | --- |
+| `mac` |Устройство MAC для установления связи|
+| `sn` |Серийный номер устройства|
+| `stb_type` |Модель STB (например, `MAG250`)|
+| `device_id` |Первый идентификатор устройства|
+| `device_id2` |Второй идентификатор устройства|
+| `hw_version` |Версия оборудования|
+
+Без этих значений сервер может отклонить `get_profile` как недопустимое устройство.
+
+---
+
+## Полезные параметры ПОЛУЧЕНИЯ
+
+|Параметр|Когда Это Необходимо|
+| --- | --- |
+| `debug_key` |Обойти ограничение `allowed_stb_types` на стороне клиента|
+| `ver` |Если включена блокировка изображения/версии|
+| `image_version` |Если включено сравнение версий изображений|
+| `access_token` |Чтобы протестировать повторный вход в систему с помощью предварительно созданного токена|
+| `auth_via_query` |Режим отладки: дублирование токена в запросе, если заголовок авторизации удален прокси-сервером/nginx|
+| `debug` |Режим отладки клиента (включен по умолчанию в текущей сборке)|
+
+---
+
+## Пример URL-адреса
+
+```text
+http://192.168.110.251/HgBjUjSI/?mac=00:1A:79:11:22:33&sn=062014N000001&stb_type=MAG250&device_id=ABC123&device_id2=DEF456&hw_version=1.7-BD-00&debug_key=1
+```
+
+При необходимости добавьте:
+
+```text
+&ver=ImageDescription%3Aemu&image_version=218&auth_via_query=1
+```
+
+Для настройки профиля STB Emulator укажите только URL-адрес портала, например:
+
+```text
+http://192.168.110.251/HgBjUjSI/
+```
+
+---
+
+## Что означает `your device is not active`
+
+Сообщение появляется, когда в ответе профиля содержится `status = 1` (сбой аутентификации/проверки подлинности устройства).
+
+Общие причины:
+
+1. MAC не найден в `mag_devices`.
+2. `sn`, `device_id`, `device_id2`, `hw_version` не совпадают, когда `lock_device = 1`.
+3. Модель STB не проходит через белый список `allowed_stb_types`.
+4. Токен подтверждения связи недействителен или отклонен на `get_profile`.
+
+---
+
+## Краткий контрольный список
+
+1. Откройте портал с правильным префиксом (`/ACCESS_CODE/` или `/c/`).
+2. В эмуляторе STB не используйте URL с `action=handshake` в качестве URL портала.
+3. Заполните все обязательные поля (`mac`, `sn`, `stb_type`, `device_id`, `device_id2`, `hw_version`).
+4. Подтверждающее подтверждение возвращает токен, а следующий `get_profile` отправляет предъявителю авторизации.
+5. Если авторизация не достигает PHP, временно включите `auth_via_query=1`.
+6. Если применяются ограничения по типу STB, добавьте `debug_key=1`.
+7. Если проблема не устранена, проверьте строку устройства в базе данных (`mag_devices`) и отметьте `lock_device` флагом.
+
+---
+
+## Рекомендации по внедрению
+
+- Параметры клиента и вызовы API: `src/Ministra/xpcom.common.js`
+- Инициализация параметра Debug/get: `src/Ministra/index.html`
+- Проверка устройств и профиль на стороне сервера: `src/Ministra/portal.php`
+- Согласование рукопожатия/get_profile: `src/Ministra/PortalHandler.php`
+
+## Связанные файлы
+
+|Файл|Роль|
+| --- | --- |
+| `src/Ministra/portal.php` |Ministra точка входа в портал|
+| `src/Ministra/index.html` |Эмулированная страница браузера STB|
+| `src/Ministra/PortalHandler.php` |Обработчик запросов портала|
diff --git a/docs/ru/guides/permissions-and-rbac.md b/docs/ru/guides/permissions-and-rbac.md
new file mode 100644
index 00000000..6e4100e2
--- /dev/null
+++ b/docs/ru/guides/permissions-and-rbac.md
@@ -0,0 +1,415 @@
+# Разрешения и RBAC
+
+XC_VM комбинации контроля доступа:
+
+- **Групповые разрешения** -- разрешенные возможности, назначенные группе администраторов
+- **Авторизация на уровне объекта** -- проверка прав собственности для конкретных объектов (пользователей, строк)
+- **Авторизация на уровне страницы** -- настройка маршрута/страницы в панелях администратора и реселлера
+
+---
+
+## Модель
+
+```text
+user -> member_group_id -> group
+         -> is_admin (boolean)
+         -> is_reseller (boolean)
+         -> advanced[] (array of permission keys)
+```
+
+Состояние разрешений загружается в `$rPermissions` global во время инициализации сеанса и остается доступным на протяжении всего жизненного цикла запроса.
+
+Ключевые поля в `$rPermissions`:
+
+|Поле|Тип|Описание|
+| --- | --- | --- |
+| `is_admin` |тип bool|Является ли пользователь администратором|
+| `advanced` |массив|Список предоставленных ключевых строк разрешений|
+| `all_reports` |массив|Дерево отчетов реселлера (идентификаторы пользователей, которыми управляет этот реселлер)|
+| `create_line` |тип bool|Реселлер: может создавать строки|
+| `create_sub_resellers` |тип bool|Реселлер: может создавать суб-реселлеров|
+| `create_mag` |тип bool|Реселлер: может создавать магнитные устройства|
+| `create_enigma` |тип bool|Реселлер: может создавать устройства Enigma|
+| `can_view_vod` |тип bool|Реселлер: может просматривать содержимое VOD/streams|
+| `reseller_client_connection_logs` |тип bool|Реселлер: может просматривать журналы подключений|
+
+---
+
+## Ключи разрешений
+
+Ключи разрешений объявляются в `src/config/permissions.php` как массив `$rPermissionKeys`. Каждый ключ представляет собой строковый идентификатор, используемый с `Authorization::check('adv', $key)`.
+
+Категории:
+
+|Категория|Примеры|
+| --- | --- |
+|Создать/Добавить|`add_stream`, `add_movie`, `add_user`, `add_server`, `add_bouquet`, `add_epg`, `add_code`, `add_hmac`, `add_rtmp`|
+|Редактировать|`edit_stream`, `edit_movie`, `edit_user`, `edit_server`, `edit_bouquet`, `edit_series`, `edit_reguser`|
+|Массовые операции|`mass_edit_streams`, `mass_edit_lines`, `mass_edit_mags`, `mass_edit_enigmas`, `mass_edit_radio`, `mass_edit_users`, `mass_sedits`, `mass_sedits_vod`, `mass_delete`|
+|Импорт|`import_streams`, `import_movies`, `import_episodes`|
+|Безопасность/блокирование|`block_ips`, `block_isps`, `block_uas`, `block_asns`, `fingerprint`|
+|Видимость раздела|`streams`, `movies`, `series`, `episodes`, `radio`, `users`, `servers`, `bouquets`, `epg`, `settings`, `database`|
+|Бревна|`connection_logs`, `live_connections`, `client_request_log`, `credits_log`, `login_logs`, `panel_logs`, `reg_userlog`, `restream_logs`|
+|Инструменты|`quick_tools`, `stream_tools`, `process_monitor`, `stream_errors`|
+|Управление|`mng_regusers`, `mng_groups`, `mng_packages`, `manage_mag`, `manage_e2`, `manage_events`, `manage_tickets`|
+|Другой|`categories`, `channel_order`, `player`, `tprofile`, `tprofiles`, `rtmp`, `folder_watch`, `folder_watch_add`, `folder_watch_output`, `folder_watch_settings`, `ticket`, `add_code`, `add_hmac`|
+
+---
+
+## Классы авторизации
+
+### `Authorization`
+
+Файл: `src/Core/Auth/Authorization.php`
+
+Первичный метод:
+
+```php
+Authorization::check(string $type, mixed $id): bool
+```
+
+**Предварительные условия:** Возвращает `false` немедленно, если `$rUserInfo`, `$rPermissions`, или `$db` не инициализированы.
+
+#### Тип: `user`
+
+Проверяет, может ли текущий пользователь получить доступ к целевому пользователю-администратору. Создает список из идентификатора текущего пользователя плюс его дерева `all_reports`, затем запрашивает таблицу `users`, чтобы убедиться, что целевой пользователь `owner_id` находится в этом списке (или целью является текущий пользователь).
+
+```php
+Authorization::check('user', $userId);
+```
+
+#### Тип: `line`
+
+Проверяет, может ли текущий пользователь получить доступ к целевой строке. Тот же подход к дереву отчетов - запрашивает таблицу `lines`, чтобы убедиться, что целевая строка `member_id` находится в дереве отчетов текущего пользователя.
+
+```php
+Authorization::check('line', $lineId);
+```
+
+#### Тип: `adv`
+
+Проверяет, есть ли у текущего пользователя-администратора определенный ключ расширенных прав доступа.
+
+```php
+Authorization::check('adv', 'edit_bouquet');
+Authorization::check('adv', 'block_isps');
+```
+
+**Важно: `is_admin` gate.** Перед проверкой массива расширенных разрешений метод требует, чтобы значение `$rPermissions['is_admin']` было равно true. Если пользователь не является администратором, `check('adv', ...)` всегда возвращает `false`:
+
+```php
+if (!($rType == 'adv' && $rPermissions['is_admin'])) {
+    return false;
+}
+```
+
+Это означает, что проверки `adv` предназначены исключительно для пользователей с правами администратора. Для разрешений реселлеров используется отдельная система (см. ниже).
+
+#### Обход прав суперадминистратора
+
+`member_group_id = 1` - это группа суперадминистраторов. Если массив расширенных разрешений непустой, но пользователь принадлежит к группе 1, проверка для каждого ключа пропускается и метод возвращает значение `true`:
+
+```php
+if (0 < count($rPermissions['advanced']) && $rUserInfo['member_group_id'] != 1) {
+    return in_array($rID, $rPermissions['advanced']);
+}
+return true;
+```
+
+Это означает, что суперадминистраторы проходят все проверки `adv` независимо от того, какие ключи назначены их группе.
+
+#### Помощник реселлера
+
+```php
+Authorization::hasResellerPermissions(string $type): bool
+```
+
+Возвращает, не является ли значение `$rPermissions[$type]` непустым. Используется для логических флагов, специфичных для реселлера, таких как `create_line`, `create_mag`, и т.д.
+
+---
+
+### `PageAuthorization`
+
+Файл: `src/Core/Auth/PageAuthorization.php`
+
+Обеспечивает управление на уровне страницы для панелей администратора и торгового посредника. Вызывается во время отправки запроса, чтобы определить, может ли текущий пользователь получить доступ к данной странице.
+
+```php
+PageAuthorization::checkPermissions(?string $page = null): bool
+PageAuthorization::checkResellerPermissions(?string $page = null): bool
+```
+
+Если `$page` опущено, название страницы выводится из `SCRIPT_FILENAME` (базовое имя без расширения `.php`, в нижнем регистре).
+
+#### Поведение, разрешенное по умолчанию
+
+Оба метода возвращают значение `true` для любой страницы, явно не указанной в их инструкциях switch. Это означает, что страницы без сопоставления доступны всем авторизованным пользователям соответствующего типа (администраторам или торговым посредникам). Доступ ограничен только к страницам с явно указанными записями.
+
+---
+
+## Сопоставления разрешений на странице администратора
+
+Метод `checkPermissions()` сопоставляет страницы панели администратора с ключами доступа `adv`. Ниже приведено полное сопоставление, сгруппированное по категориям.
+
+### Шаблон для создания и редактирования
+
+Многие страницы сущностей используют условную логику, основанную на параметрах запроса:
+
+- Если указан параметр `id`, проверяется разрешение **редактировать**
+- Если параметр `id` отсутствует, проверяется разрешение **добавить**
+- Некоторые страницы (stream, movie) также проверяют наличие параметра `import` и требуют соответствующего разрешения на импорт
+
+Когда ни одно из условий не выполняется, поведение зависит от страницы: некоторые переходят к соответствующему разрешению на перечисление, другие переходят к переключателю по умолчанию (который возвращает `true`).
+
+### Потоки и контент
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+|`streams`, `stream_view`, `provider`, `providers`, `epg_view`, `created_channels`, `stream_rank`, `archive`| `streams` | |
+| `stream` | `edit_stream` |Когда присутствует `id`|
+| `stream` | `add_stream` |Когда нет `id`|
+| `stream` | `import_streams` |Когда присутствует параметр `import` (в дополнение к параметру `add_stream`)|
+| `stream_categories` | `categories` | |
+| `stream_category` | `add_cat` | |
+| `stream_errors` | `stream_errors` | |
+|`stream_mass`, `created_channel_mass`| `mass_edit_streams` | |
+| `mass_edit_streams` | `edit_stream` | |
+| `review` | `import_streams` | |
+| `channel_order` | `channel_order` | |
+| `created_channel` | `edit_cchannel` |Когда присутствует `id`|
+| `created_channel` | `create_channel` |Когда нет `id`|
+
+### Фильмы и VOD
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `movies` | `movies` | |
+| `movie` | `edit_movie` |Когда присутствует `id`|
+| `movie` | `add_movie` |Когда нет `id`|
+| `movie` | `import_movies` |Когда присутствует параметр `import` (в дополнение к параметру `add_movie`)|
+| `movie_mass` | `mass_sedits_vod` | |
+| `record` | `add_movie` | |
+| `recordings` | `movies` | |
+
+### Сериалы и эпизоды
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `series` | `series` | |
+| `serie` | `edit_series` |Когда присутствует `id`|
+| `serie` | `add_series` |Когда нет `id`|
+| `series_order` | `edit_series` | |
+| `episodes` | `episodes` | |
+| `episode` | `edit_episode` |Когда присутствует `id`|
+| `episode` | `add_episode` |Когда нет `id`; при отказе переключается на `episodes`|
+|`series_mass`, `episodes_mass`| `mass_sedits` | |
+
+### Радио
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `radios` | `radio` | |
+| `radio` | `edit_radio` |Когда присутствует `id`|
+| `radio` | `add_radio` |Когда нет `id`|
+| `radio_mass` | `mass_edit_radio` | |
+
+### Линии (Абонентские пользователи)
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `lines` | `users` | |
+| `line` | `edit_user` |Когда присутствует `id`|
+| `line` | `add_user` |Когда нет `id`|
+| `line_mass` | `mass_edit_lines` | |
+|`line_activity`, `theft_detection`, `line_ips`| `connection_logs` | |
+| `live_connections` | `live_connections` | |
+
+### Устройства MAG и Enigma
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `mags` | `manage_mag` | |
+| `mag` | `edit_mag` |Когда присутствует `id`|
+| `mag` | `add_mag` |Когда нет `id`|
+| `mag_events` | `manage_events` | |
+| `mag_mass` | `mass_edit_mags` | |
+| `enigmas` | `manage_e2` | |
+| `enigma_mass` | `mass_edit_enigmas` | |
+
+### Пользователи с правами администратора (Зарегистрированные пользователи)
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `users` | `mng_regusers` | |
+| `user` | `edit_reguser` |Когда присутствует `id`|
+| `user` | `add_reguser` |Когда нет `id`|
+| `user_mass` | `mass_edit_users` | |
+| `user_logs` | `reg_userlog` | |
+
+### Букеты и посылки
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `bouquets` | `bouquets` | |
+| `bouquet` | `edit_bouquet` |Когда присутствует `id`|
+| `bouquet` | `add_bouquet` |Когда нет `id`; при отказе переключается на `edit_bouquet`|
+|`bouquet_order`, `bouquet_sort`| `edit_bouquet` | |
+|`packages`, `addons`| `mng_packages` | |
+| `package` | `edit_package` |Когда присутствует `id`|
+| `package` | `add_packages` |Когда нет `id`|
+
+### Группы
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `groups` | `mng_groups` | |
+| `group` | `edit_group` |Когда присутствует `id`|
+| `group` | `add_group` |Когда нет `id`; при отказе переключается на `mng_groups`|
+
+### EPG
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `epgs` | `epg` | |
+| `epg` | `epg_edit` |Когда присутствует `id`|
+| `epg` | `add_epg` |Когда нет `id`; при отказе переключается на `epg`|
+
+### Серверы
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+|`servers`, `server_view`, `server_order`, `proxies`| `servers` | |
+|`server`, `proxy`| `edit_server` |Когда присутствует `id`|
+|`server`, `proxy`| `add_server` |Когда нет `id`|
+| `server_install` | `add_server` | |
+
+### Безопасность и блокирование
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+|`isps`, `isp`, `asns`| `block_isps` | |
+|`ip`, `ips`| `block_ips` | |
+|`useragents`, `useragent`| `block_uas` | |
+| `fingerprint` | `fingerprint` | |
+
+### Билеты
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `ticket` | `ticket` | |
+|`ticket_view`, `tickets`| `manage_tickets` | |
+
+### Инструменты и настройки
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `settings` | `settings` | |
+|`backups`, `cache`, `setup`| `database` | |
+|`settings_watch`, `settings_plex`| `folder_watch_settings` | |
+|`plex`, `watch`| `folder_watch` | |
+|`plex_add`, `watch_add`| `folder_watch_add` | |
+| `watch_output` | `folder_watch_output` | |
+| `mass_delete` | `mass_delete` | |
+| `quick_tools` | `quick_tools` | |
+| `stream_tools` | `stream_tools` | |
+| `process_monitor` | `process_monitor` | |
+| `queue` |`streams` ИЛИ `episodes` ИЛИ `series`|Доступ, если у пользователя есть какой-либо из этих|
+
+### Профили и коды
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `profiles` | `tprofiles` | |
+| `profile` | `tprofile` | |
+| `player` | `player` | |
+|`code`, `codes`| `add_code` | |
+|`hmac`, `hmacs`| `add_hmac` | |
+
+### RTMP
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `rtmp_ip` | `add_rtmp` | |
+|`rtmp_ips`, `rtmp_monitor`| `rtmp` | |
+
+### Бревна
+
+|Страница|Разрешение|Записи|
+| --- | --- | --- |
+| `client_logs` | `client_request_log` | |
+| `credit_logs` | `credits_log` | |
+|`mysql_syslog`, `panel_logs`| `panel_logs` | |
+| `login_logs` | `login_logs` | |
+| `restream_logs` | `restream_logs` | |
+
+---
+
+## Сопоставления разрешений на странице реселлера
+
+Метод `checkResellerPermissions()` сопоставляет страницы панели реселлера с логическими флагами в `$rPermissions`. В отличие от разрешений администратора, которые используют массив `advanced` через `Authorization::check('adv', ...)`, разрешения реселлера - это простые логические поля, которые проверяются напрямую.
+
+|Страницы|Требуемое разрешение|
+| --- | --- |
+|`user`, `users`| `create_sub_resellers` |
+|`line`, `lines`| `create_line` |
+|`mag`, `mags`| `create_mag` |
+|`enigma`, `enigmas`| `create_enigma` |
+|`epg_view`, `streams`, `created_channels`, `movies`, `episodes`, `radios`| `can_view_vod` |
+|`live_connections`, `line_activity`| `reseller_client_connection_logs` |
+
+Любая страница реселлера, не указанная выше, возвращает `true` (доступна по умолчанию).
+
+---
+
+## Добавление нового разрешения
+
+1. Добавьте ключ к `src/config/permissions.php`:
+
+```php
+$rPermissionKeys = array(
+    // ...existing keys...
+    'my_new_permission',
+);
+```
+
+2. Используйте это в коде через `Authorization::check()`:
+
+```php
+if (!Authorization::check('adv', 'my_new_permission')) {
+    // deny access
+}
+```
+
+3. Если разрешение должно открывать доступ к странице, добавьте обращение к `PageAuthorization::checkPermissions()`:
+
+```php
+case 'my_new_page':
+    return Authorization::check('adv', 'my_new_permission');
+```
+
+4. Для создания/редактирования страниц сущностей используйте условный шаблон:
+
+```php
+case 'my_entity':
+    if (isset(RequestManager::getAll()['id']) && Authorization::check('adv', 'edit_my_entity')) {
+        return true;
+    }
+    if (isset(RequestManager::getAll()['id']) || !Authorization::check('adv', 'add_my_entity')) {
+        break;
+    }
+    return true;
+```
+
+5. Для получения разрешений торгового посредника добавьте логическое поле в `$rPermissions` и регистр в `checkResellerPermissions()`.
+
+---
+
+## Связанные файлы
+
+|Файл|Цель|
+| --- | --- |
+| `src/config/permissions.php` |Реестр ключей разрешений (`$rPermissionKeys` array)|
+| `src/Core/Auth/Authorization.php` |Проверка разрешений на уровне объекта и расширенные проверки разрешений|
+| `src/Core/Auth/PageAuthorization.php` |Настройка на уровне страницы для панелей администратора и реселлера|
+| `src/Core/Auth/SessionManager.php` |Контекст сеанса; заполняет `$rPermissions` и `$rUserInfo`|
+| `src/Core/Auth/Authenticator.php` |Аутентификация (вход в систему, проверка учетных данных)|
diff --git a/docs/ru/guides/phpunit-phar.md b/docs/ru/guides/phpunit-phar.md
new file mode 100644
index 00000000..28ecf385
--- /dev/null
+++ b/docs/ru/guides/phpunit-phar.md
@@ -0,0 +1,82 @@
+# PHPUnit PHAR: Загрузка и запуск тестов
+
+В этом руководстве показано, как запускать тесты проекта с фиксированным двоичным кодом PHP:
+
+- PHP двоичный код: `/home/xc_vm/bin/php/bin/php`
+- Двоичный файл PHPUnit: local `tools/.bin/phpunit.phar`
+
+## Почему такая установка
+
+`phpunit.phar` не включает PHP. Он всегда выполняется через интерпретатор PHP.
+
+Если вы запустите `./phpunit.phar` напрямую, он может использовать другой `php` из `PATH`.
+Используйте явный двоичный путь для обеспечения согласованности во время выполнения.
+
+## 1. Проверить PHP
+
+```bash
+/home/xc_vm/bin/php/bin/php -v
+```
+
+## 2. Скачать PHPUnit PHAR
+
+Этот проект привязан к PHP 8.1, поэтому используйте PHPUnit 10.
+
+```bash
+cd /home/xc_vm
+mkdir -p tools/.bin
+wget -O tools/.bin/phpunit.phar https://phar.phpunit.de/phpunit-10.phar
+chmod +x tools/.bin/phpunit.phar
+```
+
+## 3. Проверьте PHPUnit
+
+```bash
+/home/xc_vm/bin/php/bin/php tools/.bin/phpunit.phar --version
+```
+
+## 4. Выполните все тесты
+
+Конфигурационный файл проекта:
+
+- `tests/phpunit.xml.dist`
+
+Бежать:
+
+```bash
+/home/xc_vm/bin/php/bin/php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist
+```
+
+## 5. Запустите один тестовый файл
+
+```bash
+/home/xc_vm/bin/php/bin/php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist tests/Unit/GitHubReleasesTest.php
+```
+
+## 6. Показать, какой тест выполняется сейчас
+
+Используйте режим отладки для печати текущего теста:
+
+```bash
+/home/xc_vm/bin/php/bin/php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist --debug --no-progress
+```
+
+## 7. Дополнительно: Выход покрытия
+
+Если установлен `xdebug` или `pcov`:
+
+```bash
+XDEBUG_MODE=coverage /home/xc_vm/bin/php/bin/php tools/.bin/phpunit.phar -c tests/phpunit.xml.dist --coverage-text
+```
+
+## Защитная записка
+
+Не фиксируйте `phpunit.phar` в репозитории. Сохраняйте его локальным (`tools/.bin`) и обновляйте независимо.
+
+## Связанные файлы
+
+|Файл|Роль|
+| --- | --- |
+| `tools/.bin/phpunit.phar` |Закрепленный двоичный файл PHPUnit|
+| `tests/phpunit.xml.dist` |Конфигурация PHPUnit|
+| `tests/bootstrap.php` |Тестовый bootstrap (Composer автозагрузчик + константы)|
diff --git a/docs/ru/guides/process-management.md b/docs/ru/guides/process-management.md
new file mode 100644
index 00000000..1adb383f
--- /dev/null
+++ b/docs/ru/guides/process-management.md
@@ -0,0 +1,101 @@
+# Модели управления процессами
+
+`ProcessManager` централизует проверку процессов Linux, завершение работы, проверку PID-файлов и блокировку cron.
+Он заменяет разрозненные специальные проверки `posix_kill`, `ps`, и `/proc`.
+
+---
+
+## Основные операции
+
+### Проверьте, запущен ли процесс
+
+```php
+ProcessManager::isRunning(int $pid, ?string $exe = null): bool
+```
+
+- без `$exe`: проверяет наличие `/proc/{pid}`
+- с помощью `$exe`: проверяет имя исполняемого файла с помощью `/proc/{pid}/exe`
+
+### Проверьте именованный процесс
+
+```php
+ProcessManager::isNamedProcessRunning(
+    int $pid,
+    string $processName,
+    int|string $identifier,
+    ?string $exe = null
+): bool
+```
+
+Соответствует шаблону командной строки `NAME[ID]` (для работников, основанных на названии процесса).
+
+### Проверьте потоковый процесс
+
+```php
+ProcessManager::isStreamRunning(int $pid, int $streamId): bool
+```
+
+- `ffmpeg`: проверяет шаблон вывода для конкретного потока в командной строке
+- `php`: считается активным для контекста stream worker
+
+---
+
+## Утилиты для работы с PID-файлами
+
+```php
+ProcessManager::checkPidFile(string $pidFile, string $searchString): bool
+ProcessManager::matchesCmdline(int $pid, string $search): bool
+```
+
+---
+
+## Завершение процесса
+
+```php
+ProcessManager::kill(int $pid, int $signal = SIGKILL): bool
+```
+
+Используйте `SIGTERM` для плавного завершения работы, когда это возможно.
+
+---
+
+## Блокировка Cron
+
+```php
+ProcessManager::acquireCronLock(string $pidFile, int $maxAge = 1800): void
+```
+
+Поведение:
+
+- активная блокировка -> выход из текущего режима
+- устаревший замок -> удален и заменен
+- очистка блокировки -> регистрация с помощью обратного вызова shutdown
+
+---
+
+## `/proc` Проверить кэш
+
+`isRunning()` использует короткое TTL-кэширование для проверки `/proc` (1 секунда), чтобы уменьшить количество повторных операций ввода-вывода в узких циклах.
+
+---
+
+## Соглашение об именовании
+
+Общий формат названия процесса:
+
+- `XC_VM[{id}]`
+- `Thumbnail[{id}]`
+- `TVArchive[{id}]`
+
+Используется вместе с помощниками по заголовку процесса CLI.
+
+---
+
+## Связанные файлы
+
+|Файл|Цель|
+| --- | --- |
+| `src/Core/Process/ProcessManager.php` |технологические операции|
+| `src/Core/Process/Multithread.php` |многопоточные помощники|
+| `src/Core/Process/Thread.php` |обертка для нитей|
+| `src/bootstrap.php` |Контекст процесса CLI|
diff --git a/docs/ru/guides/translations.md b/docs/ru/guides/translations.md
new file mode 100644
index 00000000..d6871d34
--- /dev/null
+++ b/docs/ru/guides/translations.md
@@ -0,0 +1,98 @@
+# Добавление пользовательского языка
+
+XC_VM использует файловую систему перевода. Каждый язык представляет собой отдельный файл `.ini` в каталоге `src/resources/langs/`. Добавление нового языка не требует изменения кода — просто создайте файл, и он автоматически появится в панели администратора.
+
+## быстрый старт
+
+1. Скопируйте файл ссылки на английском языке в качестве шаблона:
+
+```bash
+cp src/resources/langs/en.ini src/resources/langs/xx.ini
+```
+
+Замените `xx` на [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) код языка (например, `it` для итальянского, `pl` для польского, `ja` для японского).
+
+2. Откройте `xx.ini` и переведите значения (справа от `=`).:
+
+```ini
+[Language]
+a_to_z = "A to Z"          ; ← translate this
+access_code = "Access Code" ; ← translate this
+actions = "Actions"         ; ← translate this
+```
+
+3. Перейдите в раздел **Настройки → Интерфейс → Язык интерфейса** и выберите новый код языка.
+
+Вот и все. Перезагрузка не требуется.
+
+## Формат файла
+
+Каждый файл `.ini` соответствует этой структуре:
+
+```ini
+[Language]
+key = "Translated text"
+another_key = "Another translated text"
+```
+
+**Правила:**
+
+- Заголовок раздела `[Language]` является **обязательным** в первой строке.
+- Ключи являются идентификаторами `snake_case` — **не меняйте их **.
+- Значения должны быть заключены в **двойные кавычки**.
+- Строки отсортированы в алфавитном порядке по ключу для обеспечения согласованности.
+- Кодировка файла должна быть **UTF-8** (без спецификации).
+
+## как это работает
+
+|Шаг|Что происходит|
+|------|-------------|
+|Панель загрузки|`Translator::init()` сканирует `src/resources/langs/` на наличие `*.ini` файлов|
+|Список языков|`Translator::available()` возвращает все найденные языковые коды|
+|Выбор пользователя|Язык сохраняется в файле cookie `lang` (для каждого браузера) и в столбце `settings.language` DB (по умолчанию).|
+|Отсутствующий ключ|Если ключ перевода используется в коде, но отсутствует в вашем файле `.ini`, система ** автоматически добавит ** его с именем ключа в качестве значения по умолчанию|
+
+## Доступные языки
+
+|Код|Файл|
+|------|------|
+| `bg` |`bg.ini` — Болгарский|
+| `de` |`de.ini` — Немецкий|
+| `en` |`en.ini` — Английский (справочный)|
+| `es` |`es.ini` — Испанский|
+| `fr` |`fr.ini` — Французский|
+| `pt` |`pt.ini` — Португальский|
+| `ru` |`ru.ini` — Русский|
+
+## Чаевые
+
+- **Всегда используйте `en.ini` в качестве источника достоверности ** — он содержит все ключи. В других файлах могут отсутствовать ключи, которые автоматически заполняются во время выполнения.
+- **Автоматическое создание отсутствующих ключей**: если в вашем файле отсутствует ключ, `Translator` автоматически добавит `key = "key"` к вашему файлу. Затем вы сможете найти и перевести эти непереведенные записи.
+- **Поиск непереведенных ключей** — поиск строк, в которых ключ равен значению:
+
+```bash
+grep -P '^(\w+)\s*=\s*"\1"$' src/resources/langs/xx.ini
+```
+
+- ** Проверьте свой файл ** — убедитесь, что `parse_ini_file()` может его прочитать:
+
+```bash
+php -r "var_dump(parse_ini_file('src/resources/langs/xx.ini', false, INI_SCANNER_RAW));" | head -20
+```
+
+## Дополнительные переводы
+
+Внести свой вклад в перевод проекта:
+
+1. Разветвите репозиторий.
+2. Создайте свой языковой файл, как описано выше.
+3. Отправьте запрос на извлечение с новым файлом `.ini`.
+
+Пожалуйста, убедитесь, что все ключи из `en.ini` присутствуют и переведены.
+
+## Связанные файлы
+
+|Файл|Роль|
+| --- | --- |
+| `src/Core/Localization/Translator.php` |Поиск перевода|
+| `src/resources/langs/` |Языковые файлы|
diff --git a/docs/ru/guides/ucs-integration.md b/docs/ru/guides/ucs-integration.md
new file mode 100644
index 00000000..6bb2793f
--- /dev/null
+++ b/docs/ru/guides/ucs-integration.md
@@ -0,0 +1,75 @@
+# UCS Поддомены с подстановочными знаками интеграции для каждой строки
+
+Эта функция поддерживает интеграцию **UCS**, предоставляя каждому клиенту уникальный поддомен для балансировки нагрузки на основе его ** идентификатора строки ** при создании перенаправлений потоков.
+
+## Обзор
+
+Когда сервер балансировки нагрузки (LB) использует домен с подстановочным знаком и включена функция **Обслуживать случайный IP / домен**, XC_VM заменяет префикс `wildcard.` на аутентифицированный идентификатор строки перед перенаправлением клиента.
+
+|Идентификатор строки|Домен LB (настроен)|Перенаправление домена|
+|--------:|------------------------|-----------------|
+|10| `wildcard.example.com` | `10.example.com` |
+|42| `wildcard.lb1.com` | `42.lb1.com` |
+
+Каждая строка получает стабильный поддомен. UCS (или вышестоящий DNS/прокси-сервер) может перенаправлять `*.example.com` на нужный сервер или применять политики для каждой строки.
+
+## Требования
+
+1. **Сервер балансировки нагрузки** (не основной сервер), по крайней мере, с одним доменом, настроенным в соответствии с **Доменами и IP-адресами**.
+2. **Для этого сервера включена поддержка произвольного IP / домена** (флажок`random_ip` на странице редактирования сервера).
+3. Запись в домене, начинающаяся с **`wildcard.`** ( например, `wildcard.example.com`).
+4. DNS: подстановочная запись для базовой зоны (например, `*.example.com`), указывающая на LB или ваш UCS edge.
+
+## Установка
+
+1. Откройте **Серверы → Редактировать** на целевом сервере LB.
+2. В разделе **Домены и IP-адреса** добавьте `wildcard.example.com` (используйте свою реальную зону).
+3. Включить ** Обслуживание случайного IP / домена**.
+4. Сохраните сервер.
+5. Убедитесь, что DNS разрешает `{line_id}.example.com` для любого введенного вами идентификатора строки (wildcard DNS или UCS automation).
+
+Когда клиент проходит аутентификацию с помощью `auth.php`, XC_VM считывает идентификатор строки из `$rUserInfo['id']` и передает его в конструктор URL-адресов перенаправления.
+
+## Когда Это применимо
+
+Подстановка выполняется только в том случае, если ** все** из приведенного ниже значения являются истинными:
+
+- Запрос аутентифицирован, и доступен идентификатор строки.
+- **Для выбранного LB включена функция обслуживания случайного IP / домена**.
+- Случайно выбранный домен из списка доменов сервера содержит подстроку `wildcard.`.
+
+Если клиент подключился, используя соответствующий заголовок `Host`, существующий хост сохраняется, а подстановочный знак пропускается.
+
+## Пример потока
+
+1. Домены сервера LB: `wildcard.example.com`, `lb2.example.com`
+2. **Обслуживать случайный IP / домен**: включен
+3. Идентификатор клиентской линии: `10`
+4. Auth выбирает `wildcard.example.com` из списка доменов
+5. URL-адрес перенаправления хоста становится `10.example.com`
+
+## Реализация
+
+Логика живет в `StreamRedirector::getStreamingURL()`:
+
+```php
+// line_id : 10 => wildcard.lb1.com => 10.lb1.com
+if ($rUserID && strpos($rDomain, 'wildcard.') !== false) {
+    $rDomain = str_replace('wildcard.', $rUserID . '.', $rDomain);
+}
+```
+
+Вызывается с `Public/stream/auth.php` с помощью `$rUserID = $rUserInfo['id']` для прямого эфира, VOD, timeshift, и связанных с ним перенаправлений.
+
+## Записи
+
+- Используйте буквальный префикс **`wildcard.`** в строке домена; заменяется только этот сегмент.
+- Идентификатор строки - это внутренний идентификатор таблицы **lines** (`users.id` в потоковой авторизации), а не имя пользователя.
+- Подстановка подстановочных знаков применяется только к URL-адресам перенаправления LB; она не изменяет имена плейлистов или хостов API, если только эти пути также не вызывают `getStreamingURL()` с идентификатором строки.
+
+## Связанные файлы
+
+|Файл|Роль|
+| --- | --- |
+| `src/Streaming/Delivery/StreamRedirector.php` |`getStreamingURL()` подстановка подстановочных знаков|
+| `src/Public/stream/auth.php` |Вызывает конструктор перенаправления с использованием идентификатора строки|
diff --git a/docs/ru/info/faq.md b/docs/ru/info/faq.md
new file mode 100644
index 00000000..d1f7964b
--- /dev/null
+++ b/docs/ru/info/faq.md
@@ -0,0 +1,375 @@
+# FAQ - Часто задаваемые вопросы
+
+Здесь вы найдете ответы на наиболее распространенные вопросы и проблемы при работе с XC_VM.
+
+---
+
+## Проблемы с потоками
+
+
+❌ Моя трансляция не начинается на MAIN или LB + +--- + +### Диагностика + +Подключитесь к консоли вашего сервера и выполните следующую команду: + +```bash +sudo -u xc_vm /home/xc_vm/console.php monitor 291 +```` + +> 🧩 Где `291` - это ваш **идентификатор потока ** (замените его на свой собственный). + +--- + +### Что делает команда + +Команда **monitor** пытается запустить поток вручную и в случае сбоя выдает сообщение об ошибке. + +--- + +### Возможные причины + +#### 1️⃣ Отсутствующие системные библиотеки + +Если выходные данные содержат ошибку типа: + +``` +error while loading shared libraries: libxyz.so.1: cannot open shared object file +``` + +Установите отсутствующую библиотеку с помощью: + +```bash +sudo apt install +``` + +После установки запустите тест повторно. + +> 💬 Дайте мне знать, нужно ли добавить библиотеку в сценарий установки. + +--- + +#### 2️⃣ Ошибка, не связанная с библиотеками + +Если ошибка другого типа — пришлите ее вывод, чтобы я мог помочь ее диагностировать. + +--- + +### Резюме + +1. Запустите диагностическую команду. +2. Проверьте, нет ли каких-либо ошибок. +3. При необходимости установите отсутствующие библиотеки. +4. Сообщайте о любых других ошибках для дальнейшего анализа. + +--- + +
+ +
+❌ Сбой потоковой передачи из-за "IP_MISMATCH" или "TOKEN_EXPIRED" + +--- + +Это функции безопасности, а не баги: + +- **TOKEN_EXPIRED** — токен сеанса имеет ограничение по времени. Пользователю необходимо повторно пройти аутентификацию. +- **IP_MISMATCH** — IP-адрес пользователя изменился в середине потока (часто определяется как общий доступ к учетным данным). + +**Соответствующие настройки:** +- `restrict_same_ip` — насколько строго соблюдается соответствие IP-адресов +- `disallow_2nd_ip_con` — блокирует одновременные подключения с разных IP-адресов + +Если это вызывает проблемы у законных пользователей (например, мобильные сети часто меняют IP-адреса), измените уровень ограничений в настройках панели администратора. + +--- + +
+ +--- + +## Проблемы с логином и доступом + +
+❌ Я заблокирован — IP продолжает блокироваться + +--- + +@Защита от перебора @0@@ блокирует IP-адреса после слишком большого числа неудачных попыток входа в систему. Это контролируется: + +- `bruteforce_mac_attempts` — количество попыток для каждого MAC за определенный промежуток времени +- `bruteforce_username_attempts` — количество попыток для каждого пользователя за определенный промежуток времени +- `flood_limit` — общее количество запросов в окне + +** Чтобы разблокировать себя:** + +1. **Из панели администратора:** Сервис → Управление IP-адресами → удалить из списка заблокированных. +2. **Из интерфейса командной строки:** `sudo /home/xc_vm/console.php tools flush` — очищает все заблокированные IP-адреса. +3. **Если вы полностью заблокированы:** Используйте `console.php tools rescue` для создания аварийного кода доступа (см. [CLI Tools](../guides/cli-tools.md)). + +--- + +
+ +
+❌ Телеприставка блокируется после сброса настроек/смены прошивки (ее серийный номер или device_id изменились, но на панели установлен старый) + +--- + +Блок, который раньше работал, начинает блокировать свой ** IP-адрес ** после ** сброса настроек, изменения /обновления прошивки, аппаратной замены ** (или перемещения MAC в другой блок): теперь он сообщает ** другой серийный номер (`sn`) или `device_id`** больше, чем сохранено на панели. На `get_profile` сервер блокирует IP-адрес, и портал возвращает 404. + +Это ** не ошибка ** — это защита портала от клонирования, **MAGSCAN**. Для этого требуется ввести серийный номер и сравнить опубликованный `sn` с сохраненным `mag_devices.sn`: + +- **Нет серийного номера** в запросе → запретить (`[MS] No Serial Number`). +- **Опубликовано `sn` ≠ сохранено на устройстве `sn`** → бан (`[MS] Invalid Serial Number`). + +В обоих случаях IP-адрес записывается в таблицу `blocked_ips` (а оттуда в iptables), и устройство получает код 404. Если на устройстве установлен флаг **`lock_device`**, `device_id`, `device_id2` и `hw_version` также отмечены — проверка несоответствия не выполняется, и устройство показывает, что "ваше устройство неактивно" (без запрета IP-адреса). + +**Как исправить (для легитимного ящика, данные которого действительно изменились):** + +1. **Сбросьте привязку на панели:** откройте это устройство MAG в admin и **очистите его сохраненный серийный номер / `device_id`** (или удалите и повторно добавьте устройство). После этого условие "серийный номер уже записан" больше не срабатывает, и при следующем подключении новые значения будут привязаны. +2. **Разблокируйте IP.** Самый простой способ — **через веб-панель**: откройте **Сервис → Управление IP-адресами** (`//ips`), в котором перечислены заблокированные IP—адреса - удалите тот, который вам нужен (или очистите весь список). Настройки CLI / вручную, если вы не можете добраться до панели: + - CLI (очистить все блоки): `sudo /home/xc_vm/console.php tools flush`; + - Вручную, для каждого IP-адреса: `sudo iptables -D INPUT -s -j DROP && sudo rm -f /home/xc_vm/tmp/flood/block_`. + +> ⚠️ Настройка `enable_debug_stalker` обходит проверку `lock_device` / изображений, но НЕ последовательный жесткий запрет MAGSCAN (который выполняется ранее) - вам все равно придется очистить сохраненные `sn` на панели. + +--- + +
+ +
+❌ Забыли пароль администратора / вообще не можете войти в систему + +--- + +Создайте нового пользователя rescue admin с помощью CLI: + +```bash +sudo /home/xc_vm/console.php tools user +``` + +В результате будут введены случайные имя пользователя и пароль с полными правами администратора. Войдите в систему, измените пароль и по завершении удалите пользователя rescue. + +Если сам URL-адрес панели администратора неизвестен, создайте аварийный код доступа: + +```bash +sudo /home/xc_vm/console.php tools rescue +``` + +--- + +
+ +--- + +## База данных и конфигурация + +
+❌ " Не удалось подключиться к базе данных" при запуске + +--- + +Самая распространенная проблема. Причины: + +1. **Неверные учетные данные в `config.ini`** — проверьте `host`, `port`, `db_user`, `db_pass`, `db_name` +2. **MySQL/MariaDB не работает** — `sudo systemctl status mariadb` +3. **Сеть недоступна** — сервер базы данных на другом хосте и порту защищен брандмауэром. +4. **Пользователю не хватает привилегий ** — повторно предоставить их с помощью `console.php tools mysql` + +** Исправлено:** Отредактируйте `/home/xc_vm/config/config.ini`, затем запустите: + +```bash +sudo /home/xc_vm/console.php status +``` + +--- + +
+ +
+❌ Сбой миграции базы данных во время обновления + +--- + +Перенос `.sql` файлов из `migrations/` выполняется автоматически во время обновления. В случае сбоя одного из них: + +- Миграция регистрируется со статусом `[WARN]` — она не будет повторена автоматически. +- Распространенные причины: синтаксическая ошибка, таблица уже существует, конфликт внешних ключей, отсутствует привилегия ALTER. + +**Отладка:** + +1. Проверьте, какая миграция завершилась неудачно, в выходных данных консоли. +2. Откройте файл в `migrations/` и проверьте SQL. +3. Устраните проблему вручную в MySQL, после чего следующее обновление продолжится с того места, где оно было остановлено. + +Дополнительные сведения см. в разделе [Миграция базы данных](../guides/cli-tools.md#database-updates-after-version-upgrade). + +--- + +
+ +--- + +## SSL и Nginx + +
+❌ Ошибка генерации SSL-сертификата + +--- + +`console.php certbot` может завершиться ошибкой с разными кодами ошибок: + +|Ошибка|Причина|Чинить| +|---|---|---| +|Ошибка 3|Домен - это простой IP-адрес|Для Certbot требуется доменное имя, а не IP-адрес| +|Ошибка 4|Не удалось выполнить пробный запуск — используется порт 80/443|Остановить конфликтующую службу: `sudo lsof -i :80`| +|Ошибка 0|Файлы, не найденные после генерации|Проверить `/home/xc_vm/bin/certbot/logs/xc_vm.log`| +|Ошибка 2|Неожиданная ошибка certbot|Проверьте журналы, убедитесь, что DNS разрешает доступ к вашему серверу| + +** Также:** Удалите устаревшие файлы блокировки, если certbot был прерван: + +```bash +sudo rm -f /home/xc_vm/bin/certbot/*/.certbot.lock +``` + +--- + +
+ +
+❌ Nginx не будет перезагружаться — конфликты портов + +--- + +XC_VM запускает **два** nginx экземпляра: + +1. **nginx** (`bin/nginx/`) — HTTP(ы) трафик +2. **nginx_rtmp** (`bin/nginx_rtmp/`) — RTMP потоковая передача + +Каждый из них может выйти из строя, если его порт уже используется. + +**Диагностировать:** + +```bash +sudo netstat -tlnp | grep -E ':80|:443|:1935' +``` + +** Исправлено:** Измените широковещательный порт в настройках панели администратора, затем заново создайте настройки: + +```bash +sudo /home/xc_vm/console.php tools ports +``` + +--- + +
+ +--- + +## Обновления и обслуживание + +
+❌ Ошибка загрузки обновления или несоответствие контрольной суммы + +--- + +Система обновлений загружается с GitHub releases. Если это не удается: + +- **Сеть/брандмауэр ** блокирует доступ к GitHub +- **Частичная загрузка** — соединение прервано на полпути +- **Несоответствие MD5** — поврежденный файл (обновление благополучно прервано) + +Обновления никогда не применяются, если контрольная сумма не совпадает. Повторно запустите обновление после устранения проблем с сетью: + +```bash +sudo -u xc_vm /home/xc_vm/console.php update update +``` + +--- + +
+ +
+❌ Обслуживание неожиданно прекращается или не будет завершено полностью + +--- + +Сервисная команда использует нарастающие сигналы об остановке. Если процессы зависают: + +```bash +# Check for stuck processes +ps -u xc_vm + +# Force kill if necessary +sudo killall -9 -u xc_vm + +# Restart cleanly +sudo /home/xc_vm/console.php service start +``` + +Распространенные причины: PHP взаимоблокировка транзакций, бесконечный цикл потоковой обработки или тайм-аут сетевого сокета в ожидании ответа. + +--- + +
+ +--- + +## Разрешения и система + +
+❌ Ошибки с отказом в разрешении продолжают появляться + +--- + +Запустите команду status — она автоматически устранит все известные проблемы с правами доступа: + +```bash +sudo /home/xc_vm/console.php status +``` + +Что это исправляет: +- PHP-FPM разрешения на использование сокета (`bin/php/sockets/*`) +- Владелец каталога содержимого (`content/streams/`) +- Владение конфигурационным файлом (`config/`) +- Исполняемый бит на `daemons.sh` +- Права доступа к сетевому интерфейсу (`/sys/class/net`) + +Если после каждой перезагрузки разрешения нарушаются, проверьте, существует ли системный пользователь `xc_vm`, которому принадлежит `/home/xc_vm`. + +--- + +
+ +
+❌ Балансировщик нагрузки отображается как отключенный / не удается синхронизировать с основным + +--- + +Серверы LB опрашивают MAIN по протоколу HTTP и обрабатывают сигналы. При сбое синхронизации: + +1. **Сеть:** LB не может подключиться к HTTP—порту MAIN - проверьте правила брандмауэра +2. **База данных:** LB не может подключиться к MAIN'у MySQL — повторно предоставить привилегии: + ```bash + sudo /home/xc_vm/console.php tools mysql + ``` +3. **Тайм-аут:** Если `last_check_ago` превышает 180 секунд, сервер помечается как отключенный + +**Отладка:** Запустите на главном экране, чтобы проверить подключение: + +```bash +sudo -u xc_vm /home/xc_vm/console.php watchdog +``` + +--- + +
+ +--- + +📘 *Эта страница обновляется с течением времени. Если вы обнаружите новую распространенную проблему, пожалуйста, сообщите об этом в разделе [Проблемы](https://github.com/Vateron-Media/XC_VM/issues).* + +--- diff --git a/docs/ru/info/migration_guide.md b/docs/ru/info/migration_guide.md new file mode 100644 index 00000000..1c2ad2a8 --- /dev/null +++ b/docs/ru/info/migration_guide.md @@ -0,0 +1,188 @@ +# XC_VM Руководство по миграции + +Безопасная миграция с совместимых IPTV-систем с помощью встроенных инструментов миграции XC_VM. + +--- + +## Важное уведомление о переносе + +> **Прочтите это перед началом миграции.** + +XC_VM миграция переносит только **данные**. +**Вся конфигурация намеренно исключена из процесса миграции.** + +Это включает в себя (но не ограничивается этим): + +- Ключи API (например, **TMDb**) +- Учетные данные внешней службы +- Настройки, зависящие от конкретной среды +- Конфигурация панели и системы +- Время выполнения и состояние потока + +Эти значения ** должны быть перенастроены вручную после миграции**. + +Это ** дизайнерское решение**, а не ограничение или ошибка. +Пропуск переконфигурации приведет к ** прерыванию выборки метаданных, обновлению заголовков потоков и связанных с ними функций **. + +--- + +## Прежде чем Вы начнете + +> 💡 **Рекомендация:** +> Выполните миграцию на **новой установке XC_VM**. +> +> ⚠️ **Важно:** +> Системные настройки и настройки панели ** НЕ перенесены**. +> Передаются только данные базы данных, поддерживаемые процессом миграции. + +Если вы решите перейти на **существующую установку**, имейте в виду: + +- XC_VM приведет к удалению всех таблиц** в основной базе данных, которые соответствуют данным из базы данных миграции. +- **Резервное копирование является обязательным.** Автоматический откат не предусмотрен. + +--- + +## Этапы миграции + +### 1. Загрузить резервную копию + +Загрузите существующую резервную копию базы данных на сервер XC_VM, используя **SFTP**. + +Примерное местоположение: + +```text +/tmp/backup.sql +``` + +--- + +### 2. Восстановить резервную копию в базе данных миграции + +Очистите базу данных миграции и восстановите резервную копию: + +```bash +sudo /home/xc_vm/console.php tools migration "/tmp/backup.sql" +``` + +Прежде чем продолжить, убедитесь, что восстановление завершилось ** без ошибок**. + +--- + +### 3. Начать миграцию + +Как только резервная копия будет восстановлена, запустите миграцию, используя один из следующих способов. + +#### Вариант 1 — Командная строка (рекомендуется) + +```bash +/home/xc_vm/console.php migrate +``` + +#### Вариант 2 — Веб-установщик + +- Вернитесь к веб-установщику ** (ссылка показана во время настройки панели). +- Выберите **Перенос** +- Следуйте инструкциям на экране + +Вы будете видеть обновления о ходе выполнения в режиме реального времени. +После завершения работы система будет доступна. + +--- + +## Восстановление Доступа После Миграции + +Если войти в систему не удается из-за отсутствия учетных данных или кода доступа, воспользуйтесь средствами восстановления. + +### Создайте аварийный код доступа + +```bash +sudo /home/xc_vm/console.php tools access +``` + +### Создайте учетную запись администратора + +```bash +sudo /home/xc_vm/console.php tools user +``` + +> ❗️ После восстановления доступа ** немедленно измените ** код доступа и учетные данные администратора. + +--- + +## Подготовка балансировщика нагрузки + +Подсистемы балансировки нагрузки **не перенесены**. + +- При необходимости переустановите операционную систему +- Перенастройка сети и маршрутизации +- Повторно подключите их к основному серверу + +--- + +## Постмиграционный период (обязательно) + +После миграции система ** не готова к работе** до тех пор, пока не будут выполнены эти шаги. + +Пропуск их приведет к ** ожидаемому, но нарушенному поведению **. + +--- + +### 1. Повторная инициализация состояния среды выполнения + +- Запускайте все потоки вручную +- Убедитесь, что потоки доступны и стабильны + +> Состояние выполнения потока ** никогда не сохраняется ** во время миграции. + +--- + +### 2. Перенастроить системные параметры + +Просмотрите и восстановите все настройки, зависящие от конкретной среды: + +- Пути к файлам +- Ограничения и квоты +- Настройки сети и обратного прокси-сервера +- Настройка производительности + +> Не предполагайте, что значения по умолчанию соответствуют вашим предыдущим настройкам. +> Значения по умолчанию применяются намеренно. + +--- + +### 3. Восстановление ключей API и поставщиков + +#### Ключи API Никогда Не Переносятся + +Следующие параметры ** необходимо перенастроить вручную**: + +- **Ключ API TMDb** + +Это ** ожидаемое поведение**. + +> Если выборка метаданных не работает после миграции, +> убедитесь, что ключ API был повторно добавлен и поставщик включен. +> Это не указывает на ошибку переноса. + +--- + +## Общие проблемы после миграции + +### Метаданные Не извлекаются (TMDb) + +**Причина:** +Ключ API TMDb и конфигурация поставщика не были восстановлены. + +**Разрешение:** +Повторно добавьте ключ API TMDb и включите провайдера в настройках главного сервера. + +--- + +## Резюме + +- Миграционный перенос ** только основных данных приложения** +- Конфигурация ** исключена по замыслу** +- Ключи API и настройки, зависящие от среды **, должны быть восстановлены вручную** +- Отсутствие функциональности после миграции ** ожидается до завершения реконфигурации** + +--- diff --git a/docs/ru/info/watch_folder.md b/docs/ru/info/watch_folder.md new file mode 100644 index 00000000..cf3f7a6b --- /dev/null +++ b/docs/ru/info/watch_folder.md @@ -0,0 +1,219 @@ +# Папка просмотра — Автоматический импорт мультимедиа + +Watch Folder - это автоматизированная система импорта контента. Он отслеживает локальные каталоги (или пульты дистанционного управления rclone) в поисках новых видеофайлов, анализирует их названия для извлечения метаданных (название, год, сезон, серия), ищет в базе данных TMDB обложки и описания и создает записи о фильмах/сериалах в базе данных — и все это без ручного вмешательства. + +--- + +## как это работает + +``` +┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐ +│ Watch Folder │────▶│ WatchCron │────▶│ WatchItem │ +│ (directory on │ │ scans for new │ │ parses filename │ +│ disk / rclone) │ │ files, filters │ │ queries TMDB │ +│ │ │ already imported │ │ creates DB row │ +└──────────────────┘ └──────────────────┘ └──────────────────┘ + │ + ▼ + ┌──────────────────┐ + │ Bouquet update │ + │ (auto-assign │ + │ to bouquets) │ + └──────────────────┘ +``` + +### Шаг за шагом + +1. **Администратор создает папку просмотра** в панели администратора (Папка просмотра → Добавить) или через API (`create_watch_folder`). Конфигурация включает в себя: путь к каталогу, тип контента (фильм/сериал), целевую категорию, букеты, настройки парсера и назначенный сервер. +2. **Задание Cron `cron:watch`** выполняется периодически (контролируется `scan_offset` — секундами между сканированиями). Оно запрашивает таблицу `watch_folders` для активных папок, в которых `last_run` превышено смещение. +3. **Обнаружение файлов** — cron использует `find` для локальных каталогов или `rclone lsjson` для облачных/удаленных подключений. Файлы фильтруются по разрешенным расширениям (по умолчанию: `mp4, mkv, avi, mpg, flv, 3gp, m4v, wmv, mov, ts`). Файлы, уже присутствующие в `streams.stream_source`, пропускаются. +4. **Проверка стабильности ** — файлы, измененные менее 30 секунд назад, пропускаются (чтобы избежать импорта частично загруженных файлов). +5. **Параллельная обработка ** — каждый новый файл отправляется команде `watch_item` (через `shell_exec`), параллельно обрабатывая до `thread_count` элементов с помощью `Multithread`. +6. **WatchItem** анализирует имя файла с помощью PTN или guessit (смотрите документацию по синтаксическому анализу ниже), преобразует метаданные с помощью TMDB API и вставляет запись в `streams` (для фильмов) или `streams_series` + `streams_episodes` ( для сериалов). +7. **Назначение букета** — импортированные элементы автоматически добавляются в настроенные букеты. + +--- + +## Конфигурация + +### Просмотр настроек папки (для каждой папки) + +|Установка|Описание| +|---------|-------------| +| `directory` |Локальный путь для сканирования (например, `/mnt/media/movies/`)| +| `rclone_dir` |удаленный путь к rclone (альтернатива локальному каталогу)| +| `type` |Тип содержимого: `movie` или `series`| +| `server_id` |Сервер, на котором выполняется сканирование| +| `category_id` |Целевая категория для импортируемого контента| +| `bouquets` |Автоматическое назначение этим букетам| +| `fb_category_id` |Резервная категория (в случае сбоя в сопоставлении жанров TMDB)| +| `fb_bouquets` |Резервные букеты| +| `allowed_extensions` |Расширения файлов для сканирования (пустые = список по умолчанию)| +| `language` |Предпочтительный язык TMDB для метаданных| +| `active` |Включить/отключить эту папку| + +### Логические параметры + +|Вариант|Описание| +|--------|-------------| +| `disable_tmdb` |Пропустить поиск в TMDB — импортировать файл только с разобранным заголовком| +| `ignore_no_match` |Импорт, даже если TMDB не возвращает результата| +| `auto_subtitles` |Автоматическое определение `.srt`, `.sub`, `.sbv` файлов рядом с видео| +| `fallback_title` |Используйте имя папки в качестве заголовка, если синтаксический анализатор не может ее извлечь| +| `read_native` |Чтение собственного заголовка из TMDB| +| `movie_symlink` |Создавайте символические ссылки вместо ссылок на исходный путь| +| `auto_encode` |Автоматическое кодирование импортированного содержимого| +| `auto_upgrade` |Замените существующую версию более низкого качества, если идентификатор TMDB совпадает| +| `duplicate_tmdb` |Разрешить многократный импорт с одним и тем же идентификатором TMDB| +| `ffprobe_input` |Запустите ffprobe в исходном файле, чтобы извлечь метаданные кодека| +| `extract_metadata` |Извлечение дополнительных метаданных из файла| + +### Глобальные настройки + +|Установка|Где|Описание| +|---------|-------|-------------| +| `tmdb_api_key` |Администратор → Настройки|**Требуется** — Ключ API TMDB. Без него Watch не будет работать| +| `fallback_parser` |Администратор → Настройки|Синтаксический анализатор, используемый при сбое основного синтаксического анализатора| +| `alternative_titles` |Администратор → Настройки|Поиск альтернативных названий в базе данных TMDB| +| `max_genres` |Администратор → Настройки|Максимальное количество жанров, назначаемых для каждого элемента| + +--- + +## Панель администратора и API + +### Страницы панели администратора + +|Страница|Описание| +|------|-------------| +|Смотреть папку → Список|Просмотр всех настроенных папок наблюдения со статусом| +|Посмотреть папку → Добавить|Создание/редактирование папки наблюдения| +|Папка просмотра → Настройки|Глобальные настройки просмотра (синтаксический анализатор, конфигурация TMDB)| +|Просмотр папки → Журналы|Просмотр результатов сканирования и ошибок| + +### Действия администратора API + +|Действие|Описание| +|--------|-------------| +| `get_watch_folders` |Перечислите все папки для просмотра| +| `get_watch_folder` |Получить единую папку по идентификатору| +| `create_watch_folder` |Создайте новую папку наблюдения| +| `edit_watch_folder` |Обновите существующую папку наблюдения| +| `delete_watch_folder` |Удалить папку наблюдения| +| `reload_watch_folder` |Принудительное немедленное повторное сканирование| +| `enable_watch` |Включить все папки наблюдения| +| `disable_watch` |Отключите все папки наблюдения| +| `kill_watch` |Уничтожьте все запущенные процессы наблюдения| + +### КЛИ + +```bash +# Normal cron execution (usually triggered automatically) +sudo -u xc_vm /home/xc_vm/console.php cron:watch + +# Force scan a specific folder (by ID) +sudo -u xc_vm /home/xc_vm/console.php cron:watch 5 +``` + +--- + +## Анализаторы + +Доступны два средства анализа имен файлов. Средство анализа извлекает структурированные метаданные (название, год, сезон, эпизод, разрешение, кодек) из имени видеофайла. + +### Выбор синтаксического анализатора + +|Синтаксический анализатор|Лучше всего для| +|--------|----------| +|**PTN (Почтовый индекс)**|Простые имена файлов с пробелами: `San Andreas 2015 720p.mkv`| +|** угадать**|Имена файлов, разделенные точками: `The.Matrix.1999.1080p.BluRay.mkv`| + +Установите основной синтаксический анализатор для каждой папки просмотра. Параметр `fallback_parser` global используется, когда основной синтаксический анализатор не возвращает совпадений. + +--- + +## 1️⃣ Синтаксический анализатор PTN + +PTN parser поддерживает синтаксический анализ файлов фильмов и телешоу с типичными шаблонами именования. + +### Фильмы + +|Пример файла|Проанализированные данные| +|-------------|-------------| +| `San Andreas 2015 720p WEB-DL x264 AAC-JYK.mkv` |Название: *San Andreas*, Год выпуска: 2015, Разрешение: 720p, Видео: x264, Аудио: AAC, Группа: JYK| +| `The Martian 2015 540p HDRip KORSUB x264 AAC2 0-FGT.mp4` |Название: "Марсианин", Год выпуска: 2015, Разрешение: 540p, Видео: x264, Аудио: AAC2.0, Группа: FGT| + +### Телевизионные шоу + +|Пример файла|Проанализированные данные| +|-------------|-------------| +| `friends.s02e01.720p.bluray-sujaidr.mkv` |Название: *Друзья*, Сезон: 2, Серия: 1, Разрешение: 720p, Формат: bluray, Группа: sujaidr| +| `Mr Robot S01E05 HDTV x264-KILLERS[ettv].mp4` |Название: *Мистер Робот*, Сезон: 1, Серия: 5, Формат: HDTV, Видео: x264, Группа: KILLERS| + +--- + +## 2️⃣ синтаксический анализатор догадок + +Guessit поддерживает более сложные имена файлов, включая разделители точек и многоязычные заголовки. + +### Фильмы + +|Пример файла|Проанализированные данные| +|-------------|-------------| +| `The.Matrix.1999.1080p.BluRay.x264.DTS-FGT.mkv` |Название: "Матрица", Год выпуска: 1999, Разрешение: 1080p, Видео: x264, Аудио: DTS, Группа: FGT| +| `Inception.2010.720p.BRRip.x264.AAC-ETRG.mkv` |Название: *Начало*, Год выпуска: 2010, Разрешение: 720p, Видео: x264, Аудио: AAC, Группа: ETRG| + +### Телевизионные шоу + +|Пример файла|Проанализированные данные| +|-------------|-------------| +| `Breaking.Bad.S03E07.720p.BluRay.x264-REWARD.mkv` |Название: "Во все тяжкие", Сезон: 3, Серия: 7, Разрешение: 720p, Видео: x264, Группа: REWARD| +| `Game.of.Thrones.S05E09.1080p.WEB-DL.DD5.1.H.264-NTb.mkv` |Название: *Игра престолов*, Сезон: 5, Серия: 9, Разрешение: 1080p, Видео: H.264, Аудио: DD5.1, Группа: NTb| + +--- + +### Возврат к имени резервной папки + +Если имя файла не содержит отображаемого заголовка, включите ** Резервный вариант для имени папки**: + +|Пример пути к файлу|Проанализированные данные| +|-----------------|-------------| +| `/path/to/Show Name/S01E01 720p WEB-DL.mkv` |Название: *Название сериала*, Сезон: 1, Серия: 1| +| `/path/to/Show.Name/S01E01.720p.WEB-DL.mkv` |Название: *Название сериала*, Сезон: 1, Серия: 1| + +#### Структура сезонных папок + +Если вы хотите, чтобы эпизоды были отсортированы по папкам сезонов, имя файла должно содержать название шоу: + +|Пример пути к файлу|Проанализированные данные| +|-----------------|-------------| +| `/path/to/Show Name/Season 01/Show Name S01E01 720p WEB-DL.mkv` |Название: *Название сериала*, Сезон: 1, Серия: 1| +| `/path/to/Show.Name/Season.01/Show.Name.S01E01.720p.WEB-DL.mkv` |Название: *Название сериала*, Сезон: 1, Серия: 1| + +--- + +### Языки RTL + +Для показов на языках RTL (арабский, иврит и т.д.): + +- Имя файла **НЕ должно содержать заголовка show** +- Включить ** Резервное копирование к имени папки** + +|Пример пути к файлу|Проанализированные данные| +|-----------------|-------------| +| `/path/to/Show Name/S01E01 (year).mp4` |Название: *Название сериала*, Сезон: 1, Серия: 1, Год выпуска: `year`| +| `/path/to/Show Name/Season 01/S01E01 (year).mp4` |Название: *Сезон 01*, Сезон: 1, Серия: 1, Год выпуска: `year`| + +> ❗️ Примечание: Для языков RTL название показа берется только из названия папки. + +--- + +### Резюме + +- **Синтаксический анализатор PTN** — простые имена файлов, локальные форматы +- ** синтаксический анализатор guessit ** — поддерживает имена, разделенные точками, многоязычные заголовки, возврат к имени папки +- **Языки RTL** — необходимо использовать резервную копию имени папки +- **Структура папок сезона ** — для правильной сортировки в имени файла должно быть указано название показа + +--- + +💡 ** Совет:** Используйте согласованные имена файлов и папок для точного анализа и автоматической сортировки по сезонам. diff --git a/mkdocs.yml b/mkdocs.yml index 712eaaf0..2cf2fd22 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -7,9 +7,9 @@ docs_dir: docs site_dir: site edit_uri: edit/main/docs/ -# ru/ is generated at build time (translated from en/) and consumed by the i18n -# plugin. adr/agents/xc_fanout.md are repo-internal English notes, not part of -# the published site; requirements.txt is toolchain, not content. +# ru/ is a committed generated tree (translated from en/ locally before release) +# consumed by the i18n plugin. adr/agents/xc_fanout.md are repo-internal English +# notes, not part of the published site; requirements.txt is toolchain. exclude_docs: | adr/ agents/ diff --git a/tools/docs/requirements.txt b/tools/docs/requirements.txt new file mode 100644 index 00000000..a85e009a --- /dev/null +++ b/tools/docs/requirements.txt @@ -0,0 +1,11 @@ +# Translation dependencies for tools/docs/translate.py — LOCAL only (run before a +# release via `make docs-translate`; NOT installed or run in CI). Pick per +# DOCS_TRANSLATE_PROVIDER: +# translators = free, NO API KEY (web engines: yandex/google/bing/...) — default +# anthropic = highest quality for structure-heavy Markdown (ANTHROPIC_API_KEY) +# deepl = DeepL API (DEEPL_AUTH_KEY) +# noop = copy English verbatim (no dependency) + +translators>=5.9 +# anthropic>=0.40 # uncomment for DOCS_TRANSLATE_PROVIDER=anthropic +# deepl>=1.18 # uncomment for DOCS_TRANSLATE_PROVIDER=deepl diff --git a/tools/docs/translate.py b/tools/docs/translate.py index 5e04cfa6..924257b1 100644 --- a/tools/docs/translate.py +++ b/tools/docs/translate.py @@ -2,8 +2,9 @@ """Generate translated docs from the canonical English source. English (``docs/en``) is the single source of truth; every other language tree -(e.g. ``docs/ru``) is produced from it by this script and is NOT committed — -``mkdocs build`` consumes the generated tree via the mkdocs-static-i18n plugin. +(e.g. ``docs/ru``) is produced from it by this script. The generated tree IS +committed and refreshed by running this script LOCALLY before a release (not in +CI); ``mkdocs build`` then consumes it via the mkdocs-static-i18n plugin. Design goals ------------