Files
jellybit/docs/todo.md
T
avandClaude Opus 4.8 cc7e51b3a4 Обработка рассинхрона состояния с реальностью (state-reconciliation)
Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели
(разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий.

- Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице
  «источник × цель» → состояния target_missing/orphaned/deleted, переходы и
  самовосстановление (healing).
- worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс
  пропажи источника (порог [worker].source_missing_threshold) и синхронный
  preflight перед действиями (relink/recognize/apply/undo) — не доверяем
  state в БД.
- layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника),
  отказ всего батча без частичного отката (ErrLastCopy).
- store: единый список terminalStates для IsTerminal и FindActiveByInfohash
  (иначе семантика «активности» разъезжается), столбец source_miss_count,
  миграция 0003.
- httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне.
- Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config.

Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:07:09 +03:00

279 lines
21 KiB
Markdown

# TODO
Конкретные задачи на будущее, ранжированные по приоритету. Это не план
реализации (он — в [drafts/roadmap.md](drafts/roadmap.md)) и не свалка
идей ([drafts/ideas.md](drafts/ideas.md)): сюда попадает то, что уже решили
сделать, но ещё не сделали. Принятое и реализованное переезжает в
`docs/specs`/`docs/adr`.
Приоритет — грубая оценка «ценность / стоимость», не обязательство к
порядку.
## Высокий
### Проблема второго сезона
Если первый сезон сериала уже разложен, а мы добавляем второй/третий/…,
распознавание должно привязать новый сезон к **тому же** названию и папке,
а не завести рядом почти одинаковую вторую папку. Ключ — стабильный
`provider_id`: один и тот же `[tvdbid-…]` → одна папка сериала, новые
`Season NN` доливаются внутрь. Нужно: при матче учитывать уже существующие
в библиотеке сериалы (или прошлые распознавания с тем же провайдер-id) и
склонять LLM/выбор кандидата к согласованности с ними.
Связано: [recognition.md](specs/recognition.md) (модель уверенности,
матч в базе), [jellyfin-layout.md](specs/jellyfin-layout.md) (папка
сериала с провайдер-id).
### Удаление средствами 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. Нужно
продумать: команду удаления (снять наши хардлинки + опц. удалить раздачу из
qBittorrent с файлами), подтверждение осознанности (а не случайный клик) и
как это сочетается с инвариантом «источник неприкосновенен», когда
пользователь сам просит убрать источник.
Связано: [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md),
[architecture.md](specs/architecture.md) → «Раскладка файлов»,
[workflow.md](specs/workflow.md).
### Наблюдаемость: метрики и учёт стоимости LLM
Сейчас единственное окно в систему — `slog`. Нет быстрых ответов на
вопросы «сколько задач висит в review», «сколько токенов и денег съело
распознавание», «какова медиана времени ingest → done». Нужны метрики:
эндпоинт `/metrics` (Prometheus-формат) со счётчиками загрузок по
состояниям, длительностями стадий и расходом LLM (токены/стоимость на
задачу).
**Расход LLM уже снимается с провода**`llm.openai` парсит `usage`
(prompt/completion/total tokens и `cost`, который отдаёт шлюз) в
`llm.Response.Usage` и пишет в лог. Не хватает только **персистентности и
отображения**: сохранять `usage` у попытки распознавания (`recognition`) и
показывать в карточке загрузки + агрегатом в `/metrics`. Где провайдер не
шлёт `cost` — считать из токенов по таблице «модель → цена» в конфиге.
Отдельный LLM-прокси (LiteLLM и т.п.) для этого **не нужен** и противоречит
принципам «один бинарь» / «минимум компонентов»: подсчёт токенов уже в коде,
а роль мульти-модельного шлюза играет используемый OpenAI-совместимый
эндпоинт (он и возвращает `cost`); разные модели подключаются сменой
`[llm].model` или новым типом провайдера за интерфейсом `llm.Provider`.
Связано: [architecture.md](specs/architecture.md) → «Логирование»,
[recognition.md](specs/recognition.md) (провайдер LLM, `[llm].type`),
пакеты `worker`, `llm`, `httpapi`.
### Ретеншн и очистка БД
Терминальные задачи (`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`.
## Средний
### Машина состояний на 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) →
«Транспорты».
### Раздачи с докачиванием (слияние при повторном добавлении)
Свежий сериал часто раздают по мере выхода: торрент содержит 5 эпизодов из
10. Позже его перезаливают целиком (или добавляют недостающие серии), и
пользователь повторно добавляет тот же торрент. Нужно распознать, что это
**та же** раздача/сезон, и повторить раскладку с **слиянием**: доложить
недостающие хардлинки, не дублируя уже разложенное и не перезаписывая
существующее (инвариант «существующее не трогаем»). Перекликается с
«Проблемой второго сезона», но здесь доливаются эпизоды внутри одного
сезона, а не новый сезон. Нужно продумать: как опознать повторное
добавление (хеш торрента / провайдер-id + сезон), как сверять состав файлов
и доливать только новые.
Связано: [«Проблема второго сезона»](#проблема-второго-сезона),
[jellyfin-layout.md](specs/jellyfin-layout.md) (раскладка, идемпотентность),
[workflow.md](specs/workflow.md) (повторный прогон загрузки).
### Улучшения UI клиентов: показывать матч с записью метабазы
Во всех транспортах (веб, Telegram) показывать, **с какой именно записью**
метабазы (TMDB/TVDB) сматчилась загрузка: название, год, провайдер-id,
ссылку. Сейчас результат распознавания непрозрачен — пользователь не видит,
к чему привязались, и не может быстро поймать ошибочный матч.
Связано: [review-ux.md](specs/review-ux.md), [recognition.md](specs/recognition.md)
(матч в базе), [architecture.md](specs/architecture.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`.
### Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)
Фильм уже разложен, позже добавили раздачу лучшего качества — сейчас это
просто новая задача, упирающаяся в «коллизию цели → review», без понятия
«это та же вещь, заменить версию». Нужно осознанно обработать апгрейд
качества: распознать тот же тайтл, предложить замену существующей раскладки
либо сосуществование версий (Jellyfin поддерживает несколько версий одного
фильма). Близко к «докачиванию», но про качество, а не про эпизоды.
Связано: [«Раздачи с докачиванием»](#раздачи-с-докачиванием-слияние-при-повторном-добавлении),
[jellyfin-layout.md](specs/jellyfin-layout.md) (never-overwrite, коллизия),
[architecture.md](specs/architecture.md) → «Идентификация торрента»
(репаки = разные infohash → разные задачи).
### Глубокий 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`).
### Список загрузок: фильтр, поиск, пагинация
Прямое следствие роста БД (см. «Ретеншн»): плоский список загрузок со
временем становится непригоден. Нужны фильтр по состоянию, поиск по
названию и пагинация. Естественно ложится на экран расширенной информации.
Связано: [«Расширенная информация о загрузке в web-UI»](#расширенная-информация-о-загрузке-в-web-ui),
пакет `httpapi`.
## Низкий
### Многоступенчатая верификация привязки (тема для размышления)
Идея: несколько раз извлекать данные из раздачи и контекста разными
промптами, искать в метабазах, затем сводить результаты в общий вердикт
(голосование/консенсус) — выше точность ценой нескольких вызовов LLM и
запросов к базам. Требует проработки: когда включать, как мерджить
расхождения, стоимость/латентность.
Связано: [recognition.md](specs/recognition.md) (конвейер и модель
уверенности).
### Расширенная информация о загрузке в web-UI
Экран просмотра деталей одной загрузки: исходный контекст и magnet, лог
переходов состояний, распознанные данные и матч в метабазе (см. «показывать
матч»), целевые пути и созданные хардлинки. Помогает разбираться, когда
что-то пошло не так, без чтения логов сервера.
Связано: [review-ux.md](specs/review-ux.md), пакет `httpapi`.
### Выбор из нескольких находок метабазы в Telegram
Когда распознавание даёт несколько подходящих кандидатов в метабазе,
предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча
брать первый/лучший. Веб остаётся точкой точных правок, бот — быстрый выбор
из готового короткого списка.
Связано: [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`.
### Современный Web-UI как PWA
Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое,
отзывчивое, удобное с телефона). Текущий server-rendered UI функционален,
поэтому это улучшение, а не блокер; большой объём работы.
Связано: [review-ux.md](specs/review-ux.md) (веб = точные правки),
пакет `httpapi`.