Метки времени в 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>
8.5 KiB
Конфигурация
Конвенция: как устроена и грузится конфигурация 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 коммитим как единый справочник по конфигу: все секции
и все поля. Каждое поле снабжаем комментарием, из которого ясно:
- зачем поле — что оно меняет в поведении;
- диапазон/допустимые значения — перечисление или границы;
- единицы измерения, если применимо — секунды/миллисекунды, байты/КБ,
доля
0–1и т.п.
[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.enabled→token.
Таймзоны. Хранение времени в БД и логи — всегда UTC. Зона отображения в
веб-UI задаётся [general].timezone (дефолт UTC); только она конфигурируема,
на хранение/сортировку/логи не влияет. Бизнес-логика оперирует временем с явным
TZ (см. CLAUDE.md).
Структура в коде
- Весь разбор и валидация — в
internal/config; наружу отдаётся готоваяConfig. - Одна корневая структура
Configс под-структурами по секциям (QBittorrent,Paths,LLM,Metadata,Jellyfin,Worker,Recognition,Telegram,HTTP,Log).