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

8.5 KiB
Raw Blame History

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

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

Краткая выжимка и инварианты — в 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 и т.п.
[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блоками-комментариями ниже, каждый со своим описанием полей (зачем/диапазон/единицы — как у обычных полей);
    • так из примера видны все варианты и поля каждого, не открывая код.
[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, «Безопасность».

Валидация и fail-fast

Конфиг валидируем на старте, до приёма трафика. Невалидный конфиг — лог ERROR и выход с ненулевым кодом (не стартуем «наполовину»).

Что проверяем (jellybit):

  • обязательные поля заданы (напр. qbittorrent.url, paths.*, storage.db_path);
  • пути paths.movies/series/downloads существуют и доступны; целевые — под единой песочницей (см. инварианты в 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.enabledtoken.

Таймзоны. Хранение времени в БД и логи — всегда UTC. Зона отображения в веб-UI задаётся [general].timezone (дефолт UTC); только она конфигурируема, на хранение/сортировку/логи не влияет. Бизнес-логика оперирует временем с явным TZ (см. CLAUDE.md).

Структура в коде

  • Весь разбор и валидация — в internal/config; наружу отдаётся готовая Config.
  • Одна корневая структура Config с под-структурами по секциям (QBittorrent, Paths, LLM, Metadata, Jellyfin, Worker, Recognition, Telegram, HTTP, Log).