Беклог: перенос четырёх задач из инбокса Tududi
Инбокс Tududi разгружен: четыре открытые задачи проекта оформлены файлами в docs/backlog и помечены выполненными в Tududi. - режиссёр в блоке «Распознано как» на странице загрузки (средний); - карточка информации о торренте вылезает за экран на мобильном (средний); - сигнал Jellyfin после отката и удаления файлов (средний); - кнопки выглядят как кнопки: btn-ghost и рассогласование весов (низкий). Каждая задача проверена по коду: все четыре актуальны, ни одну не закрыли прошлые коммиты. В телах — точки правки, развилки и нужные дельты спек. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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) — просто и работает…
|
||||
|
||||
@@ -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` надо дописать.
|
||||
@@ -0,0 +1,56 @@
|
||||
# Карточка информации о торренте вылезает за границу экрана на мобильном
|
||||
|
||||
**Приоритет:** средний
|
||||
|
||||
На узком экране блок «Информация о торренте» на `/download/{id}` выталкивает
|
||||
страницу за вьюпорт — появляется горизонтальный скролл всей страницы.
|
||||
|
||||
## Причина
|
||||
|
||||
Grid-blowout на `.kv` из-за неразрывного infohash:
|
||||
|
||||
- `web/templates/partials/download_main.html:83-86` — `<dl class="kv">` с
|
||||
`<dd class="mono">{{.}}</dd>` для каждого инфохэша.
|
||||
- `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]] (крупная переделка, этот фикс — точечный).
|
||||
@@ -0,0 +1,50 @@
|
||||
# Режиссёр в блоке «Распознано как» на странице загрузки
|
||||
|
||||
**Приоритет:** средний
|
||||
|
||||
На странице `/download/{id}` в блоке «Распознано как» режиссёр всегда показан
|
||||
прочерком — поля просто нет:
|
||||
`web/templates/partials/download_main.html:40` содержит захардкоженное
|
||||
`<dt>Режиссёр</dt><dd><span class="faint">—</span></dd>`.
|
||||
|
||||
Экран ревью режиссёра уже выводит (`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]].
|
||||
@@ -0,0 +1,64 @@
|
||||
# Кнопки выглядят как кнопки: `btn-ghost` и рассогласование весов
|
||||
|
||||
**Приоритет:** низкий
|
||||
|
||||
Кнопка «Откатить» в списке загрузок неотличима от текста/ссылки. Это **не**
|
||||
разметка — там уже `<button type="submit">` внутри `<form>`; проблема чисто
|
||||
визуальная, в классе `btn-ghost`:
|
||||
|
||||
- `web/static/css/jellybit.css:165-166` — `.btn-ghost{background:transparent;
|
||||
border-color:transparent;color:var(--text-dim)}`. Прозрачный фон + прозрачный
|
||||
бордер + приглушённый текст = в покое нет аффорданса. Он появляется только на
|
||||
`:hover`, а на тач-устройствах hover'а нет вообще.
|
||||
|
||||
## Инвентаризация
|
||||
|
||||
| Действие | Список (`card.html`) | Страница (`download_main.html`) |
|
||||
|---|---|---|
|
||||
| Ревью | `:28` `btn btn-primary btn-sm` | `:21` `btn btn-primary` |
|
||||
| **Откатить** | `:29` **`btn btn-sm btn-ghost`** | `:23` **`btn btn-danger`** «↩ Откатить» |
|
||||
| Привязать заново | `:30` `btn btn-sm` | `:24` `btn` |
|
||||
| Повторить | `:31` `btn btn-sm` | `:25` `btn` |
|
||||
| **Отменить/Отклонить** | `:32` **`btn btn-sm btn-danger`** | `:26` **`btn btn-ghost`** |
|
||||
| Обновить имя | — | `:22` `btn btn-ghost` |
|
||||
|
||||
Расхождения:
|
||||
|
||||
1. `btn-ghost` — корень проблемы, 3 места: `card.html:29`,
|
||||
`download_main.html:22`, `download_main.html:26`.
|
||||
2. «Откатить» несогласован сам с собой: невидимая `btn-ghost` в списке против
|
||||
красной `btn-danger` на странице.
|
||||
3. «Отменить» инвертирован ровно наоборот: `btn-danger` в списке против
|
||||
`btn-ghost` на странице. В списке Откатить тише Отменить, на странице — наоборот.
|
||||
4. Эмодзи-префиксы несогласованы: `↩ Откатить` (страница) vs `Откатить` (список);
|
||||
`review_main.html:76-78` — эмодзи на всех (`🕗/❌/✅`), `card.html` — ни одного.
|
||||
5. Вероятно ок: `review_main.html:54,58`, `review_source_block.html:49`,
|
||||
`index.html:32,65-67` (пагинация — ссылка-как-кнопка законно),
|
||||
`download_main.html:112,124` (danger-zone).
|
||||
6. `.copy-btn` (`card.html:9`, `download_main.html:15,90`) — отдельная система
|
||||
классов (`jellybit.css:210-214`), не `.btn`. Формально вне скоупа, но при
|
||||
унификации всплывёт.
|
||||
|
||||
База CSS — `jellybit.css:153-171`.
|
||||
|
||||
## Развилки
|
||||
|
||||
- **(а)** дать `.btn-ghost` видимый бордер/фон — одна строка, чинит все 3 места.
|
||||
- **(б)** выкинуть `.btn-ghost` и назначить семантику явно: Откатить = `btn-danger`
|
||||
везде, Отменить = `btn-danger` везде, Обновить имя = нейтральный `.btn`.
|
||||
- **(в)** решить судьбу `↩` — оставляем или убираем.
|
||||
|
||||
Рекомендация — б+в: (а) чинит видимость, но оставляет пункты 2-4 (одно действие
|
||||
имеет два разных визуальных веса на разных экранах).
|
||||
|
||||
**Подводный камень:** `.btn-ghost` портирован из макета (`tmp/design/index.html:109`
|
||||
— тоже `btn btn-sm btn-ghost` для «Откатить», а `tmp/design/download.html:43` —
|
||||
`btn btn-danger`). Макет сам непоследователен → либо чинить и `tmp/design/*`, либо
|
||||
явно зафиксировать, что макет больше не источник истины (см. `tmp/design/handoff.md:96-97`).
|
||||
|
||||
## Спеки
|
||||
|
||||
`openspec/specs/web-ui/spec.md:249-264` («Действия соответствуют состоянию»)
|
||||
описывает, какие действия показывать и имена полей форм; требований к визуальной
|
||||
стилизации нет. «Откатить» упоминается только в htmx-сценариях (`:435`, `:444`).
|
||||
→ чистый nit-фикс либо новая дельта, если хотим зафиксировать семантику весов.
|
||||
Reference in New Issue
Block a user