# Беклог Единый список будущих задач по проекту: то, что уже решили сделать, и идеи, которые ещё надо обдумать. Это не план реализации (он — в [drafts/roadmap.md](drafts/roadmap.md)) и не источник истины: принятое и реализованное переезжает в `docs/specs`/`docs/adr`. Приоритет — грубая оценка «ценность / стоимость», не обязательство к порядку. Спекулятивные пункты (ещё без решения «делаем») помечены _(идея)_ — их сперва надо проработать. ## Высокий ### Проблема второго сезона Если первый сезон сериала уже разложен, а мы добавляем второй/третий/…, новый сезон должен лечь в **ту же** папку сериала, а не завести рядом почти одинаковую вторую. Разбор ([drafts/logical-title-model.md](drafts/logical-title-model.md)) показал: проблема не в группировке, а в **сходимости папки** — папка каждый раз печатается заново из выхода LLM, и совпадение `provider_id` не гарантирует совпадение строки («Fargo» vs «Фарго», год сезона vs год сериала). Отдельная сущность «тайтл» **не вводится**; решение — правило сходимости при построении плана: при подтверждённом матче наследовать базу папки от живых `file_link`'ов загрузок с тем же `(provider, provider_id)`, игнорируя LLM-выход; якоря нет — папка из распознавания, как сейчас (первая загрузка «печатает» имя). - [ ] lookup живых ссылок по `(provider, provider_id)` через current recognition - [ ] наследование базы папки (имя + год) при построении плана раскладки - [ ] рассинхрон (несколько живых папок с одним матчем) → review, не молча - [ ] тесты: сходимость, отсутствие якоря (свежая папка), смена провайдера Связано: [drafts/logical-title-model.md](drafts/logical-title-model.md) §5.2, [recognition.md](specs/recognition.md) (модель уверенности, матч в базе), [jellyfin-layout.md](specs/jellyfin-layout.md) (папка сериала с провайдер-id). ### Раздачи с докачиванием (слияние при повторном добавлении) Свежий сериал раздают по мере выхода: торрент содержит 5 эпизодов из 10, позже его перезаливают целиком, и пользователь добавляет раздачу повторно. Решение проработано ([drafts/logical-title-model.md](drafts/logical-title-model.md) §6.2): новая загрузка приходит в ту же папку за счёт правила сходимости, а раскладка становится **merge** — доложить только недостающее. Существующие пути не трогаем (never-overwrite, владение остаётся у старой загрузки), новые кладём (владеет новая). Split-ownership сезона (серии поделены между загрузками) принят как норма per-path модели; обе раздачи сидируют независимо. - [ ] в плане раскладки отличать «путь занят живой ссылкой того же матча» (→ пропустить) от настоящей коллизии (→ review, как сейчас) - [ ] merge-раскладка: существующее пропустить, недостающее доложить - [ ] показать итог в карточке: сколько доложено, сколько уже было - [ ] решить «слияние загрузок» при перезаливе той же вещи (одна строка `download` + новый infohash vs новая загрузка) — открытый вопрос черновика §10 Зависит от правила сходимости ([«Проблема второго сезона»](#проблема-второго-сезона)) и выигрывает от ULID-идентичности. Связано: [jellyfin-layout.md](specs/jellyfin-layout.md) (раскладка, идемпотентность), [workflow.md](specs/workflow.md) (повторный прогон загрузки). ### Удаление средствами jellybit («единое окно», path 2) Распознавание **ручного** удаления (источник из qBittorrent / цель из Jellyfin) и пометка рассинхрона уже сделаны: фоновая сверка по матрице «источник × цель» → состояния `target_missing`/`orphaned`/`deleted`, безопасный `undo` (не снимает последнюю копию, `nlink <= 1`), синхронный preflight перед действиями. См. `openspec/specs/state-reconciliation/`, [workflow.md](specs/workflow.md) → «Сверка с реальностью». Осталось (path 2) — продолжение «единого окна»: удалять просмотренное **из самого jellybit**, не идя руками в qBittorrent/Jellyfin. Решения из разбора ([drafts/logical-title-model.md](drafts/logical-title-model.md) §5.3, §6.4): «тайтл» — вычисляемая группа загрузок по `(provider, provider_id)` / общей папке, без новой сущности; удаление целиком — обход загрузок группы штатным undo; удаление раздачи из qBittorrent — осознанный выход за инвариант «источник неприкосновенен», только по явному подтверждению (не случайному клику). - [ ] удаление одной загрузки: снять её живые хардлинки (штатный undo, `superseded` пропускаем, `nlink`-гард) + опц. удалить раздачу из qBittorrent с файлами — с осознанным подтверждением - [ ] вычисляемая группа «тайтл» в UI: состав сериала/фильма (загрузки, сезоны, файлы) одним экраном - [ ] удаление тайтла целиком: обход загрузок группы + опц. снос опустевшей папки - [ ] после полного удаления память о тайтле не остаётся (линза без содержимого не нужна) Связано: [drafts/logical-title-model.md](drafts/logical-title-model.md), [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md), [architecture.md](specs/architecture.md) → «Раскладка файлов», [workflow.md](specs/workflow.md). ### Ретеншн и очистка БД Терминальные задачи (`done`/`cancelled`/`failed`/`reverted`), их попытки `recognition` с сырыми ответами LLM и `metadata_candidate` копятся вечно — со временем БД и список загрузок распухают и становятся нечитаемыми. Нужна авточистка старше N дней (с настройкой в `[storage]` или `[worker]`) и/или ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере эксплуатации. Связано: [architecture.md](specs/architecture.md) → «Хранилище» (таблицы `download`/`recognition`/`metadata_candidate`/`file_link`), пакет `store`. ### Eval-харнес распознавания (корпус кейсов + метрика точности) Распознавание — ядро продукта, но смена модели или правка промпта сейчас вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по нему с метрикой точности (тип/название/год/нумерация). Тогда можно сравнивать LLM-провайдеры и версии промпта по числам, а не на ощупь. Прогон — отдельной командой (`jellybit eval` или тестом), на фикстурах, без реального qBittorrent. Связано: [recognition.md](specs/recognition.md) (конвейер, модель уверенности), пакет `recognize`. ### НФТ: масштаб до 100 одновременных загрузок (потолок — 1000) Сейчас потолок по нагрузке нигде не зафиксирован: воркер, поллинг qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз». Записать в **нефункциональные требования** целевой ориентир — архитектура держит до **100 одновременных загрузок** в работе (приём → распознавание → раскладка), план-максимум — **1000**. Сама запись требования дешева и высокоценна: она задаёт рамку для решений ниже по списку. Отдельно (уже дороже) — аудит узких мест под эту цифру: одиночное соединение SQLite и сериализация записи, конкурентность воркера и лимит параллельных распознаваний, частота/стоимость поллинга и дедуп при наплыве. Связано: [architecture.md](specs/architecture.md) → «Отслеживание загрузки»/«Хранилище», пакеты `worker`, `store`, `qbt`, `llm`. ## Средний ### Словарь единого языка (ubiquitous language) Свести термины домена в один глоссарий, чтобы пользователь, документация, код и агент говорили на одном языке: загрузка, раздача, распознавание, матч, кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и т.д. — русский термин, английский идентификатор в коде, краткое определение. Сейчас наименования расходятся между спеками, UI и кодом, и в диалоге с агентом приходится каждый раз сверять понятия. Глоссарий — источник истины по именам; на нём же строится агент-ревьювер наименований (см. [«Агенты-ревьюверы качества»](#агенты-ревьюверы-качества-наименования-архитектура-конвенции-стиль)). Связано: [docs/conventions](conventions/README.md) (кросс-каттинг), [architecture.md](specs/architecture.md) (домен), новый файл-глоссарий. ### Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль) Набор узких сабагентов-ревьюверов поверх ревью-процесса из `CLAUDE.md`, каждый со своей оптикой: соответствие наименований словарю единого языка, соблюдение архитектурных границ (единое ядро/тонкие транспорты, инварианты безопасности данных), конвенций (ошибки, логирование, конфиг, TZ), стиля кода на высоком уровне и поиск дублирования. Запускаются как чекпоинт перед `archive`/коммитом. Развивает ревью-процесс OpenSpec в сторону воспроизводимых автоматических проверок, не заменяя человеческое ревью. Связано: `CLAUDE.md` (ревью-процесс, конвенции), [docs/conventions](conventions/README.md), [«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language). ### Пересмотр набора capabilities и рефакторинг спек Деление capabilities в OpenSpec сложилось по ходу миграции и смешивает разные действия в одной спеке. Пример: `recognition` держит и разбор через LLM, и **сверку с внешними базами** — а «поиск в базе» и «подтверждение матча официальным id» суть разные действия, значит и разные capability. Нужно пересмотреть набор и границы, чтобы имя capability отвечало одному поведению: - `recognition` — только разбор сигналов через LLM (план, тип, название, файлы → серии); - отдельные capability под работу с метабазами: поиск записей во внешних базах и сверку/подтверждение матча (разнести пока смешанное в `recognition`); - `web-ui` — только общее оформление, дизайн-система и общие компоненты страниц; - `review` — весь процесс ревью после распознавания и матча (мигрировать из [review-ux.md](specs/review-ux.md); сейчас поведение ревью не в OpenSpec). Работа чисто по спекам (границы, RENAMED/MOVED requirements), код не трогаем. Ценно тем, что снимает путаницу «какой capability трогать» на каждой задаче. Связано: `CLAUDE.md` (SDD, миграция capabilities), openspec/specs (`recognition`, `web-ui`), [review-ux.md](specs/review-ux.md), [«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language). ### Сила совпадения кандидата и пересмотр распознавания/матчинга _(идея)_ Сейчас у кандидата метабазы нет метрики силы совпадения (`metadata_candidate` хранит provider/id/title/year/url), а решение «авто vs review» — по правилу «единственный сильный матч + валидация», не по числовой уверенности. Для ревью это значит: список кандидатов нечем отсортировать/подсветить по уверенности — берём порядок сбора. Идея — ввести на этапе матча **силу совпадения кандидата** (точное совпадение названия+года vs частичное) для сортировки и подсказки в UI. Шире — отдельно продумать **сам процесс распознавания и матчинга**: границы «разбор LLM / поиск в базе / сверка», что храним у кандидата, как считаем и показываем уверенность. Требует проработки перед реализацией. Связано: [recognition.md](specs/recognition.md) (модель уверенности), [ADR-2026-06-13-auto-link-requires-db-match](adr/ADR-2026-06-13-auto-link-requires-db-match.md), [review-ux.md](specs/review-ux.md) (выбор источника в ревью, реализовано), [«Пересмотр набора capabilities»](#пересмотр-набора-capabilities-и-рефакторинг-спек). ### История переходов загрузки Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто инициировал — воркер, человек, сверка), а не только текущее состояние. Сейчас по задаче виден лишь актуальный статус, а разбор «как мы сюда попали» идёт по логам сервера. Отдельная таблица истории даёт лог переходов в карточке/расширенной информации и фундамент для метрик длительности стадий. Естественно ложится на собственный идентификатор загрузки. Связано: детальный экран загрузки (`/download/{id}`) уже реализован — лог переходов ложится в него; [drafts/logical-title-model.md](drafts/logical-title-model.md) §5.4 (схема `state_transition`, actor `worker|human|reconcile`), [workflow.md](specs/workflow.md) (граф состояний), [database.md](specs/database.md), пакеты `worker`, `store`. ### Машина состояний на go-библиотеке Сейчас FSM реализована вручную в `worker`. Выбрать подходящую go-библиотеку для описания воркфлоу/машины состояний и перевести переходы на неё — ради декларативности, проверяемости переходов и единого места правды. Кандидаты для оценки: `looplab/fsm`, `qmuntal/stateless` (и аналоги). Граф и переходы уже формализованы — переносим один в один. Связано: [workflow.md](specs/workflow.md) (текущий граф состояний). ### Привязка уведомлений к источнику в ботах (мульти-бот) Уведомления и запросы подтверждения должен получать тот, кто прислал загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней. Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся **единым для всех** и точкой правды по функциональности (боты — тонкие адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и идентификатор отправителя, маршрутизировать пинги по нему. Связано: [review-ux.md](specs/review-ux.md) (разделение труда транспортов, веб = точные правки), [architecture.md](specs/architecture.md) → «Транспорты». ### Улучшения UI: показывать матч с записью метабазы Web-сторона реализована: страница загрузки `/download/{id}` и экран ревью показывают, **с какой именно записью** метабазы (TMDB/TVDB/IMDb) сматчилась загрузка — провайдер, id и ссылку на запись. Осталось довести то же в **Telegram**: в уведомлениях/подтверждениях показывать запись матча (название, год, провайдер-id, ссылку), чтобы ошибочную привязку было видно и из бота. Полный выбор источника в вебе уже реализован — см. [review-ux.md](specs/review-ux.md). Связано: [review-ux.md](specs/review-ux.md), [recognition.md](specs/recognition.md) (матч в базе), [architecture.md](specs/architecture.md) → «Транспорты». ### Аниме с абсолютной нумерацией Релизы аниме часто нумеруют серии сквозным числом (`#137`) без сезонов, а Jellyfin ждёт `SxxEyy`. Нужен пересчёт абсолютной нумерации в сезон/серию — надёжнее всего через TVDB (там есть absolute order). Отдельный крайний случай распознавания; на стороне ревью — веб-хелпер «absolute → S·E». Связано: [recognition.md](specs/recognition.md) (конвейер, сезон-паки), [jellyfin-layout.md](specs/jellyfin-layout.md) (нумерация серий), [review-ux.md](specs/review-ux.md) (крайние сценарии). ### Добавление торрентов файлом/ссылкой — «единое окно» Поддержать источники помимо magnet: `.torrent`-файл и URL (отдаём их в qBittorrent, без исходящих запросов на пользовательский URL — SSRF исключён). Идеал — одно поле «единого окна»: кидаем туда текст или файл, а сервис сам разбирает, что это (magnet / ссылка / .torrent / сообщение бота), и заводит загрузку. Связано: [architecture.md](specs/architecture.md) → «Транспорты» (`source_type = magnet|torrent|url` уже в схеме), пакет `ingest` (сейчас поддержан только magnet). ### Бэкап SQLite `architecture.md` требует «бекапить data-том», но *как* — не описано. Без понятной стратегии сбой или редеплой стирают всё in-flight состояние. Зафиксировать решение и реализовать: периодический `VACUUM INTO` в `/data/backups` по расписанию (с ротацией) либо потоковая репликация (litestream). Лучше сделать, пока БД маленькая. Связано: [architecture.md](specs/architecture.md) → «Деплой» (data-том, «бекапить-и-не-терять»), пакет `store`. ### Глубокий healthcheck и статус зависимостей `/healthz` проверяет только сам сервис. Если qBittorrent, LLM или метабаза недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent недоступен»), чтобы причина простоя была видна сразу. Связано: [architecture.md](specs/architecture.md) → «Деплой» (healthcheck), пакеты `qbt`, `llm`, `metadata`, `httpapi`. ### Обучение на правках человека (few-shot из прошлых ревью) Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать похожие в будущие промпты. Системно повышает точность на «твоих» трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой верификации, но дешевле: учимся на уже собранных `hint`/`override`. Связано: [recognition.md](specs/recognition.md) (конвейер, промпт), [«Многоступенчатая верификация»](#многоступенчатая-верификация-привязки-идея), [architecture.md](specs/architecture.md) → «Хранилище» (`hint`, `override`). ## Низкий ### Баг: ссылка на запись TVDB всегда `/series/` (для фильмов ведёт не туда) `metadata.TVDB.Search` знает тип запроса (`series`/`movie`, [tvdb.go](../internal/metadata/tvdb.go) строки 156–159 — он идёт в API), но URL кандидата хардкодит `.../dereferrer/series/{id}` (там же, строка 179) **независимо от типа**. Для фильма ссылка ведёт на `/series/{id}` вместо `/movie/{id}`, хотя идентификатор верный. Пример: «Последний единорог», id 3015 → отдаём `https://www.thetvdb.com/dereferrer/series/3015`, рабочая — `https://www.thetvdb.com/dereferrer/movie/3015`. `matchURL`/`sourceMatchURL` (`internal/httpapi/review.go`) предпочитают сохранённый URL кандидата над `providerURL` (который тип учитывает верно), поэтому неверный `/series/` виден в UI. Фикс — подставлять тип запроса в URL (одна строка); заодно свериться, что `providerURL` не расходится по типам. Связано: [recognition.md](specs/recognition.md) (сверка с базой, кандидаты), пакеты `metadata`, `httpapi`. ### Мгновенные обновления через SSE Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает, но с задержкой в интервал опроса и холостыми запросами. Перевести динамический контент (прогресс загрузки, смена статуса, раздача) на Server-Sent Events, чтобы обновления приходили почти мгновенно и без лишнего поллинга. Поллинг работает, поэтому это улучшение, а не блокер; SSE — один долгоживущий ответ на соединение, ложится на server-rendered UI без тяжёлого фронтенда. Связано: [architecture.md](specs/architecture.md) → «Транспорты», [review-ux.md](specs/review-ux.md), пакет `httpapi`. ### Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p) По калибровке болей (2026-07-02, [drafts/logical-title-model.md](drafts/logical-title-model.md) §6.3) — **не боль**, из приоритета выпало. Сосуществование версий доступно уже сейчас (Jellyfin multi-version, другой целевой путь), коллизия на тот же путь штатно уходит в review. Явный replace (undo старого хардлинка → lay нового → супересид владения путём) — отдельный change, если/когда станет болью. Связано: [«Раздачи с докачиванием»](#раздачи-с-докачиванием-слияние-при-повторном-добавлении), [jellyfin-layout.md](specs/jellyfin-layout.md) (never-overwrite, коллизия). ### Многоступенчатая верификация привязки _(идея)_ Несколько раз извлекать данные из раздачи и контекста разными промптами, искать в метабазах, затем сводить результаты в общий вердикт (голосование/консенсус) — выше точность ценой нескольких вызовов LLM и запросов к базам. Требует проработки: когда включать, как мерджить расхождения, стоимость/латентность. Связано: [recognition.md](specs/recognition.md) (конвейер и модель уверенности). ### Выбор из нескольких находок метабазы в Telegram Когда распознавание даёт несколько подходящих кандидатов в метабазе, предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча брать первый/лучший. Веб остаётся точкой точных правок (полный выбор источника уже реализован — см. [review-ux.md](specs/review-ux.md)), бот — быстрый выбор из готового короткого списка. Связано: [review-ux.md](specs/review-ux.md) (боты — быстрые действия, веб — точные правки), [recognition.md](specs/recognition.md) (кандидаты матча). ### Проверка свободного места перед copy-fallback Когда хардлинк невозможен (`EXDEV`/`ENOTSUP`/…), `layout` копирует файл, дублируя место на диске. На забитом диске это упрётся в полку посреди раскладки. Перед копированием проверять доступное место и при нехватке внятно уходить в `failed` с понятной причиной, а не падать на полпути. Связано: [architecture.md](specs/architecture.md) → «Раскладка файлов» (фолбэк-копирование), пакет `layout`. ### Кэш метабаз (и опционально LLM) Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее полезен, т.к. вход меняется подсказками). Связано: [recognition.md](specs/recognition.md) (сверка с базой), пакеты `metadata`, `llm`. ### guessit как сервис-спутник _(идея)_ `go-ptn` слабее питоновского `guessit`. Если точности пред-парса не хватит — завернуть `guessit` в крошечный HTTP-сервис (один файл, поставляется рядом с бинарём jellybit) и спрашивать его на шаге пред-парса. Сохраняет «доставку копированием»: два файла вместо одного. Связано: [recognition.md](specs/recognition.md) → «На будущее» (пред-парс). ### Завершение загрузки через webhook _(идея)_ Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд. Альтернатива: «Run external program on torrent completion» в qBittorrent дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом qBittorrent. Решим по опыту эксплуатации. Связано: [architecture.md](specs/architecture.md) → «Отслеживание загрузки», пакет `worker`. ### Авторизация веб-UI (на будущее) Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей (`http.trusted_subnets`) — как умеет qBittorrent. Если понадобится защита: токен/Basic в самом приложении или вынос за reverse-proxy с аутентификацией. Связано: [architecture.md](specs/architecture.md) → «Транспорты» (доступ к веб-UI), пакет `httpapi`. ### Современный Web-UI как PWA Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое, отзывчивое, удобное с телефона). Текущий server-rendered UI функционален, поэтому это улучшение, а не блокер; большой объём работы. Связано: [review-ux.md](specs/review-ux.md) (веб = точные правки), пакет `httpapi`.