diff --git a/docs/backlog/README.md b/docs/backlog/README.md index 40ae296..a3315a0 100644 --- a/docs/backlog/README.md +++ b/docs/backlog/README.md @@ -30,6 +30,9 @@ Tududi (проект `jellybit`) больше **не** держит беклог - [[идея] Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки](slozhnye-serialnye-razdachi.md) — ИДЕЯ (проработать крайние случаи) - [Аниме с абсолютной нумерацией](anime-absolyutnaya-numeraciya.md) — Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а Jellyfin ждёт… - [Бэкап SQLite](backup-sqlite.md) — architecture +- [Сигнал Jellyfin после отката и удаления файлов](jellyfin-skan-posle-udaleniya.md) — Скан шлётся только на done; после reverted/deleted Jellyfin держит битые записи. Клиент и гейт готовы, но скана нет в openspec-спеках +- [Режиссёр в блоке «Распознано как» на странице загрузки](rezhisser-v-kartochke-zagruzki.md) — На /download/{id} режиссёр всегда прочерк (поля нет в шаблоне); экран ревью его уже выводит +- [Карточка информации о торренте вылезает за границу экрана на мобильном](mobilnaya-verstka-kv-infohash.md) — grid-blowout на .kv из-за неразрывного infohash — страница получает горизонтальный скролл - [Глубокий healthcheck и статус зависимостей](healthcheck-zavisimosti.md) — /healthz проверяет только сам сервис - [НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)](masshtab-100-zagruzok.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг) - [Обучение на правках человека (few-shot из прошлых ревью)](obuchenie-na-pravkah.md) — Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать… @@ -40,6 +43,7 @@ Tududi (проект `jellybit`) больше **не** держит беклог ## Низкий +- [Кнопки выглядят как кнопки: `btn-ghost` и рассогласование весов](unifikaciya-knopok-btn-ghost.md) — «Откатить» неотличима от текста: btn-ghost прозрачен, аффорданс только на hover; плюс одно действие имеет разный вес в списке и на странице - [Ревью уведомлений в Telegram (аудит текстов и формата)](telegram-revyu-uvedomleniy.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи - [Download id в Telegram моноширинным (code) для tap-to-copy](telegram-download-id-code.md) — слать id как `code`; требует включить parse mode (HTML) в send() + escape всех текстов - [Мгновенные обновления через SSE](sse-obnovleniya.md) — Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто и работает… diff --git a/docs/backlog/jellyfin-skan-posle-udaleniya.md b/docs/backlog/jellyfin-skan-posle-udaleniya.md new file mode 100644 index 0000000..725a460 --- /dev/null +++ b/docs/backlog/jellyfin-skan-posle-udaleniya.md @@ -0,0 +1,59 @@ +# Сигнал Jellyfin после отката и удаления файлов + +**Приоритет:** средний + +Сейчас пересканирование Jellyfin шлётся **только** при входе в `done`. После +отката (`reverted`) или удаления (`deleted`) хардлинки сняты, а Jellyfin +продолжает показывать записи с битыми путями до следующего скана по расписанию. + +Интеграция уже есть целиком, клиент писать не надо: + +- `internal/jellyfin/jellyfin.go` — клиент, `RefreshLibraries()` (`:76`) → + `POST /Library/Refresh`. Тесты — `internal/jellyfin/jellyfin_test.go`. +- Конфиг: `internal/config/config.go:99-107` (`Jellyfin{Enabled,URL,APIKey,Proxy,Timeout}`), + валидация `:305-308`, пример `config.example.toml:59-64`. +- Проводка: `cmd/jellybit/serve.go:137-152` → `wrk.SetScanner(jf)`; интерфейс + `internal/worker/worker.go:171-176`. + +Точка правки — гейт в `transitionErr` (`internal/worker/worker.go:846-852`): +`if w.scanner != nil && state == store.StateDone`. Оба пути удаления уже проходят +через ту же `transition`: `Undo()` (`internal/worker/review.go:535` → `:574`) и +`Delete()` (`:601` → `:657`). То есть база правки = расширить условие; фоновый +ctx, `capFileLayout`-скоуп и неблокирующая горутина переиспользуются как есть. + +## Что учесть + +1. **`StateDeleted` приходит не только от пользователя.** + `internal/worker/reconcile.go:30-41` `deriveState()` возвращает `StateDeleted` + при `!sourcePresent && !targetPresent` (авто-сверка). Скан там формально уместен, + но это уже не «после удаления файлов нами». Решить: гейтить по состоянию (просто, + ловит и reconcile) или по факту снятия ссылок (точнее — в `Undo`/`Delete` есть + счётчик снятого, но тогда триггер уезжает из единого чекпоинта `transitionErr`). +2. **`StateTargetMissing`** (`reconcile.go:35`) — цель пропала мимо нас. Кандидат + по той же логике, надо явно решить, входит или нет. +3. **`Dismiss` идёт мимо чекпоинта:** `internal/worker/worker.go:944` пишет + состояние напрямую через `w.store.SetDownloadState`, минуя `transitionErr`. Для + dismiss это корректно (файлы не трогаются), но если вешать скан на `cancelled` — + не сработает. +4. `Delete()` при ошибке qBittorrent (`internal/worker/review.go:650-652`) + возвращается **до** `transition` → ссылки сняты, скана не будет. Идемпотентный + повтор дожмёт, но окно рассинхрона есть. +5. Порядок верный: `layouter.Undo` отрабатывает до `transition`, так что скан + увидит уже снятые ссылки. + +## Спеки — здесь дыра + +Про Jellyfin-скан в `openspec/specs/` **нет ни слова** (грепом +`Library/Refresh|RefreshLibraries|пересканир` — ноль попаданий). Живёт только в +рукописных доках: `docs/specs/architecture.md:182-196` («Пересканирование +Jellyfin», прямо сказано «После успешной раскладки (вход в `done`)»), +`docs/specs/workflow.md:100-102`. + +→ Задача тянет дельту в `file-layout` (пакет `jellyfin` отнесён к этой capability — +`architecture.md:44`, код скоупится `capFileLayout`) + правку +`docs/specs/architecture.md:182` и `docs/specs/workflow.md:100`, где формулировка +«при входе в done» станет неверной. + +Тесты: `internal/worker/review_test.go:88` `TestScanner_FiresOnDone` + +`recordingScanner` (`:81-86`). Негативных тестов «не стреляет на других состояниях» +нет → расширение безопасно, но тесты на `reverted`/`deleted` надо дописать. diff --git a/docs/backlog/mobilnaya-verstka-kv-infohash.md b/docs/backlog/mobilnaya-verstka-kv-infohash.md new file mode 100644 index 0000000..fa54c67 --- /dev/null +++ b/docs/backlog/mobilnaya-verstka-kv-infohash.md @@ -0,0 +1,56 @@ +# Карточка информации о торренте вылезает за границу экрана на мобильном + +**Приоритет:** средний + +На узком экране блок «Информация о торренте» на `/download/{id}` выталкивает +страницу за вьюпорт — появляется горизонтальный скролл всей страницы. + +## Причина + +Grid-blowout на `.kv` из-за неразрывного infohash: + +- `web/templates/partials/download_main.html:83-86` — `
` с + `
{{.}}
` для каждого инфохэша. +- `web/static/css/jellybit.css:448-451` — `.kv{grid-template-columns:auto 1fr}`, + у `.kv dd` **нет** `overflow-wrap`/`word-break`/`min-width:0`. Трек `1fr` = + `minmax(auto,1fr)`, его автоминимум = min-content = весь 40-символьный хэш. + +Арифметика на 360px: доступно ≈302px, требуется ≈380px (`dt` «infohash» nowrap +≈52px + gap 16px + хэш ≈312px) → переполнение ~78px. `overflow-x:hidden` на +`body`/`html` нигде нет, поэтому блок физически ломает страницу. + +Недавние коммиты про ellipsis (`44c5952` — только `.btn-file`) и `.type-ico` +(`3f80d36`) этот блок не трогали. + +## Что уже защищено (не виновники) + +- `jellybit.css:283-285` — `.src-full{word-break:break-all}` → magnet уже чинится. +- `jellybit.css:365-366` — `.table-wrap{overflow-x:auto}` + `table.tbl{min-width:560px}` + → таблица раскладки скроллится внутри себя. Страницу не ломает, но 560px + принудительного горизонтального скролла на мобильном — отдельная заноза. +- `.fname`/`.link-to` (`:374`, `:378-379`), `.card-title` (`:205`), + `.review-title` (`:298`) — `word-break` есть. + +## Развилки + +- **(а) точечно:** `.kv dd{overflow-wrap:anywhere;min-width:0}` + + `.kv dd.mono{word-break:break-all}`. +- **(б) в медиа-запросе** перевести `.kv` в одну колонку + (`grid-template-columns:1fr`, `dd{text-align:left}`) — заодно снимает + проблему `dt{white-space:nowrap}`. + +Учесть: `.kv` используется **и** в блоке «Распознано как» +(`download_main.html:34-43`) — правка затронет оба, то есть чинится и потенциально +длинный `RecTitle`. Страховку `body{overflow-x:hidden}` как единственную меру не +брать — маскирует, а не лечит. + +Мобильных стилей почти нет: единственный `@media (max-width:640px)` — +`jellybit.css:477-492` (14 правил), ни `.kv`, ни `.table-wrap` в нём не упомянуты. + +## Спеки + +`openspec/specs/web-ui/spec.md:237-247` («Блок информации о торренте») требований +к адаптивности не содержит → либо чистый bugfix, либо дельта с требованием +переноса длинных значений. + +Связано: [[web-ui-pwa]] (крупная переделка, этот фикс — точечный). diff --git a/docs/backlog/rezhisser-v-kartochke-zagruzki.md b/docs/backlog/rezhisser-v-kartochke-zagruzki.md new file mode 100644 index 0000000..db9af55 --- /dev/null +++ b/docs/backlog/rezhisser-v-kartochke-zagruzki.md @@ -0,0 +1,50 @@ +# Режиссёр в блоке «Распознано как» на странице загрузки + +**Приоритет:** средний + +На странице `/download/{id}` в блоке «Распознано как» режиссёр всегда показан +прочерком — поля просто нет: +`web/templates/partials/download_main.html:40` содержит захардкоженное +`
Режиссёр
`. + +Экран ревью режиссёра уже выводит (`internal/httpapi/review.go:49/122`, +`review_source_block.html:59`) — это сделано в change про слоистое разрешение +полей display_name; страницу загрузки он не тронул. + +Данные для вывода есть: `Plan.Director` (`internal/recognize/recognize.go:91`) +персистится в JSON-колонке `recognition.plan`, а страница загрузки уже получает +эффективный план через `Reviewer.ReviewData(...)` +(`internal/httpapi/download.go:89`) — `rd.Plan` уже с наложенными override +(`internal/worker/review.go:987`, `:1271`). + +## Развилка (решить до кода) + +`rd.Plan.Director` — это слой «override → распознавание+матч», **без слоя +контекста**. Полное слоистое разрешение живёт в неэкспортируемом +`effectiveDisplayName(d, plan)` (`internal/worker/review.go:1118`, fallback +`plan.Director → ctxf.Director` на `:1127-1130`) и отдаёт готовую строку, а не +поля. + +- **A (минимум, ~3 строки):** поле `Director` в `downloadDetailView` + (`internal/httpapi/download.go:37`) + присвоение `view.Director = + rd.Plan.Director` рядом с `:143`. Но при распознавании без матча режиссёр из + контекста не покажется, хотя в заголовке страницы (display name) он уже есть → + видимая нестыковка «в заголовке есть, в поле прочерк». +- **B (правильный):** вынести слоистое разрешение в экспортируемое + `naming.Fields`/`EffectiveFields(d, plan)`, переиспользовать в + `effectiveDisplayName` и в **обоих** билдерах вью. Чинит заодно ту же дыру в + `internal/httpapi/review.go:122`. + +Рекомендация — B: A оставляет ровно тот баг, который заводили. + +## Спеки + +- `openspec/specs/web-ui/spec.md` — про блок «Распознано как» на `/download/{id}` + режиссёра нет вообще → нужна дельта. +- `openspec/specs/review/spec.md:255-289` — сценарий «Режиссёр показан, когда + доступен» ограничен экраном ревью и формулировкой «из подтверждённого + матча/кандидата»; вариант B потребует правки формулировки. +- `openspec/specs/recognition/spec.md:62-69` — режиссёр опционален и best-effort, + пустое значение штатно. + +Связано: [[ubiquitous-language-slovar]]. diff --git a/docs/backlog/unifikaciya-knopok-btn-ghost.md b/docs/backlog/unifikaciya-knopok-btn-ghost.md new file mode 100644 index 0000000..7901140 --- /dev/null +++ b/docs/backlog/unifikaciya-knopok-btn-ghost.md @@ -0,0 +1,64 @@ +# Кнопки выглядят как кнопки: `btn-ghost` и рассогласование весов + +**Приоритет:** низкий + +Кнопка «Откатить» в списке загрузок неотличима от текста/ссылки. Это **не** +разметка — там уже `