Files
jellybit/docs/conventions/config.md
T
avandClaude Opus 4.8 5d5456fa68 Хранение времени: RFC 3339 (UTC) + таймзона отображения в конфиге
Метки времени в SQLite переведены с формата datetime('now')
(«2006-01-02 15:04:05») на RFC 3339 всегда-UTC («2006-01-02T15:04:05Z»):
самоописываемое хранилище (зона в значении), валидный ISO 8601, единый
формат с логами. Фиксированная ширина сохраняет лексикографическую
сортировку TEXT = хронологию (COALESCE(source_added_at, created_at)).

- Единая точка генерации времени в Go: store.Now()/FormatTime; DEFAULT
  (datetime('now')) снят со всех колонок — время всегда пишет приложение
  (зеркально ident.NewID для id), fail-loud при забытой вставке (NOT NULL).
  Все INSERT-сайты в store передают created_at/updated_at явно.
- Миграция 0008 (rebuild 7 таблиц без DEFAULT + backfill strftime, FK/PK/
  индексы сохранены байт-в-байт по образцу 0006); симметричная down.
- Новая секция конфига [general] с полем timezone (дефолт UTC) — зона
  ОТОБРАЖЕНИЯ в веб-UI; хранение остаётся UTC. Жёсткая валидация зоны на
  старте; zoneinfo встроен (time/tzdata), заменён зашитый Europe/Moscow.
- Тесты: round-trip миграции (up/down, NULL source_added_at), валидация
  зоны, сдвиг даты по зоне; обновлены фикстуры и TestUlidMigration.
- Docs: конвенции database/config, ER-схема; спека web-ui (таймзона).

OpenSpec change time-storage-rfc3339 (заархивирован).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:32:07 +03:00

133 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Конфигурация
Конвенция: *как* устроена и грузится конфигурация jellybit (TOML).
Правила оформления кода (How), не спецификация поведения.
Краткая выжимка и инварианты — в [CLAUDE.md](../../CLAUDE.md), раздел
«Конвенции кода».
> Каркас. Загрузчик `internal/config/config.go` уже грузит TOML; валидация
> на старте — в работе (`TODO`), обкатывается на следующем шаге.
## Принципы
- **Конфигурация — только TOML.** Env-переменные для конфига **не
используем**: окружение наследуется дочерними процессами и видно через
`/proc/<pid>/environ` — для секретов это слабее файла под `0600`.
- Грузим **один раз при старте** в одну типизированную структуру `Config`
(под-структуры по секциям). Дальше по коду читаем только её — никаких
`os.Getenv`/чтения файла в бизнес-коде, только загрузчик `internal/config`.
- Конфиг **неизменяем** после старта; смена параметров — рестарт процесса.
## Файл и поиск
- Имя конфига по умолчанию — **`config.toml`**, ищется в **рабочей
директории** процесса.
- Путь переопределяется опцией **`--config=path`**.
- Образец в репозитории — **`config.example.toml`** (см. ниже); реальный
`config.toml` не коммитится.
## config.example.toml — самодокументируемый образец
`config.example.toml` коммитим как единый справочник по конфигу: все секции
и все поля. **Каждое поле снабжаем комментарием**, из которого ясно:
- **зачем** поле — что оно меняет в поведении;
- **диапазон/допустимые значения** — перечисление или границы;
- **единицы измерения**, если применимо — секунды/миллисекунды, байты/КБ,
доля `01` и т.п.
```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 = "<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`).