# Конфигурация Конвенция: *как* устроена и грузится конфигурация jellybit (TOML). Правила оформления кода (How), не спецификация поведения. Краткая выжимка и инварианты — в [CLAUDE.md](../../CLAUDE.md), раздел «Конвенции кода». > Каркас. Загрузчик `internal/config/config.go` уже грузит TOML; валидация > на старте — в работе (`TODO`), обкатывается на следующем шаге. ## Принципы - **Конфигурация — только TOML.** Env-переменные для конфига **не используем**: окружение наследуется дочерними процессами и видно через `/proc//environ` — для секретов это слабее файла под `0600`. Запрет `os.Getenv` механизирован (`forbidigo`). - Грузим **один раз при старте** в одну типизированную структуру `Config` (под-структуры по секциям). Дальше по коду читаем только её — чтения файла в бизнес-коде нет, только загрузчик `internal/config`. - Конфиг **неизменяем** после старта; смена параметров — рестарт процесса. ## Файл и поиск - Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочей директории** процесса. - Путь переопределяется опцией **`--config=path`**. - Образец в репозитории — **`config.example.toml`** (см. ниже); реальный `config.toml` не коммитится. ## config.example.toml — самодокументируемый образец `config.example.toml` коммитим как единый справочник по конфигу: все секции и все поля. **Каждое поле снабжаем комментарием**, из которого ясно: - **зачем** поле — что оно меняет в поведении; - **диапазон/допустимые значения** — перечисление или границы; - **единицы измерения**, если применимо — секунды/миллисекунды, байты/КБ, доля `0–1` и т.п. ```toml [worker] poll_interval = "5s" # как часто опрашивать qBittorrent; Go-duration (s/m/h) magnet_timeout = "30m" # ждать метаданные magnet не дольше; Go-duration source_missing_threshold = 3 # тиков сверки без раздачи, чтобы счесть источник удалённым [recognition] auto_confidence_threshold = 0.85 # порог авто-раскладки без ревью; доля 0.0–1.0 [llm] max_retries = 3 # попыток получить валидный ответ LLM; целое ≥ 0 ``` Секретные поля оставляем пустыми — значение приходит из деплоя (см. «Секреты»). ## Поля по дискриминатору `type` Когда набор полей секции зависит от поля-дискриминатора `type` (выбор одного из бекендов/внешних сервисов — напр. `[llm].type`), обязательность и опциональность полей определяются значением `type`, а не фиксированы для секции. - **Валидация — по `type`.** Для каждого поддерживаемого `type` свой набор обязательных полей; поля, относящиеся к другим `type`, не требуются. Неизвестный `type` → ошибка на старте с перечислением поддерживаемых. - **Образец — по `type`.** В `config.example.toml`: - основной (дефолтный) `type` **предзаполнен** рабочими значениями; - альтернативные `type` — **блоками-комментариями ниже**, каждый со своим описанием полей (зачем/диапазон/единицы — как у обычных полей); - так из примера видны все варианты и поля каждого, не открывая код. ```toml [llm] type = "openai-compat" # бекенд LLM; варианты ниже base_url = "http://host.docker.internal:1234/v1" # эндпоинт OpenAI-совместимого API api_key = "" # ключ; пусто для keyless-local (LM Studio) model = "qwen2.5-32b-instruct" # имя модели у провайдера # --- альтернативный бекенд: type = "" --- # [llm] # type = "" # описание варианта # ... # его обязательные/опциональные поля ``` ## Секреты Секреты доставляет **деплой**, рендеря их прямо в `config.toml` (jellybit: Ansible + Vault). Приложение просто читает TOML — отдельного слоя секретов в коде нет. Источник истины секрета — внешнее хранилище деплоя (Vault), не репозиторий и не env. - Секретные поля jellybit: `qbittorrent.password`, `llm.api_key`, `metadata.*.api_key`, `jellyfin.api_key`, `telegram.token`. - Рендеренный `config.toml` (с секретами) **не коммитится**; права `0600`, владелец — runtime-пользователь (`1000:1000`). - В `config.example.toml` секретные поля — пустые строки. - Загрузчик на старте проверяет, что обязательные секреты не пусты (ловит криво отрендеренный файл) — см. «Валидация и fail-fast». - В логи секреты не попадают — см. [logging.md](logging.md), «Безопасность». ## Валидация и fail-fast Конфиг валидируем **на старте, до приёма трафика**. Невалидный конфиг — лог `ERROR` и выход с ненулевым кодом (не стартуем «наполовину»). Что проверяем (jellybit): - обязательные поля заданы (напр. `qbittorrent.url`, `paths.*`, `storage.db_path`); - пути `paths.movies`/`series`/`downloads` существуют и доступны; целевые — под единой песочницей (см. инварианты в [CLAUDE.md](../../CLAUDE.md)); - диапазоны: `recognition.auto_confidence_threshold` ∈ [0, 1], `llm.max_retries` ≥ 0; - длительности парсятся (`llm.timeout`, `worker.poll_interval`, …); - `general.timezone` — распознаваемая IANA-зона (валидируется `time.LoadLocation`; zoneinfo встроен через `time/tzdata`, поэтому ошибка = битое имя, а не отсутствие базы в окружении); - включённые секции консистентны: `metadata.tmdb.enabled` → задан `api_key`; `jellyfin.enabled` → заданы `url`+`api_key`; `telegram.enabled` → `token`. **Таймзоны.** Хранение времени в БД и логи — всегда UTC. Зона **отображения** в веб-UI задаётся `[general].timezone` (дефолт `UTC`); только она конфигурируема, на хранение/сортировку/логи не влияет. Бизнес-логика оперирует временем с явным TZ (см. [CLAUDE.md](../../CLAUDE.md)). ## Структура в коде - Весь разбор и валидация — в `internal/config`; наружу отдаётся готовая `Config`. - Одна корневая структура `Config` с под-структурами по секциям (`QBittorrent`, `Paths`, `LLM`, `Metadata`, `Jellyfin`, `Worker`, `Recognition`, `Telegram`, `HTTP`, `Log`).