Живые обновления прогресса и раздел «Раздача» (live-status)
Воркер ведёт in-memory снимок телеметрии раздач (прогресс, скорость, ETA, рейтинг, сиды/пиры, отдано) под отдельным RWMutex, обновляя его на каждом тике поллинга сразу после построения byHash — без лишних вызовов qBittorrent и без хранения в БД (волатильно). qbt.Torrent дополнен полями телеметрии. Веб-UI читает снимок через узкий контракт LiveStatus: карточки активных загрузок показывают живой прогресс-бар (htmx-поллинг фрагмента every 3s, точечно — без сброса фильтров), на странице загрузки появилась секция «Раздача» для сидирующих задач. Начальный кадр рендерится сразу со значениями; при отсутствии данных UI деградирует штатно. Капабилити live-status (OpenSpec), web-ui дополнен. Change заархивирован. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-06-30
|
||||
@@ -0,0 +1,196 @@
|
||||
## Context
|
||||
|
||||
Воркер (`internal/worker`) уже поллит qBittorrent каждые `PollInterval` (дефолт
|
||||
5с): `Poll` тянет `Torrents(ctx, "")` (все торренты) и строит `byHash`
|
||||
(infohash → `qbt.Torrent`) для reconcile/discovery. Это единственная точка в
|
||||
системе, где есть свежее состояние раздач. Прогресс в БД не хранится (в
|
||||
`store.Download` полей нет — и не нужно, значение волатильно).
|
||||
|
||||
Веб-UI (`internal/httpapi`) server-rendered: `index.html` рендерит карточки из
|
||||
`[]downloadView` (есть `Infohash`), `download.html` — детальную страницу из
|
||||
`downloadDetailView`. htmx уже вендорится и грузится во всех шаблонах, но ни
|
||||
одного `hx-*` пока нет. Зависимости транспорта собираются в `cmd/jellybit/serve.go`
|
||||
(`httpapi.Deps`), воркер `wrk` уже передаётся как `Commander`/`Reviewer`.
|
||||
|
||||
Сейчас прогресс виден только при ручной перезагрузке, статистики раздачи нет.
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
|
||||
- Живой прогресс активных загрузок на главной без перезагрузки страницы и без
|
||||
сброса клиентских фильтров/поиска.
|
||||
- Секция «Раздача» на странице загрузки (рейтинг, сиды/пиры, отдано, скорость
|
||||
отдачи).
|
||||
- Снимок телеметрии в памяти воркера, обновляемый на том же тике поллинга, без
|
||||
лишних сетевых вызовов и без записи в БД.
|
||||
- Контракт чтения снимка изолирован так, чтобы позже заменить поллинг на SSE
|
||||
без переделки доменного слоя.
|
||||
|
||||
**Non-Goals:**
|
||||
|
||||
- SSE/WebSocket в этой фазе (только htmx-поллинг фрагментов).
|
||||
- Хранение истории прогресса/скорости в БД, графики.
|
||||
- Управление раздачей (пауза/лимиты/удаление торрента) — источник
|
||||
неприкосновенен.
|
||||
- Живое обновление страницы ревью.
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. Снимок в воркере, ключ — infohash
|
||||
|
||||
Воркер ведёт `map[string]Live` (ключ — lowercase infohash), обновляемый
|
||||
целиком (swap готовой карты) под **отдельным** `sync.RWMutex` — не `w.mu`.
|
||||
Причина: `w.mu` сериализует переходы состояний и команды (cancel/retry), а UI
|
||||
читает телеметрию часто (каждые несколько секунд × число вкладок); смешивать
|
||||
частые чтения с замком переходов — лишняя конкуренция. Снимок строится из уже
|
||||
полученного `torrents` — дополнительного вызова qBittorrent нет.
|
||||
|
||||
**Три ключа на торрент.** Как и существующий `byHash` (`worker.go:237-244`),
|
||||
снимок кладёт каждую раздачу под все три значения: `t.Hash`, `t.InfohashV1`,
|
||||
`t.InfohashV2` (lowercase). Иначе v2-торрент, у которого `d.Infohash` =
|
||||
infohash_v1, а в карту положили только по `Hash`, не найдётся при чтении.
|
||||
|
||||
**Момент свопа — сразу после построения карты, до store-операций.** `Poll`
|
||||
после получения `torrents` делает ещё несколько шагов с ранними `return` по
|
||||
ошибкам store (`ListDownloadsByState` и т.п.). Снимок зависит только от
|
||||
`torrents`, поэтому собираем и свопаем его сразу после построения `byHash` —
|
||||
тогда телеметрия обновится даже если последующий reconcile упадёт.
|
||||
|
||||
Ключ по infohash, а не по `download_id`: `Poll` уже держит торренты по
|
||||
infohash, а каждая `downloadView`/`downloadDetailView` несёт `Infohash`. Это
|
||||
избавляет воркер от загрузки всех записей `store.Download` ради маппинга id и
|
||||
естественно покрывает и активные, и сидирующие задачи (снимок = все торренты
|
||||
последнего тика). Дедуп активных по infohash уже гарантирован воркером.
|
||||
|
||||
_Альтернатива (ключ download_id, как в исходной заметке):_ потребовал бы в
|
||||
снимке резолвить torrent→download_id, т.е. держать обратный маппинг и грузить
|
||||
записи. Отверг — infohash проще и уже под рукой на обеих сторонах.
|
||||
|
||||
### 2. Тип `worker.Live` — курированный, не `qbt.Torrent`
|
||||
|
||||
В снимок кладём отдельный `worker.Live{Progress, DlSpeed, ETA, State, Seeding,
|
||||
Ratio, Seeds, Peers, Uploaded, UpSpeed}`, а не сырой `qbt.Torrent`. Так
|
||||
`httpapi` не зависит от пакета qbt и контракт чтения остаётся узким (легче
|
||||
заменить источник). `qbt.Torrent` дополняется полями `Dlspeed`, `Eta`,
|
||||
`Ratio`, `NumSeeds`, `NumLeechs`, `Uploaded`, `Upspeed` (теги `json:"dlspeed"`
|
||||
и т.д.) — они приходят в том же ответе `/torrents/info`.
|
||||
|
||||
**Флаг «сидирует» вычисляет воркер.** «Сидирует» — это свойство qbt-состояния
|
||||
(`uploading`/`stalledUP`/…), а не доменного состояния задачи в БД. Воркер уже
|
||||
владеет `classify` (`worker.go:466-479`), поэтому при сборке снимка он считает
|
||||
`Seeding bool` (= `classify(state) == classReady`) и кладёт в `Live`. Так
|
||||
httpapi не дублирует перечень qbt-состояний и трактовка завершённости живёт в
|
||||
одном месте.
|
||||
|
||||
### 3. Контракт чтения: dep-интерфейс `LiveStatus` в httpapi
|
||||
|
||||
```go
|
||||
// в internal/httpapi
|
||||
type LiveStatus interface {
|
||||
Live(infohash string) (worker.Live, bool) // ok=false → нет данных
|
||||
}
|
||||
```
|
||||
|
||||
Воркер реализует `Live(infohash string) (Live, bool)` чтением снимка под
|
||||
RLock. В `httpapi.Deps` добавляется поле `Live LiveStatus`; в `serve.go`
|
||||
передаётся тот же `wrk`. `bool` — явный признак отсутствия (graceful
|
||||
degradation). Геттер по одному infohash достаточен: и список (по карточке), и
|
||||
страница загрузки читают по одной задаче.
|
||||
|
||||
### 4. Доставка в браузер: per-card htmx-поллинг, swap фрагмента карточки
|
||||
|
||||
На главной каждая карточка **активной** загрузки содержит элемент с
|
||||
`hx-get="/fragments/downloads/{id}/progress"`, `hx-trigger="every Ns"`,
|
||||
`hx-swap="outerHTML"`. Поллит не весь список, а свой прогресс-блок — поэтому
|
||||
клиентские фильтры/поиск/прокрутка (на уровне `#list`) не затрагиваются.
|
||||
|
||||
Когда задача покидает `downloading`, фрагмент возвращается **без** htmx-атрибутов
|
||||
поллинга (или с `hx-trigger` снятым) — поллинг сам собой прекращается. Смена
|
||||
набора действий/бейджа при завершении остаётся за обычной навигацией/ручным
|
||||
обновлением (живой пересбор всей карточки с действиями — вне scope, чтобы не
|
||||
дублировать в htmx-ветке логику доступных действий). На странице загрузки
|
||||
секция «Раздача» поллится аналогично (`/fragments/downloads/{id}/seeding`),
|
||||
пока задача сидирует.
|
||||
|
||||
**Интервал поллинга UI — фиксированный 3s** (статичный `hx-trigger="every 3s"`
|
||||
в шаблоне). Решение: не прокидывать `PollInterval` в шаблоны — для однопользова-
|
||||
тельского домашнего сервиса лишние идентичные ответы при `PollInterval` > 3s
|
||||
ничего не стоят, а плавность и простота важнее. Браузер читает только снимок —
|
||||
qBittorrent при этом не дёргается (Decision 1), так что «лишние» запросы не
|
||||
доходят до qBittorrent. Требование спеки сформулировано соответственно (ключевой
|
||||
инвариант — браузер не опрашивает qBittorrent напрямую и не видит данные свежее
|
||||
тика, а не «строго не чаще тика»).
|
||||
|
||||
_Альтернатива (интервал из `PollInterval`):_ буквально «не чаще, чем меняются
|
||||
данные», но требует проводки cfg → Deps → шаблон ради экономии, которой здесь
|
||||
нет. Отверг. _Альтернатива (swap всего `#list`):_ сбрасывал бы клиентские
|
||||
фильтры (они на JS через display) — отверг. _SSE:_ отложено, контракт
|
||||
`LiveStatus` оставляет путь.
|
||||
|
||||
### 5. Фрагмент-роуты и шаблоны-партиалы
|
||||
|
||||
Новые роуты в `NewRouter`: `GET /fragments/downloads/{id}/progress` и
|
||||
`GET /fragments/downloads/{id}/seeding`. Оба отдают HTML-партиал (не JSON):
|
||||
читают `GetDownload` (для infohash/состояния) + `Live(infohash)` и рендерят
|
||||
партиал `partials/progress.html` / `partials/seeding.html`.
|
||||
|
||||
**Начальный кадр — сразу со значениями (без мигания).** Те же партиалы
|
||||
включаются при первом полном рендере карточки/страницы, и значения для них
|
||||
готовятся там же: `handleIndex`/`handleDownload` для нужных задач тоже зовут
|
||||
`s.deps.Live.Live(infohash)` и кладут телеметрию в view-модель. Для этого
|
||||
`downloadView` (карточка) и `downloadDetailView` (страница) расширяются полями
|
||||
телеметрии (та же модель, что отдаёт фрагмент-роут) — один источник разметки и
|
||||
данных для начального рендера и для поллинга. При `ok=false` партиал рендерит
|
||||
нейтральный плейсхолдер/скрывает секцию (graceful degradation), а не падает.
|
||||
|
||||
Уровень лога для фрагментов — DEBUG (навигационный GET, как прочие страницы;
|
||||
`requestLogLevel` уже понижает не-`/api` GET — отдельной правки логов не нужно).
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- **[Карточка и фрагмент рассинхронятся по доступным действиям]** при
|
||||
завершении загрузки фоновым поллингом обновится только прогресс-блок, а
|
||||
кнопки — нет → Mitigation: фрагмент при выходе из `downloading` гасит свой
|
||||
поллинг и показывает финальное состояние прогресс-блока; полный актуальный
|
||||
набор действий пользователь видит при следующем заходе/обновлении. Это
|
||||
осознанный компромисс ради простоты (не тянем логику действий в htmx-ветку).
|
||||
- **[Снимок устаревает на рестарте]** до первого тика снимок пуст →
|
||||
Mitigation: `Live` возвращает `ok=false`, UI рендерит без живых значений
|
||||
(спека требует graceful degradation).
|
||||
- **[Рост карты снимка]** свопаем карту целиком на каждом тике из актуального
|
||||
списка торрентов → исчезнувшие раздачи естественно выпадают, утечки нет.
|
||||
- **[Гонка чтения/записи снимка]** → отдельный `RWMutex`, запись — atomic swap
|
||||
готовой карты в конце `Poll`, чтения под RLock.
|
||||
- **[Пустой infohash]** у задачи (теоретически) → `Live("")` возвращает
|
||||
`ok=false`, без паник.
|
||||
- **[Sentinel-значения qBittorrent]** `eta=8640000` означает «∞/неизвестно»,
|
||||
`ratio` может быть `-1`, скорости/размеры — в байтах → Mitigation: хелперы
|
||||
форматирования (httpapi) трактуют sentinel'ы явно (ETA → «—»/«∞», `ratio<0` →
|
||||
«—») и переводят байты в человекочитаемые единицы. Берём `num_seeds`/
|
||||
`num_leechs` (подключённые пиры), не `num_complete`/`num_incomplete` (рой) —
|
||||
выбор фиксируем в хелпере/партиале.
|
||||
|
||||
## Migration Plan
|
||||
|
||||
Изменение аддитивное: новые поля `qbt.Torrent` (обратносовместимо), новый
|
||||
снимок и геттер в воркере, новый dep + роуты в httpapi, htmx-атрибуты в
|
||||
шаблонах. БД не меняется — миграций нет. Откат — обратный revert коммита;
|
||||
рантайм-состояние волатильно, чистить нечего. Деплой — обычный (бинарь на
|
||||
umbar).
|
||||
|
||||
## Open Questions
|
||||
|
||||
Разрешены на чекпоинте ревью дизайна:
|
||||
|
||||
- **Начальный кадр** — рендерим сразу со значениями (Decision 5): `handleIndex`/
|
||||
`handleDownload` читают `Live` и кладут телеметрию во view.
|
||||
- **«Сидирует»** — вычисляемый флаг `Seeding` в `worker.Live` на базе
|
||||
`classify` (Decision 2), без дублирования состояний в httpapi.
|
||||
- **Интервал поллинга** — фиксированный `every 3s` в шаблоне (Decision 4),
|
||||
`PollInterval` в шаблоны не прокидывается; формулировка спеки смягчена.
|
||||
|
||||
Остаётся уточнить при apply (мелочь, не блокер):
|
||||
|
||||
- Конкретные единицы/стиль отображения скоростей и размеров — согласовать с
|
||||
дизайн-системой (`jellybit.css`).
|
||||
@@ -0,0 +1,57 @@
|
||||
## Why
|
||||
|
||||
Сейчас прогресс скачивания в веб-UI виден только при ручной перезагрузке
|
||||
страницы, а статистики раздачи (рейтинг, сиды/пиры, отдано) нет вовсе — в
|
||||
фазе 1 секцию намеренно отложили до появления живых данных. Воркер уже
|
||||
опрашивает qBittorrent каждые несколько секунд и держит свежее состояние
|
||||
каждой задачи — нужно лишь сохранить эту телеметрию в памяти и показать её в
|
||||
UI без перезагрузки. Это «однооконный» сервис: смотреть, как идёт загрузка,
|
||||
должно быть видно вживую.
|
||||
|
||||
## What Changes
|
||||
|
||||
- Воркер на каждом тике поллинга сохраняет **in-memory снимок** телеметрии
|
||||
всех известных раздач (прогресс, скорость загрузки, ETA + для раздачи:
|
||||
рейтинг, число сидов/пиров, отдано, скорость отдачи). Снимок волатильный,
|
||||
в БД не пишется.
|
||||
- `qbt.Torrent` дополняется полями телеметрии (`dlspeed`, `eta`, `ratio`,
|
||||
`num_seeds`, `num_leechs`, `uploaded`, `upspeed`) из `/torrents/info` —
|
||||
лишний сетевой вызов не добавляется, они приходят в том же ответе.
|
||||
- `httpapi` получает новую зависимость-источник телеметрии и **фрагмент-роут**;
|
||||
активные карточки на главной поллят свой прогресс через htmx и обновляют
|
||||
прогресс-бар/скорость/ETA на месте, без перезагрузки страницы и без сброса
|
||||
клиентских фильтров.
|
||||
- На странице загрузки `/download/{id}` появляется секция **«Раздача»** с
|
||||
живой статистикой (рейтинг, сиды/пиры, отдано, скорость отдачи) для задач,
|
||||
чей торрент сидирует.
|
||||
- htmx, который уже вендорится и грузится, впервые задействуется (`hx-*`).
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `live-status`: живая телеметрия загрузок и раздач — воркер как единственный
|
||||
сэмплер qBittorrent ведёт in-memory снимок прогресса/скорости/ETA и
|
||||
статистики раздачи; веб-UI отображает её в реальном времени (поллинг
|
||||
фрагментов) без перезагрузки страницы и без хранения в БД.
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `web-ui`: страница загрузки `/download/{id}` получает секцию живой
|
||||
статистики раздачи (в фазе 1 явно вынесена из scope «вводится вместе с
|
||||
живыми обновлениями»); карточки активных загрузок на главной показывают
|
||||
живой прогресс-бар через htmx-поллинг.
|
||||
|
||||
## Impact
|
||||
|
||||
- **Код:** `internal/qbt` (поля `Torrent`), `internal/worker` (снимок +
|
||||
геттер телеметрии под отдельным RWMutex), `internal/httpapi` (dep-интерфейс
|
||||
`LiveStatus`, фрагмент-роут, секция раздачи, прогресс во view), `cmd/jellybit`
|
||||
(инъекция снимка в `httpapi.Deps`), `web/templates` (прогресс-бар на
|
||||
карточках + htmx-атрибуты, секция «Раздача»), `web/static/css` при
|
||||
необходимости (стили прогресс-бара уже есть в дизайн-системе).
|
||||
- **БД:** изменений нет — телеметрия волатильна, миграций/ER не требуется.
|
||||
- **Внешние вызовы:** дополнительных нет (расширяется только разбор
|
||||
существующего ответа `/torrents/info`).
|
||||
- **Безопасность:** телеметрия не содержит секретов; контракт чтения снимка
|
||||
изолируется так, чтобы будущий апгрейд доставки до SSE не ломал API.
|
||||
@@ -0,0 +1,111 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: Снимок живой телеметрии
|
||||
|
||||
Воркер — единственный сэмплер qBittorrent — SHALL на каждом тике поллинга
|
||||
обновлять in-memory снимок телеметрии всех известных раздач. Снимок MUST NOT
|
||||
персиститься в БД: он волатилен и переживает только до рестарта процесса.
|
||||
|
||||
#### Scenario: Обновление снимка на тике
|
||||
|
||||
- **WHEN** воркер завершает успешный тик поллинга qBittorrent
|
||||
- **THEN** снимок телеметрии содержит актуальные данные по каждой раздаче,
|
||||
присутствующей в ответе qBittorrent
|
||||
|
||||
#### Scenario: Снимок волатилен
|
||||
|
||||
- **WHEN** процесс только что перезапущен и первый тик поллинга ещё не прошёл
|
||||
- **THEN** снимок пуст, а UI отображает задачи без живых значений, не падая
|
||||
|
||||
### Requirement: Состав телеметрии
|
||||
|
||||
Телеметрия одной раздачи SHALL включать прогресс (доля 0..1), скорость
|
||||
загрузки и ETA, а для сидирующих раздач дополнительно — рейтинг, число сидов
|
||||
и пиров, объём отданного и скорость отдачи. Значения SHALL извлекаться из
|
||||
ответа qBittorrent `/torrents/info` без дополнительного сетевого вызова.
|
||||
|
||||
#### Scenario: Телеметрия качающейся задачи
|
||||
|
||||
- **WHEN** торрент задачи находится в состоянии загрузки
|
||||
- **THEN** в снимке для неё доступны прогресс, скорость загрузки и ETA
|
||||
|
||||
#### Scenario: Телеметрия сидирующей задачи
|
||||
|
||||
- **WHEN** торрент задачи завершён и раздаётся
|
||||
- **THEN** в снимке для неё доступны рейтинг, число сидов/пиров, объём
|
||||
отданного и скорость отдачи
|
||||
|
||||
### Requirement: Чтение телеметрии транспортом
|
||||
|
||||
Сервис SHALL предоставлять чтение снимка телеметрии по задаче через
|
||||
изолированный контракт, не зависящий от способа доставки в браузер (поллинг
|
||||
сейчас, SSE в будущем). Если для задачи нет записи в снимке (соответствующий
|
||||
торрент отсутствовал в qBittorrent на последнем тике), чтение SHALL сообщать
|
||||
об отсутствии данных, а UI MUST деградировать без живых значений, не падая.
|
||||
|
||||
#### Scenario: Данные есть
|
||||
|
||||
- **WHEN** транспорт читает телеметрию задачи, чей торрент был в последнем тике
|
||||
- **THEN** он получает живые значения этой задачи
|
||||
|
||||
#### Scenario: Данных нет
|
||||
|
||||
- **WHEN** транспорт читает телеметрию задачи, торрента которой нет в qBittorrent
|
||||
- **THEN** он получает признак отсутствия данных и рендерит страницу без живых
|
||||
значений
|
||||
|
||||
### Requirement: Свежесть не выше тика поллинга
|
||||
|
||||
Живые значения, видимые в браузере, SHALL быть не свежее последнего тика
|
||||
поллинга воркера; браузер MUST NOT опрашивать qBittorrent напрямую. Любой
|
||||
запрос UI за телеметрией SHALL обслуживаться из in-memory снимка, не порождая
|
||||
обращения к qBittorrent — поэтому частота обновления UI может быть выбрана
|
||||
свободно (в т.ч. чаще тика для плавности), не нагружая qBittorrent.
|
||||
|
||||
#### Scenario: Браузер не обгоняет воркер
|
||||
|
||||
- **WHEN** браузер запрашивает фрагмент телеметрии чаще, чем длится тик
|
||||
поллинга
|
||||
- **THEN** он получает значения последнего тика, и обращения к qBittorrent при
|
||||
этом не происходит
|
||||
|
||||
### Requirement: Живой прогресс активных загрузок
|
||||
|
||||
Веб-UI SHALL обновлять прогресс активных (downloading) загрузок на главной без
|
||||
перезагрузки страницы — поллингом фрагмента через htmx. Обновление MUST NOT
|
||||
сбрасывать клиентские фильтр, поиск и прокрутку. Когда задача покидает
|
||||
состояние downloading, поллинг её прогресса SHALL прекращаться.
|
||||
|
||||
#### Scenario: Прогресс растёт без перезагрузки
|
||||
|
||||
- **WHEN** загрузка качается и пользователь смотрит на главную
|
||||
- **THEN** её прогресс-бар, скорость и ETA обновляются на месте без
|
||||
перезагрузки страницы
|
||||
|
||||
#### Scenario: Клиентское состояние сохраняется
|
||||
|
||||
- **WHEN** применён фильтр или поиск и происходит фоновое обновление прогресса
|
||||
- **THEN** выбранный фильтр, текст поиска и позиция прокрутки не сбрасываются
|
||||
|
||||
#### Scenario: Завершение останавливает поллинг
|
||||
|
||||
- **WHEN** загрузка переходит из downloading в другое состояние
|
||||
- **THEN** фоновый поллинг прогресса для этой карточки прекращается
|
||||
|
||||
### Requirement: Секция раздачи на странице загрузки
|
||||
|
||||
Страница `/download/{id}` SHALL показывать секцию «Раздача» с живой статистикой
|
||||
(рейтинг, число сидов и пиров, объём отданного, скорость отдачи) для задач,
|
||||
чей торрент сидирует. Если живых данных по задаче нет, секция SHALL
|
||||
отсутствовать либо явно показывать «нет данных», не ломая остальную страницу.
|
||||
|
||||
#### Scenario: Сидирующая задача показывает раздачу
|
||||
|
||||
- **WHEN** открыта страница задачи, торрент которой раздаётся
|
||||
- **THEN** в секции «Раздача» видны рейтинг, сиды/пиры, отдано и скорость отдачи
|
||||
|
||||
#### Scenario: Нет живых данных — секция деградирует
|
||||
|
||||
- **WHEN** открыта страница задачи, торрента которой нет в qBittorrent
|
||||
- **THEN** секция «Раздача» отсутствует или показывает «нет данных», а
|
||||
распознавание, файлы и история отображаются нормально
|
||||
@@ -0,0 +1,30 @@
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: Страницы веб-UI
|
||||
|
||||
Веб-UI SHALL предоставлять страницы: список загрузок с единым окном
|
||||
добавления и фильтром/поиском (`/`), экран ревью одной загрузки (`/review/{id}`)
|
||||
и страницу просмотра одной загрузки (`/download/{id}`) с распознаванием,
|
||||
файлами→раскладкой, историей и — для сидирующих задач — секцией живой
|
||||
статистики раздачи. Карточки активных (downloading) загрузок в списке SHALL
|
||||
содержать индикатор прогресса. Состояние `deleted` SHALL быть скрыто в списке
|
||||
по умолчанию (с переключателем «показать всё»). Механика живого обновления
|
||||
прогресса и наполнение секции раздачи определяются capability `live-status`.
|
||||
|
||||
#### Scenario: Просмотр одной загрузки
|
||||
|
||||
- **WHEN** клиент открывает `GET /download/{id}` существующей загрузки
|
||||
- **THEN** отрисовывается страница с её распознаванием, файлами, раскладкой и
|
||||
историей
|
||||
|
||||
#### Scenario: Прогресс активной загрузки в списке
|
||||
|
||||
- **WHEN** в списке есть загрузка в состоянии `downloading`
|
||||
- **THEN** её карточка содержит индикатор прогресса (прогресс-бар со скоростью
|
||||
и ETA)
|
||||
|
||||
#### Scenario: Удалённые скрыты по умолчанию
|
||||
|
||||
- **WHEN** в списке есть загрузки в состоянии `deleted` и фильтр «показать
|
||||
всё» не включён
|
||||
- **THEN** они не отображаются, но доступны при включённом переключателе
|
||||
@@ -0,0 +1,64 @@
|
||||
## 1. Телеметрия в клиенте qBittorrent
|
||||
|
||||
- [x] 1.1 Добавить в `qbt.Torrent` поля `Dlspeed`, `Eta`, `Ratio`, `NumSeeds`,
|
||||
`NumLeechs`, `Uploaded`, `Upspeed` с json-тегами из `/torrents/info`
|
||||
(`dlspeed`, `eta`, `ratio`, `num_seeds`, `num_leechs`, `uploaded`, `upspeed`)
|
||||
- [x] 1.2 Обновить комментарий-доку `Torrent` (подмножество полей расширено)
|
||||
|
||||
## 2. Снимок телеметрии в воркере
|
||||
|
||||
- [x] 2.1 Добавить тип `worker.Live{Progress, DlSpeed, ETA, State, Seeding,
|
||||
Ratio, Seeds, Peers, Uploaded, UpSpeed}` (курированный, без зависимости на
|
||||
qbt у читателей); `Seeding` вычислять через `classify(state) == classReady`
|
||||
- [x] 2.2 Добавить в `Worker` поле снимка `live map[string]Live` + отдельный
|
||||
`sync.RWMutex` (не `w.mu`); инициализация в `New`
|
||||
- [x] 2.3 В `Poll` собрать новую карту из полученного `torrents` — **три ключа
|
||||
на торрент** (lowercase `Hash`/`InfohashV1`/`InfohashV2`, как `byHash`) — и
|
||||
атомарно подменить снимок **сразу после построения `byHash`, до store-
|
||||
операций** (телеметрия обновляется даже при последующем сбое reconcile)
|
||||
- [x] 2.4 Реализовать `Live(infohash string) (Live, bool)` — чтение под RLock,
|
||||
`ok=false` при пустом/неизвестном infohash
|
||||
- [x] 2.5 Тесты: снимок обновляется после `Poll`; `Live` отдаёт данные
|
||||
качающейся и сидирующей задачи (с верным `Seeding`); поиск по любому из трёх
|
||||
хэшей; неизвестный/пустой infohash → `ok=false`
|
||||
|
||||
## 3. Транспорт: контракт и фрагмент-роуты
|
||||
|
||||
- [x] 3.1 Объявить интерфейс `LiveStatus` в `internal/httpapi` и добавить поле
|
||||
`Live LiveStatus` в `Deps`
|
||||
- [x] 3.2 Прокинуть `Live: wrk` в `httpapi.Deps` в `cmd/jellybit/serve.go`
|
||||
- [x] 3.3 Добавить роуты `GET /fragments/downloads/{id}/progress` и
|
||||
`GET /fragments/downloads/{id}/seeding` в `NewRouter`
|
||||
- [x] 3.4 Реализовать обработчики фрагментов: `GetDownload` → infohash/состояние
|
||||
+ `Live(infohash)`; рендер партиала; `ok=false` → деградация без живых значений
|
||||
- [x] 3.5 **Начальный рендер со значениями:** расширить `downloadView` и
|
||||
`downloadDetailView` полями телеметрии; в `handleIndex`/`handleDownload`
|
||||
вызвать `Live(infohash)` для нужных задач и положить во view — одна модель для
|
||||
страницы и для фрагмент-роута (без мигания при первой загрузке)
|
||||
- [x] 3.6 Добавить хелперы форматирования скорости/размера/ETA: человекочитаемые
|
||||
единицы (байты→КиБ/МиБ, скорость/с), sentinel'ы qBittorrent (`eta=8640000`→
|
||||
«—»/«∞», `ratio<0`→«—»); зафиксировать выбор `num_seeds`/`num_leechs`
|
||||
|
||||
## 4. Шаблоны и живое обновление
|
||||
|
||||
- [x] 4.1 Партиал `partials/progress.html` (прогресс-бар + скорость + ETA);
|
||||
включить его в карточку `index.html` для активных загрузок
|
||||
- [x] 4.2 Навесить на прогресс-блок активной карточки `hx-get` /
|
||||
`hx-trigger="every 3s"` / `hx-swap="outerHTML"`; при выходе из downloading
|
||||
фрагмент не содержит атрибутов поллинга (поллинг прекращается)
|
||||
- [x] 4.3 Партиал `partials/seeding.html` (рейтинг, сиды/пиры, отдано, скорость
|
||||
отдачи); секция «Раздача» в `download.html` для сидирующих задач, с
|
||||
htmx-поллингом и деградацией при отсутствии данных
|
||||
- [x] 4.4 Проверить, что фоновое обновление не сбрасывает фильтр/поиск/прокрутку
|
||||
на главной (поллинг точечный, на уровне карточки)
|
||||
- [x] 4.5 При необходимости — стили прогресс-бара/раздачи в `jellybit.css`
|
||||
(использовать существующие токены, без инлайн-хардкода цветов)
|
||||
|
||||
## 5. Проверки и ревью
|
||||
|
||||
- [x] 5.1 `task test` (включая новые тесты воркера и httpapi-рендера фрагментов;
|
||||
в т.ч. тест «фрагмент при выходе из downloading отдаётся без htmx-атрибутов
|
||||
поллинга»)
|
||||
- [x] 5.2 `task lint`, `gofmt`, `openspec validate --strict`
|
||||
- [x] 5.3 Ревью кода (второй чекпоинт) перед archive: соответствие спекам и
|
||||
конвенциям (логирование без секретов, ошибки `%w`, отдельный RWMutex)
|
||||
Reference in New Issue
Block a user