Files
jellybit/openspec/specs/web-ui/spec.md
T
avandClaude Opus 4.8 ef75a0d302 Живые обновления прогресса и раздел «Раздача» (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>
2026-06-30 20:38:25 +03:00

166 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# web-ui Specification
## Purpose
Презентационный слой веб-интерфейса: встроенная (`go:embed`) отдача статики и
self-hosted шрифтов, единая дизайн-система (`jellybit.css`, тёмная тема по
настройке ОС), рендеринг страниц (список загрузок, ревью, просмотр) с бейджами
состояний и клиентскими взаимодействиями без сборки (копирование infohash,
спойлер контекста). Превью раскладки берётся из единой логики `internal/layout`,
а не дублируется в шаблонах. Тонкий транспорт над ядром (приём — `ingest`,
команды — `worker`, чтение — `store`).
## Requirements
### Requirement: Встроенная отдача статики
Сервис SHALL отдавать статические ассеты (CSS, клиентский JS, вендорные
библиотеки, шрифты) из встроенной (`go:embed`) файловой системы под префиксом
`/static/`, без внешних зависимостей времени выполнения. Бинарь остаётся
единым и самодостаточным.
#### Scenario: Запрос встроенного ассета
- **WHEN** клиент запрашивает `GET /static/css/jellybit.css`
- **THEN** сервис отвечает `200` с телом из встроенной FS и корректным
`Content-Type`
#### Scenario: Кэширование статики
- **WHEN** сервис отдаёт ответ на запрос под `/static/`
- **THEN** ответ содержит заголовок `Cache-Control`, разрешающий кэширование
ассета браузером
#### Scenario: Несуществующий ассет
- **WHEN** клиент запрашивает несуществующий путь под `/static/`
- **THEN** сервис отвечает `404` и не раскрывает структуру файловой системы
### Requirement: Шрифты без внешних сетей
Веб-UI SHALL подключать шрифты (IBM Plex Sans, IBM Plex Mono) только из
локально встроенных файлов через `@font-face`. Страницы MUST NOT обращаться к
сторонним хостам (Google Fonts CDN и т. п.) для загрузки шрифтов или стилей.
#### Scenario: Нет внешних ссылок на шрифты
- **WHEN** браузер открывает любую страницу веб-UI
- **THEN** все используемые шрифты загружаются из-под `/static/` того же
origin, без обращений к внешним доменам
### Requirement: Единая дизайн-система
Все страницы веб-UI SHALL использовать единственный CSS-файл `jellybit.css` с
дизайн-токенами; страницы MUST NOT содержать инлайн-`<style>` или хардкод
цветов вне токенов. Тема (светлая/тёмная) SHALL определяться настройкой ОС
(`color-scheme` + `prefers-color-scheme`), без отдельного переключателя.
#### Scenario: Тёмная тема по настройке ОС
- **WHEN** в ОS пользователя выбрана тёмная тема и открывается страница
- **THEN** интерфейс отображается в тёмной палитре из токенов, без действий
пользователя
#### Scenario: Нет инлайн-стилей
- **WHEN** рендерится любая страница веб-UI
- **THEN** оформление берётся из подключённого `jellybit.css`, в разметке нет
блоков `<style>` с хардкодом цветов
### Requirement: Бейдж статуса загрузки
Веб-UI SHALL отображать состояние каждой загрузки бейджем
`<span class="badge st-{STATE}">` с человекочитаемой русской подписью; класс
определяет цвет группы. Маппинг SHALL покрывать все состояния домена
(`downloading`, `completed`, `recognizing`, `linking`, `review`, `deferred`,
`done`, `stuck`, `target_missing`, `failed`, `orphaned`, `cancelled`,
`reverted`, `deleted`).
#### Scenario: Состояние отрисовано бейджем
- **WHEN** загрузка находится в состоянии `review`
- **THEN** в списке у неё бейдж с классом `st-review` и подписью «на ревью»
#### Scenario: Полнота покрытия
- **WHEN** загрузка находится в любом из состояний домена
- **THEN** для него существует класс бейджа и русская подпись (неизвестных
состояний без оформления нет)
### 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** они не отображаются, но доступны при включённом переключателе
### Requirement: Действия соответствуют состоянию
Каждая страница SHALL показывать только действия, допустимые в текущем
состоянии загрузки, и каждое действие-кнопка SHALL отправлять форму с полями,
имена которых совпадают с ожидаемыми обработчиком (`internal/httpapi`).
#### Scenario: Набор действий по состоянию
- **WHEN** загрузка в состоянии `done`
- **THEN** доступно действие отката (undo), но недоступны «применить»/«позже»
#### Scenario: Имена полей формы
- **WHEN** пользователь отправляет форму действия (например, «уточнить»)
- **THEN** поля формы имеют `name`, которые обработчик читает без
переименования, и действие исполняется
### Requirement: Превью раскладки через единую логику именования
Превью целевых путей раскладки в веб-UI SHALL вычисляться той же логикой
именования, что и реальная раскладка (`internal/naming`), а не дублировать
правила в шаблоне. Показанные пути MUST совпадать с теми, что создались бы при
применении.
#### Scenario: Превью совпадает с реальной раскладкой
- **WHEN** на экране ревью отображается превью целевых путей для текущей
догадки
- **THEN** эти пути идентичны тем, что создаст применение (те же правила имён,
спецвыпусков, мультифайла, запрещённых символов и коллизий)
### Requirement: Клиентские взаимодействия без сборки
Веб-UI SHALL реализовывать клиентскую логику без шага сборки и без реактивных
фреймворков: копирование infohash (vanilla JS) и раскрытие контекста нативным
`<details>`. Все действия над загрузкой SHALL выполняться через формы/htmx
(раундтрип на сервер), без клиентского пересчёта доменного состояния.
#### Scenario: Копирование infohash
- **WHEN** пользователь нажимает кнопку копирования рядом с infohash
- **THEN** значение infohash копируется в буфер обмена без перезагрузки
страницы
#### Scenario: Раскрытие контекста без JS
- **WHEN** пользователь раскрывает спойлер переданного контекста
- **THEN** контекст показывается нативным `<details>`, без скриптов