From e4e7d9029f777111df90cf6e0474691b7ac76af0 Mon Sep 17 00:00:00 2001 From: Artem Kokos Date: Thu, 21 May 2026 22:19:29 +0700 Subject: [PATCH] Refresh project documentation --- README.md | 238 ++++++++++++++++++---------------- deploy/README.md | 66 +++++++--- deploy/ignis-core.env.example | 13 +- 3 files changed, 185 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index 20bdaac..86fee40 100644 --- a/README.md +++ b/README.md @@ -1,112 +1,95 @@ # Ignis Core -`ignis-core` — локальный FastAPI-сервер для WiZ-ламп и домашней автоматики вокруг них. +Локальный FastAPI-сервер для WiZ-ламп: discovery, управление, группы, расписания, API-ключи, аудит и встроенная веб-морда. -## Что есть сейчас +## Что умеет -- discovery ламп по локальной сети с `startup`, `manual` и background refresh; -- управление отдельной лампой и группой; -- реальные `status`-опросы и `blink` для идентификации; -- группы в SQLite; -- one-shot и cron-расписания; -- persisted metadata расписаний поверх APScheduler; -- роли `master`, `admin`, `guest`; -- гостевые API-ключи с revoke/activate; -- event log и простая stats summary; -- встроенный локальный UI из `static/`; -- OpenAPI-экспорт в `openapi.json`. +- искать лампы в локальной сети при старте, вручную и в фоне; +- управлять лампой или группой; +- мигать лампой (`blink`) и читать живой статус; +- хранить группы, ключи, события и расписания в SQLite; +- выполнять `one-shot` и `cron`-задачи через APScheduler; +- разделять доступ на `master`, `admin`, `guest`; +- отдавать встроенный web UI из `static/`; +- публиковать OpenAPI. -## Архитектура +## Структура -- `main.py` — инициализация FastAPI, security headers, router wiring, startup lifecycle. -- `app/api/routes/*` — HTTP-маршруты. -- `app/core/discovery.py` — выбор подсетей и UDP discovery WiZ. -- `app/core/state.py` — in-memory runtime-state устройств и групп. -- `app/core/scheduler.py` — APScheduler, reconciliation и cleanup old events. -- `app/models/*` — SQLAlchemy-модели и Pydantic-схемы. -- `static/` — встроенный web UI без внешних CDN. +- `main.py` — FastAPI, middleware, lifecycle, статика. +- `app/api/routes/` — HTTP API. +- `app/core/discovery.py` — discovery и выбор подсетей. +- `app/core/scheduler.py` — расписания и reconciliation. +- `app/core/state.py` — runtime-state устройств и групп. +- `app/models/` — SQLAlchemy и схемы данных. +- `static/` — встроенный UI. +- `deploy/` — `systemd`-деплой и пример env. -## Запуск локально +## Быстрый старт ```bash python3 -m venv .venv source .venv/bin/activate pip install -r requirements.txt cp deploy/ignis-core.env.example .env -uvicorn main:app --host 0.0.0.0 --port 8000 +.venv/bin/python -m uvicorn main:app --host 0.0.0.0 --port 8000 ``` -UI: `http://:8000/` +- UI: `http://127.0.0.1:8000/` +- OpenAPI runtime: `http://127.0.0.1:8000/openapi.json` +- Коммитнутый экспорт: [openapi.json](openapi.json) -Готовые файлы для `systemd`: `deploy/README.md` +`python-dotenv` подхватывает `.env` автоматически. -## Конфигурация +## Конфиг -Минимальный набор: +Минимум: ```env IGNIS_API_KEY=change-me IGNIS_INSTANCE_NAME=Home APP_TIMEZONE=Asia/Novosibirsk -LOG_LEVEL=INFO +SCAN_NETWORK=192.168.0.0/24 IGNIS_DATABASE_URL=sqlite+aiosqlite:///./ignis.db IGNIS_SYNC_DATABASE_URL=sqlite:///./ignis.db ``` -Параметры server metadata / versioning: +Основные переменные: -```env -IGNIS_PUBLIC_BASE_URL=https://ignis.example.local -IGNIS_BUILD_VERSION=1.0.0 -IGNIS_BUILD_DATE=2026-05-21T12:00:00Z -IGNIS_GIT_SHA=abc1234def56 -``` +- `IGNIS_API_KEY` — мастер-ключ. Без него сервер работает в `fail-closed` и защищённые маршруты отвечают `503`. +- `IGNIS_INSTANCE_NAME` — имя инстанса в UI и `GET /system/info`. +- `APP_TIMEZONE` — таймзона расписаний. +- `SCAN_NETWORK` — подсеть или список подсетей для discovery. На хостах с VPN или несколькими NIC лучше задавать явно. +- `DISCOVERY_INTERVAL_SECONDS` — период фонового refresh. +- `DISCOVERY_BACKGROUND_MISSING_THRESHOLD` — сколько циклов подряд лампа может не отвечать до удаления. +- `DISCOVERY_ENV_MIN_PREFIX_LEN` — минимально допустимая маска для `SCAN_NETWORK`. +- `DISCOVERY_AUTO_MIN_PREFIX_LEN` — минимально допустимая маска для auto-discovery. +- `EVENT_LOG_RETENTION_DAYS` — срок хранения event log. +- `IGNIS_PUBLIC_BASE_URL` — внешний URL, если сервер стоит за reverse proxy. +- `IGNIS_BUILD_VERSION`, `IGNIS_BUILD_DATE`, `IGNIS_GIT_SHA` — build metadata для диагностики. -- `IGNIS_INSTANCE_NAME` — человекочитаемое имя инстанса, которое видно в UI и `GET /system/info`. -- `IGNIS_PUBLIC_BASE_URL` — внешний URL сервера, если он стоит за reverse proxy или доступен по доменному имени. -- `IGNIS_BUILD_VERSION`, `IGNIS_BUILD_DATE`, `IGNIS_GIT_SHA` — build metadata установленного сервера для диагностики и сверки версий. +Полный пример: [deploy/ignis-core.env.example](deploy/ignis-core.env.example) -Параметры discovery: +## Discovery -```env -SCAN_NETWORK=192.168.0.0/24 -DISCOVERY_INTERVAL_SECONDS=600 -DISCOVERY_BACKGROUND_MISSING_THRESHOLD=2 -DISCOVERY_ENV_MIN_PREFIX_LEN=16 -DISCOVERY_AUTO_MIN_PREFIX_LEN=24 -``` +- при старте выполняется `startup_refresh()`; +- `POST /devices/rescan` делает ручной refresh и сразу убирает оффлайн-устройства; +- фоновый refresh удаляет устройство только после `DISCOVERY_BACKGROUND_MISSING_THRESHOLD` подряд промахов; +- если `SCAN_NETWORK` пуст, сервер сам выбирает private IPv4 подсети и старается не лезть в `docker`, `tun`, `wg`, `tailscale` и похожие интерфейсы. -Параметры retention: +## Роли -```env -EVENT_LOG_RETENTION_DAYS=30 -``` - -## Как работает discovery - -- Если `SCAN_NETWORK` задан, сервер сканирует только указанные подсети. -- Если `SCAN_NETWORK` пуст, `DiscoveryService` пытается выбрать private IPv4-сегменты обычных интерфейсов и избегает `docker`, `tun`, `wg`, `tailscale` и похожих интерфейсов. -- При старте выполняется `startup_refresh()`. -- `POST /devices/rescan` делает ручной refresh и сразу удаляет оффлайн-устройства. -- Background refresh работает циклически и удаляет устройство только после `DISCOVERY_BACKGROUND_MISSING_THRESHOLD` подряд пропусков. - -Для хостов с VPN, несколькими NIC или нетипичной маршрутизацией `SCAN_NETWORK` лучше задавать явно. - -## Авторизация и роли - -Заголовок: `X-API-Key` +Заголовок авторизации: `X-API-Key` - `master` — значение `IGNIS_API_KEY`, полный доступ. -- `admin` — ключ из БД, доступ к группам, расписаниям, stats и `rescan`. -- `guest` — чтение и обычное управление светом. +- `admin` — ключ из БД с `is_admin=true`, доступ к группам, рескану, расписаниям, stats и расширенному `system/info`. +- `guest` — управление светом и чтение безопасной части API. -Сервер работает в `fail-closed`: если `IGNIS_API_KEY` не задан, защищённые маршруты отвечают `503`. +`GET /auth/me` возвращает текущую роль и имя ключа. ## HTTP API -Основные маршруты: +Основные группы маршрутов: -- `GET /auth/me` - `GET /devices` - `GET /devices/groups` - `GET /devices/scenes` @@ -130,42 +113,83 @@ EVENT_LOG_RETENTION_DAYS=30 - `GET /stats/log` - `GET /system/info` -`control/*` и `schedules/*` принимают JSON body. +Ролевые ограничения: -Поддерживаемые параметры команды: +- `guest`: чтение устройств/групп/сцен, управление светом, `status`, `blink`, безопасный `system/info`. +- `admin`: всё выше плюс создание/удаление групп, `rescan`, расписания, stats/log, расширенный `system/info`. +- `master`: всё выше плюс управление API-ключами. -- `state` -- `brightness` -- `scene` -- `temp` -- `r`, `g`, `b` +Формат основных тел: + +```json +{"state": true} +``` + +```json +{"brightness": 60} +``` + +```json +{"temp": 3200} +``` + +```json +{"r": 255, "g": 180, "b": 120} +``` + +```json +{"scene": "Cozy"} +``` Валидация: - `brightness`: `10..100` - `temp`: `2200..6500` - `r/g/b`: `0..255` -- `scene`, `temp` и `rgb` взаимоисключаемы -- `r`, `g`, `b` нужно передавать полной тройкой -- для `schedules/once` нужно передать ровно одно из `run_at` или `hours_from_now` +- можно передать только один режим из `scene`, `temp` или `rgb` +- `r/g/b` нужно передавать полной тройкой +- для `POST /schedules/once` нужно передать ровно одно из `run_at` или `hours_from_now` -Пример: +Примеры: ```bash -curl -X POST http://127.0.0.1:8000/control/group/bedroom \ +curl -sS http://127.0.0.1:8000/control/group/bedroom \ -H 'X-API-Key: change-me' \ -H 'Content-Type: application/json' \ -d '{"state":true,"brightness":60}' ``` +```bash +curl -sS http://127.0.0.1:8000/api-keys \ + -H 'X-API-Key: change-me' +``` + +Замечания по ключам: + +- `GET /api-keys` возвращает публичные `key_id`, а не полный секрет; +- `POST /api-keys` показывает полный секрет только один раз; +- `POST /api-keys/revoke` и `POST /api-keys/activate` принимают JSON вида `{"key":""}`. + ## Встроенный UI -- лежит в `static/index.html`, `static/app.js`, `static/ui.css`; +UI лежит в `static/` и не требует отдельной сборки. + +Что есть сейчас: + +- вход по API-ключу; +- роль-зависимые вкладки; +- пульт групп со сценами, яркостью, температурой, цветом и таймером на 4 часа; +- список устройств и сборка групп; +- one-shot и cron; +- серверная вкладка с метаданными инстанса; +- аудит, stats и API-ключи для нужных ролей. + +Свойства UI: + - использует только локальные ассеты; - не использует `localStorage`; -- может хранить API-ключ только в `sessionStorage` текущей вкладки; -- показывает build/server metadata текущего инстанса; -- умеет базовое управление группами, расписания, API-ключи, stats/log и быстрый таймер на 4 часа. +- может хранить ключ только в `sessionStorage` текущей вкладки; +- гость не видит чувствительные поля `system/info`. ## Хранилище @@ -178,11 +202,15 @@ SQLite-таблицы: - `apscheduler_jobs` - `devices` -Важно: реальным runtime-источником истины для онлайн-устройств остаётся in-memory `state_manager.devices`. Таблица `devices` пока не используется как полноценный persistent source of truth. +Важно: + +- онлайн-устройства и статусы живут в runtime-памяти процесса; +- таблица `devices` пока не является полноценным source of truth; +- миграций схемы нет, используется `Base.metadata.create_all()`. ## OpenAPI -Актуальная схема хранится в `openapi.json`. +Актуальная схема закоммичена в `openapi.json`. Перегенерация: @@ -192,34 +220,22 @@ SQLite-таблицы: ## Тесты -На 2026-05-21 в `tests/` лежит 29 `unittest`-сценариев. - -Покрыто: - -- fail-closed auth и role checks; -- lifecycle API-ключей; -- control/status error handling и partial success; -- validation для scene, control body и schedules body; -- one-shot и cron-расписания; -- reconciliation и миграция legacy jobs; -- auto subnet selection для discovery; -- background offline cleanup threshold; -- manual rescan summary; -- server metadata endpoint и отсутствие утечки секретов в нём; -- security headers и локальные UI-ассеты; -- stats summary без двойного счёта `*_requested`. - -Команды: +Основная команда: ```bash -.venv/bin/python -m compileall app tests main.py timeout 120s .venv/bin/python -m unittest discover -s tests -v ``` -## Известные ограничения +Дополнительно: -- discovery по-прежнему основан на переборе IP в подсетях; -- миграций схемы БД нет, используется `Base.metadata.create_all()`; +```bash +.venv/bin/python -m compileall app tests main.py +node --check static/app.js +``` + +## Ограничения + +- discovery всё ещё сетевой перебор внутри выбранных подсетей; - runtime-state устройств живёт в памяти процесса; -- встроенный UI остаётся монолитным файлом без отдельной frontend-сборки; -- stats — это audit/summary, а не полноценная аналитика. +- UI монолитный, без отдельного frontend-build step; +- stats — это аудит и summary, а не полноценная аналитика. diff --git a/deploy/README.md b/deploy/README.md index 90065ac..ede456a 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -2,27 +2,29 @@ Минимальный `systemd`-деплой для `ignis-core`. -В каталоге: +Файлы: -- `ignis-core.service` — unit-файл; -- `ignis-core.env.example` — пример env-конфига. +- `ignis-core.service` — unit-файл +- `ignis-core.env.example` — пример env-конфига -## Предполагаемая раскладка +## Ожидаемая раскладка - код: `/opt/ignis/ignis-core` - env: `/etc/ignis-core/ignis-core.env` - пользователь: `ignis` -- SQLite: `/var/lib/ignis-core/ignis.db` +- база: `/var/lib/ignis-core/ignis.db` -Если у вас другие пути, поправьте unit и env-файл. +Если пути другие, поправьте unit и env. -## 1. Создать системного пользователя +## Установка + +### 1. Пользователь ```bash sudo useradd --system --home /opt/ignis --shell /usr/sbin/nologin ignis ``` -## 2. Разложить проект и зависимости +### 2. Код и зависимости ```bash sudo mkdir -p /opt/ignis @@ -34,7 +36,7 @@ pip install -r requirements.txt sudo chown -R ignis:ignis /opt/ignis ``` -## 3. Подготовить env-файл +### 3. Env-файл ```bash sudo mkdir -p /etc/ignis-core @@ -43,14 +45,13 @@ sudo chmod 640 /etc/ignis-core/ignis-core.env sudo chown root:ignis /etc/ignis-core/ignis-core.env ``` -Минимум, который надо заполнить руками: +Минимум, который надо заполнить: - `IGNIS_API_KEY` +- `IGNIS_INSTANCE_NAME` - `SCAN_NETWORK` -Для машин с VPN или несколькими интерфейсами `SCAN_NETWORK` лучше задавать явно. - -## 4. Установить unit +### 4. Unit ```bash sudo cp deploy/ignis-core.service /etc/systemd/system/ignis-core.service @@ -58,14 +59,17 @@ sudo systemctl daemon-reload sudo systemctl enable --now ignis-core.service ``` -## 5. Проверить запуск +## Проверка ```bash sudo systemctl status ignis-core.service sudo journalctl -u ignis-core.service -n 100 --no-pager -curl -H 'X-API-Key: ' http://127.0.0.1:8000/auth/me +curl -sS http://127.0.0.1:8000/auth/me -H 'X-API-Key: ' +curl -sS http://127.0.0.1:8000/system/info -H 'X-API-Key: ' ``` +Если сервис стоит за reverse proxy, имеет смысл задать `IGNIS_PUBLIC_BASE_URL`. + ## Обновление ```bash @@ -75,9 +79,31 @@ pip install -r requirements.txt sudo systemctl restart ignis-core.service ``` -## Замечания +Если менялся env: -- `StateDirectory=ignis-core` в unit создаёт `/var/lib/ignis-core`. -- По умолчанию сервис слушает `0.0.0.0:8000`. -- Reverse proxy проще ставить перед сервисом, а не внутрь него. -- Перед обновлением backend-контракта полезно перегенерировать `openapi.json` и прогнать `unittest`. +```bash +sudo systemctl restart ignis-core.service +``` + +Если менялся unit: + +```bash +sudo systemctl daemon-reload +sudo systemctl restart ignis-core.service +``` + +## Что делает unit + +Текущий [ignis-core.service](ignis-core.service): + +- запускает `python -m uvicorn main:app --host 0.0.0.0 --port 8000` +- читает `/etc/ignis-core/ignis-core.env` +- рестартует сервис при падении +- пишет состояние в `/var/lib/ignis-core` +- даёт записи в `/opt/ignis/ignis-core` и `/var/lib/ignis-core` + +## Практические замечания + +- для хостов с VPN, Docker или несколькими интерфейсами лучше всегда задавать `SCAN_NETWORK` явно; +- перед релизом полезно перегенерировать `openapi.json` и прогнать `unittest`; +- reverse proxy лучше ставить перед сервисом, а не встраивать в него. diff --git a/deploy/ignis-core.env.example b/deploy/ignis-core.env.example index df0d51e..0bd25c9 100644 --- a/deploy/ignis-core.env.example +++ b/deploy/ignis-core.env.example @@ -1,14 +1,25 @@ +# Auth and instance IGNIS_API_KEY=change-me IGNIS_INSTANCE_NAME=Home +APP_TIMEZONE=Asia/Novosibirsk + +# Optional external URL and build metadata IGNIS_PUBLIC_BASE_URL= IGNIS_BUILD_VERSION=1.0.0 IGNIS_BUILD_DATE= IGNIS_GIT_SHA= -APP_TIMEZONE=Asia/Novosibirsk + +# Discovery SCAN_NETWORK=192.168.0.0/24 DISCOVERY_INTERVAL_SECONDS=600 DISCOVERY_BACKGROUND_MISSING_THRESHOLD=2 +DISCOVERY_ENV_MIN_PREFIX_LEN=16 +DISCOVERY_AUTO_MIN_PREFIX_LEN=24 + +# Logging and retention LOG_LEVEL=INFO EVENT_LOG_RETENTION_DAYS=30 + +# SQLite IGNIS_DATABASE_URL=sqlite+aiosqlite:////var/lib/ignis-core/ignis.db IGNIS_SYNC_DATABASE_URL=sqlite:////var/lib/ignis-core/ignis.db