# Конфигурация

Конвенция: *как* устроена и грузится конфигурация jellybit (TOML).
Правила оформления кода (How), не спецификация поведения.

Краткая выжимка и инварианты — в [CLAUDE.md](../../CLAUDE.md), раздел
«Конвенции кода».

## Принципы

- **Конфигурация — только TOML.** Env-переменные для конфига **не
  используем**: окружение наследуется дочерними процессами и видно через
  `/proc/<pid>/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  = "<duration>"      # как часто опрашивать qBittorrent; Go-duration (s/m/h)
magnet_timeout = "<duration>"      # ждать метаданные magnet не дольше; Go-duration
source_missing_threshold = <N>     # тиков поллинга без раздачи, чтобы счесть источник удалённым

[recognition]
auto_confidence_threshold = <0.0–1.0>  # порог авто-раскладки без ревью; доля

[llm]
max_retries = <N>                  # попыток получить валидный ответ LLM; целое ≥ 0
```

Значения намеренно заменены плейсхолдерами: предмет конвенции — форма
комментария, а числа, совпадающие с фактом до цифры, от факта неотличимы и
начинают врать молча при смене умолчания. Действующие умолчания и их
смысл живут одним домом — таблица «Настройки с числовым значением» в
[../database.md](../database.md); `config.example.toml` — источник истины по
составу полей.

Секретные поля оставляем пустыми — значение приходит из деплоя (см.
«Секреты»).

## Поля по дискриминатору `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 = "<other>" ---
# [llm]
# type  = "<other>"                 # описание варианта
# ...                               # его обязательные/опциональные поля
```

## Секреты

Секреты доставляет **деплой**, рендеря их прямо в `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`).
