Беклог: перенос четырёх задач из инбокса 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) — ИДЕЯ (проработать крайние случаи)
|
- [[идея] Сложные сериальные раздачи: все сезоны разом, паки, спецраскладки](slozhnye-serialnye-razdachi.md) — ИДЕЯ (проработать крайние случаи)
|
||||||
- [Аниме с абсолютной нумерацией](anime-absolyutnaya-numeraciya.md) — Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а Jellyfin ждёт…
|
- [Аниме с абсолютной нумерацией](anime-absolyutnaya-numeraciya.md) — Релизы аниме часто нумеруют серии сквозным числом (#137) без сезонов, а Jellyfin ждёт…
|
||||||
- [Бэкап SQLite](backup-sqlite.md) — architecture
|
- [Бэкап 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 проверяет только сам сервис
|
- [Глубокий healthcheck и статус зависимостей](healthcheck-zavisimosti.md) — /healthz проверяет только сам сервис
|
||||||
- [НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)](masshtab-100-zagruzok.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
|
- [НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)](masshtab-100-zagruzok.md) — Зафиксировать в НФТ ориентир 100/1000 загрузок + аудит узких мест (SQLite, воркер, поллинг)
|
||||||
- [Обучение на правках человека (few-shot из прошлых ревью)](obuchenie-na-pravkah.md) — Когда человек поправил матч, тип или нумерацию — сохранять это как пример и подмешивать…
|
- [Обучение на правках человека (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) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
|
- [Ревью уведомлений в Telegram (аудит текстов и формата)](telegram-revyu-uvedomleniy.md) — зонтичный проход по всем текстам бота: полнота карточек, единый язык, оформление; порождает под-задачи
|
||||||
- [Download id в Telegram моноширинным (code) для tap-to-copy](telegram-download-id-code.md) — слать id как `code`; требует включить parse mode (HTML) в send() + escape всех текстов
|
- [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) — просто и работает…
|
- [Мгновенные обновления через 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