Files
jellybit/docs/backlog.md
T

476 lines
37 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.
# Беклог
Единый список будущих задач по проекту: то, что уже решили сделать, и
идеи, которые ещё надо обдумать. Это не план реализации (он — в
[drafts/roadmap.md](drafts/roadmap.md)) и не источник истины: принятое и
реализованное переезжает в `docs/specs`/`docs/adr`.
Приоритет — грубая оценка «ценность / стоимость», не обязательство к
порядку. Спекулятивные пункты (ещё без решения «делаем») помечены
_(идея)_ — их сперва надо проработать.
## Высокий
### Проблема второго сезона
Если первый сезон сериала уже разложен, а мы добавляем второй/третий/…,
распознавание должно привязать новый сезон к **тому же** названию и папке,
а не завести рядом почти одинаковую вторую папку. Ключ — стабильный
`provider_id`: один и тот же `[tvdbid-…]` → одна папка сериала, новые
`Season NN` доливаются внутрь. Нужно: при матче учитывать уже существующие
в библиотеке сериалы (или прошлые распознавания с тем же провайдер-id) и
склонять LLM/выбор кандидата к согласованности с ними.
Связано: [recognition.md](specs/recognition.md) (модель уверенности,
матч в базе), [jellyfin-layout.md](specs/jellyfin-layout.md) (папка
сериала с провайдер-id).
### Удаление средствами jellybit («единое окно», path 2)
Распознавание **ручного** удаления (источник из qBittorrent / цель из
Jellyfin) и пометка рассинхрона уже сделаны: фоновая сверка по матрице
«источник × цель» → состояния `target_missing`/`orphaned`/`deleted`,
безопасный `undo` (не снимает последнюю копию, `nlink <= 1`), синхронный
preflight перед действиями. См. `openspec/specs/state-reconciliation/`,
[workflow.md](specs/workflow.md) → «Сверка с реальностью».
Осталось (path 2) — продолжение «единого окна»: удалять просмотренное
**из самого jellybit**, не идя руками в qBittorrent/Jellyfin. Нужно
продумать: команду удаления (снять наши хардлинки + опц. удалить раздачу из
qBittorrent с файлами), подтверждение осознанности (а не случайный клик) и
как это сочетается с инвариантом «источник неприкосновенен», когда
пользователь сам просит убрать источник.
Связано: [ADR-2026-06-13-hardlinks](adr/ADR-2026-06-13-hardlinks.md),
[architecture.md](specs/architecture.md) → «Раскладка файлов»,
[workflow.md](specs/workflow.md).
### Ревью: выбор источника совпадения и предпросмотр
Переработать страницу ревью так, чтобы показывать **все** совпавшие
результаты по метабазам списком и дать выбрать из них. Принцип: совпадение
есть **всегда** — мы лишь выбираем источник. Поэтому матч нейронки — это
отдельная строка в том же списке (наравне с кандидатами TMDB/TVDB), а не
особый режим.
Возможности экрана:
- список кандидатов из баз + строка «распознано нейронкой»;
- выбрать один кандидат, переключиться на другой, отменить матч с базой в
пользу нейронки;
- добавить кандидат вручную (по id/url базы), когда автопоиск промахнулся;
- при выборе/переключении — **предпросмотр полей** (название, режиссёр,
год) и **предпросмотр раскладки** (целевые пути) до применения.
Развивает «показывать матч с записью метабазы» (web-сторона), пересекается
с «Расширенной информацией о загрузке» (детальный экран) и быстрым выбором
в Telegram. Веб остаётся точкой точных правок.
Связано: [review-ux.md](specs/review-ux.md) (выбор кандидата, «без базы»,
переключатель типа), [recognition.md](specs/recognition.md) (кандидаты
матча, провайдер-id), [«Улучшения UI: показывать матч»](#улучшения-ui-показывать-матч-с-записью-метабазы),
пакет `httpapi`.
### Наблюдаемость: метрики и учёт стоимости LLM
Сейчас единственное окно в систему — `slog`. Нет быстрых ответов на
вопросы «сколько задач висит в review», «сколько токенов и денег съело
распознавание», «какова медиана времени ingest → done». Нужны метрики:
эндпоинт `/metrics` (Prometheus-формат) со счётчиками загрузок по
состояниям, длительностями стадий и расходом LLM (токены/стоимость на
задачу).
**Расход LLM уже снимается с провода**`llm.openai` парсит `usage`
(prompt/completion/total tokens и `cost`, который отдаёт шлюз) в
`llm.Response.Usage` и пишет в лог. Не хватает только **персистентности и
отображения**: сохранять `usage` у попытки распознавания (`recognition`) и
показывать в карточке загрузки + агрегатом в `/metrics`. Где провайдер не
шлёт `cost` — считать из токенов по таблице «модель → цена» в конфиге.
Отдельный LLM-прокси (LiteLLM и т.п.) для этого **не нужен** и противоречит
принципам «один бинарь» / «минимум компонентов»: подсчёт токенов уже в коде,
а роль мульти-модельного шлюза играет используемый OpenAI-совместимый
эндпоинт (он и возвращает `cost`); разные модели подключаются сменой
`[llm].model` или новым типом провайдера за интерфейсом `llm.Provider`.
Связано: [architecture.md](specs/architecture.md) → «Логирование»,
[recognition.md](specs/recognition.md) (провайдер LLM, `[llm].type`),
пакеты `worker`, `llm`, `httpapi`.
### Ретеншн и очистка БД
Терминальные задачи (`done`/`cancelled`/`failed`/`reverted`), их попытки
`recognition` с сырыми ответами LLM и `metadata_candidate` копятся вечно —
со временем БД и список загрузок распухают и становятся нечитаемыми. Нужна
авточистка старше N дней (с настройкой в `[storage]` или `[worker]`) и/или
ручное удаление. Маленькая задача, но без неё интерфейс деградирует по мере
эксплуатации.
Связано: [architecture.md](specs/architecture.md) → «Хранилище» (таблицы
`download`/`recognition`/`metadata_candidate`/`file_link`), пакет `store`.
### Eval-харнес распознавания (корпус кейсов + метрика точности)
Распознавание — ядро продукта, но смена модели или правка промпта сейчас
вслепую: регрессий не видно. Нужен корпус размеченных кейсов (русские
релизы, аниме, сезон-паки, репаки, спецвыпуски) и прогон распознавания по
нему с метрикой точности (тип/название/год/нумерация). Тогда можно
сравнивать LLM-провайдеры и версии промпта по числам, а не на ощупь.
Прогон — отдельной командой (`jellybit eval` или тестом), на фикстурах, без
реального qBittorrent.
Связано: [recognition.md](specs/recognition.md) (конвейер, модель
уверенности), пакет `recognize`.
### НФТ: масштаб до 100 одновременных загрузок (потолок — 1000)
Сейчас потолок по нагрузке нигде не зафиксирован: воркер, поллинг
qBittorrent, пул LLM-вызовов и запись в SQLite спроектированы «на глаз».
Записать в **нефункциональные требования** целевой ориентир — архитектура
держит до **100 одновременных загрузок** в работе (приём → распознавание →
раскладка), план-максимум — **1000**. Сама запись требования дешева и
высокоценна: она задаёт рамку для решений ниже по списку. Отдельно (уже
дороже) — аудит узких мест под эту цифру: одиночное соединение SQLite и
сериализация записи, конкурентность воркера и лимит параллельных
распознаваний, частота/стоимость поллинга и дедуп при наплыве.
Связано: [architecture.md](specs/architecture.md) → «Отслеживание
загрузки»/«Хранилище», пакеты `worker`, `store`, `qbt`, `llm`.
## Средний
### Словарь единого языка (ubiquitous language)
Свести термины домена в один глоссарий, чтобы пользователь, документация,
код и агент говорили на одном языке: загрузка, раздача, распознавание, матч,
кандидат, раскладка, источник/цель, хардлинк, ревью, переход состояния и
т.д. — русский термин, английский идентификатор в коде, краткое определение.
Сейчас наименования расходятся между спеками, UI и кодом, и в диалоге с
агентом приходится каждый раз сверять понятия. Глоссарий — источник истины
по именам; на нём же строится агент-ревьювер наименований (см.
[«Агенты-ревьюверы качества»](#агенты-ревьюверы-качества-наименования-архитектура-конвенции-стиль)).
Связано: [docs/conventions](conventions/README.md) (кросс-каттинг),
[architecture.md](specs/architecture.md) (домен), новый файл-глоссарий.
### Агенты-ревьюверы качества (наименования, архитектура, конвенции, стиль)
Набор узких сабагентов-ревьюверов поверх ревью-процесса из `CLAUDE.md`,
каждый со своей оптикой: соответствие наименований словарю единого языка,
соблюдение архитектурных границ (единое ядро/тонкие транспорты, инварианты
безопасности данных), конвенций (ошибки, логирование, конфиг, TZ), стиля
кода на высоком уровне и поиск дублирования. Запускаются как чекпоинт перед
`archive`/коммитом. Развивает ревью-процесс OpenSpec в сторону
воспроизводимых автоматических проверок, не заменяя человеческое ревью.
Связано: `CLAUDE.md` (ревью-процесс, конвенции),
[docs/conventions](conventions/README.md),
[«Словарь единого языка»](#словарь-единого-языка-ubiquitous-language).
### Автогенерируемый идентификатор загрузки (ULID/UUID)
Сейчас загрузка фактически идентифицируется хешем торрента (`infohash`). Это
хрупко: у одной логической загрузки может быть **несколько** хешей
(перезаливы, докачивание, репаки, v1/v2 infohash), и привязка домена к хешу
мешает слиянию и истории. Ввести собственный стабильный идентификатор
(ULID/UUID), генерируемый при приёме, как первичный ключ домена;
`infohash`(ы) — отдельный атрибут/таблица «многие к одному», по которому
**остаётся** поиск и дедуп для обратной совместимости. Enabler для
«докачивания», «второго сезона», «версий/качества» и истории переходов.
Связано: [database.md](specs/database.md) (PK `download`, `infohash`),
[«Раздачи с докачиванием»](#раздачи-с-докачиванием-слияние-при-повторном-добавлении),
[architecture.md](specs/architecture.md) → «Идентификация торрента», пакет
`store`.
### История переходов загрузки
Сохранять полную историю переходов состояний загрузки (что/когда/почему/кто
инициировал — воркер, человек, сверка), а не только текущее состояние.
Сейчас по задаче виден лишь актуальный статус, а разбор «как мы сюда попали»
идёт по логам сервера. Отдельная таблица истории даёт лог переходов в
карточке/расширенной информации и фундамент для метрик длительности стадий.
Естественно ложится на собственный идентификатор загрузки.
Связано: [«Расширенная информация о загрузке»](#расширенная-информация-о-загрузке-в-web-ui)
(лог переходов), [workflow.md](specs/workflow.md) (граф состояний),
[«Наблюдаемость: метрики»](#наблюдаемость-метрики-и-учёт-стоимости-llm)
(длительности стадий), [database.md](specs/database.md), пакеты `worker`,
`store`.
### Главная: список загрузок вместо таблицы
Переделать главную страницу из таблицы в **список** карточек. Для каждой
загрузки: название (как распознали при добавлении в qBittorrent), `infohash`
с кнопкой быстрого копирования, статус и кнопки действий. Контекст (исходное
сообщение/magnet) спрятать под спойлер или вынести на отдельный детальный
экран (см. «Расширенная информация о загрузке»). Естественно сочетается с
фильтром/поиском/пагинацией по мере роста БД.
Связано: [«Расширенная информация о загрузке в web-UI»](#расширенная-информация-о-загрузке-в-web-ui),
[«Список загрузок: фильтр, поиск, пагинация»](#список-загрузок-фильтр-поиск-пагинация),
[review-ux.md](specs/review-ux.md), пакет `httpapi`.
### Расширенная информация о загрузке в web-UI
Отдельная страница просмотра одной загрузки — целиком отображение того, что
лежит в БД по задаче: актуальный статус (текущее состояние + лог переходов),
исходный контекст и magnet, распознанные данные и матч в метабазе (см.
«показывать матч»), а также **точная раскладка, если есть** — целевые пути и
созданные хардлинки. Помогает разбираться, когда что-то пошло не так, без
чтения логов сервера. Сюда же выносится контекст с карточки в списке
загрузок.
Связано: [«Главная: список загрузок»](#главная-список-загрузок-вместо-таблицы),
[review-ux.md](specs/review-ux.md), [architecture.md](specs/architecture.md)
→ «Хранилище» (`download`/`recognition`/`file_link`), пакет `httpapi`.
### Машина состояний на go-библиотеке
Сейчас FSM реализована вручную в `worker`. Выбрать подходящую go-библиотеку
для описания воркфлоу/машины состояний и перевести переходы на неё — ради
декларативности, проверяемости переходов и единого места правды. Кандидаты
для оценки: `looplab/fsm`, `qmuntal/stateless` (и аналоги). Граф и переходы
уже формализованы — переносим один в один.
Связано: [workflow.md](specs/workflow.md) (текущий граф состояний).
### Привязка уведомлений к источнику в ботах (мульти-бот)
Уведомления и запросы подтверждения должен получать тот, кто прислал
загрузку: автор сообщения о новой раздаче — адресат пингов и ревью по ней.
Транспортов-ботов может быть несколько (Telegram, в перспективе Matrix и
др.); каждый адресует «своему» отправителю. Веб-интерфейс остаётся
**единым для всех** и точкой правды по функциональности (боты — тонкие
адаптеры над тем же ядром). Нужно: хранить у загрузки источник/транспорт и
идентификатор отправителя, маршрутизировать пинги по нему.
Связано: [review-ux.md](specs/review-ux.md) (разделение труда транспортов,
веб = точные правки), [architecture.md](specs/architecture.md) →
«Транспорты».
### Раздачи с докачиванием (слияние при повторном добавлении)
Свежий сериал часто раздают по мере выхода: торрент содержит 5 эпизодов из
10. Позже его перезаливают целиком (или добавляют недостающие серии), и
пользователь повторно добавляет тот же торрент. Нужно распознать, что это
**та же** раздача/сезон, и повторить раскладку с **слиянием**: доложить
недостающие хардлинки, не дублируя уже разложенное и не перезаписывая
существующее (инвариант «существующее не трогаем»). Перекликается с
«Проблемой второго сезона», но здесь доливаются эпизоды внутри одного
сезона, а не новый сезон. Нужно продумать: как опознать повторное
добавление (хеш торрента / провайдер-id + сезон), как сверять состав файлов
и доливать только новые.
Связано: [«Проблема второго сезона»](#проблема-второго-сезона),
[jellyfin-layout.md](specs/jellyfin-layout.md) (раскладка, идемпотентность),
[workflow.md](specs/workflow.md) (повторный прогон загрузки).
### Улучшения UI: показывать матч с записью метабазы
Во всех транспортах (веб, Telegram) показывать, **с какой именно записью**
метабазы (TMDB/TVDB) сматчилась загрузка: название, год, провайдер-id,
ссылку. Сейчас результат распознавания непрозрачен — пользователь не видит,
к чему привязались, и не может быстро поймать ошибочный матч. На web-стороне
развивается в полноценный выбор источника — см. [«Ревью: выбор источника
совпадения»](#ревью-выбор-источника-совпадения-и-предпросмотр).
Связано: [review-ux.md](specs/review-ux.md), [recognition.md](specs/recognition.md)
(матч в базе), [architecture.md](specs/architecture.md) → «Транспорты».
### Аниме с абсолютной нумерацией
Релизы аниме часто нумеруют серии сквозным числом (`#137`) без сезонов, а
Jellyfin ждёт `SxxEyy`. Нужен пересчёт абсолютной нумерации в сезон/серию —
надёжнее всего через TVDB (там есть absolute order). Отдельный крайний
случай распознавания; на стороне ревью — веб-хелпер «absolute → S·E».
Связано: [recognition.md](specs/recognition.md) (конвейер, сезон-паки),
[jellyfin-layout.md](specs/jellyfin-layout.md) (нумерация серий),
[review-ux.md](specs/review-ux.md) (крайние сценарии).
### Добавление торрентов файлом/ссылкой — «единое окно»
Поддержать источники помимо magnet: `.torrent`-файл и URL (отдаём их в
qBittorrent, без исходящих запросов на пользовательский URL — SSRF
исключён). Идеал — одно поле «единого окна»: кидаем туда текст или файл, а
сервис сам разбирает, что это (magnet / ссылка / .torrent / сообщение
бота), и заводит загрузку.
Связано: [architecture.md](specs/architecture.md) → «Транспорты»
(`source_type = magnet|torrent|url` уже в схеме), пакет `ingest` (сейчас
поддержан только magnet).
### Бэкап SQLite
`architecture.md` требует «бекапить data-том», но *как* — не описано. Без
понятной стратегии сбой или редеплой стирают всё in-flight состояние.
Зафиксировать решение и реализовать: периодический `VACUUM INTO` в
`/data/backups` по расписанию (с ротацией) либо потоковая репликация
(litestream). Лучше сделать, пока БД маленькая.
Связано: [architecture.md](specs/architecture.md) → «Деплой» (data-том,
«бекапить-и-не-терять»), пакет `store`.
### Версии/качество одного тайтла (репаки, апгрейд 1080p → 2160p)
Фильм уже разложен, позже добавили раздачу лучшего качества — сейчас это
просто новая задача, упирающаяся в «коллизию цели → review», без понятия
«это та же вещь, заменить версию». Нужно осознанно обработать апгрейд
качества: распознать тот же тайтл, предложить замену существующей раскладки
либо сосуществование версий (Jellyfin поддерживает несколько версий одного
фильма). Близко к «докачиванию», но про качество, а не про эпизоды.
Связано: [«Раздачи с докачиванием»](#раздачи-с-докачиванием-слияние-при-повторном-добавлении),
[jellyfin-layout.md](specs/jellyfin-layout.md) (never-overwrite, коллизия),
[architecture.md](specs/architecture.md) → «Идентификация торрента»
(репаки = разные infohash → разные задачи).
### Глубокий healthcheck и статус зависимостей
`/healthz` проверяет только сам сервис. Если qBittorrent, LLM или метабаза
недоступны — узнаёшь лишь по застрявшим задачам. Нужна readiness-проверка
ключевых зависимостей и отражение их состояния в UI (бейдж «qBittorrent
недоступен»), чтобы причина простоя была видна сразу.
Связано: [architecture.md](specs/architecture.md) → «Деплой» (healthcheck),
пакеты `qbt`, `llm`, `metadata`, `httpapi`.
### Обучение на правках человека (few-shot из прошлых ревью)
Когда человек поправил матч, тип или нумерацию — сохранять это как пример и
подмешивать похожие в будущие промпты. Системно повышает точность на «твоих»
трекерах и форматах имён без смены модели. Развитие идеи многоступенчатой
верификации, но дешевле: учимся на уже собранных `hint`/`override`.
Связано: [recognition.md](specs/recognition.md) (конвейер, промпт),
[«Многоступенчатая верификация»](#многоступенчатая-верификация-привязки-идея),
[architecture.md](specs/architecture.md) → «Хранилище» (`hint`, `override`).
### Список загрузок: фильтр, поиск, пагинация
Прямое следствие роста БД (см. «Ретеншн»): плоский список загрузок со
временем становится непригоден. Нужны фильтр по состоянию, поиск по
названию и пагинация. Естественно ложится на список-карточки главной и на
экран расширенной информации.
Частный случай как разумный дефолт: на главной **по умолчанию скрывать
загрузки в статусе `deleted`** (терминальные, удалённые из источника и
цели — шум в ленте), с переключателем/фильтром «показать всё». Маленькая
часть, может приехать раньше полноценного фильтра.
Связано: [«Главная: список загрузок»](#главная-список-загрузок-вместо-таблицы),
[«Расширенная информация о загрузке в web-UI»](#расширенная-информация-о-загрузке-в-web-ui),
пакет `httpapi`.
## Низкий
### Мгновенные обновления через SSE
Живые обновления прогресса сейчас на htmx-поллинге (фаза 2 веб-UI) — просто
и работает, но с задержкой в интервал опроса и холостыми запросами.
Перевести динамический контент (прогресс загрузки, смена статуса, раздача)
на Server-Sent Events, чтобы обновления приходили почти мгновенно и без
лишнего поллинга. Поллинг работает, поэтому это улучшение, а не блокер; SSE —
один долгоживущий ответ на соединение, ложится на server-rendered UI без
тяжёлого фронтенда.
Связано: [architecture.md](specs/architecture.md) → «Транспорты»,
[review-ux.md](specs/review-ux.md), пакет `httpapi`.
### Полировка веб-UI: список и карточка загрузки
Мелкие правки вёрстки и согласованности (низкая цена, заметный эффект):
- в списке на главной показывать имя загрузки **ровно то**, что отдаём в
qBittorrent как имя раздачи, — чтобы список и клиент были синхронны
(уточняет [«Главная: список загрузок»](#главная-список-загрузок-вместо-таблицы));
- в карточке блок «Распознано как» растянуть на всю ширину блока (сейчас
обрывается на середине);
- в блоке «Файлы и раскладка» приклеить стрелку к первой строке
(файл-источник), чтобы источник и цель выводились строго один под другим.
Связано: [«Главная: список загрузок»](#главная-список-загрузок-вместо-таблицы),
[review-ux.md](specs/review-ux.md), пакет `httpapi`.
### Многоступенчатая верификация привязки _(идея)_
Несколько раз извлекать данные из раздачи и контекста разными промптами,
искать в метабазах, затем сводить результаты в общий вердикт
(голосование/консенсус) — выше точность ценой нескольких вызовов LLM и
запросов к базам. Требует проработки: когда включать, как мерджить
расхождения, стоимость/латентность.
Связано: [recognition.md](specs/recognition.md) (конвейер и модель
уверенности).
### Выбор из нескольких находок метабазы в Telegram
Когда распознавание даёт несколько подходящих кандидатов в метабазе,
предлагать их в Telegram списком (кнопки) для ручного выбора, а не молча
брать первый/лучший. Веб остаётся точкой точных правок (полный выбор
источника — см. [«Ревью: выбор источника
совпадения»](#ревью-выбор-источника-совпадения-и-предпросмотр)), бот —
быстрый выбор из готового короткого списка.
Связано: [review-ux.md](specs/review-ux.md) (боты — быстрые действия, веб —
точные правки), [recognition.md](specs/recognition.md) (кандидаты матча).
### Проверка свободного места перед copy-fallback
Когда хардлинк невозможен (`EXDEV`/`ENOTSUP`/…), `layout` копирует файл,
дублируя место на диске. На забитом диске это упрётся в полку посреди
раскладки. Перед копированием проверять доступное место и при нехватке
внятно уходить в `failed` с понятной причиной, а не падать на полпути.
Связано: [architecture.md](specs/architecture.md) → «Раскладка файлов»
(фолбэк-копирование), пакет `layout`.
### Кэш метабаз (и опционально LLM)
Повторные и ретраящиеся прогоны распознавания бьют TMDB/TVDB/TVMaze одним и
тем же запросом. Кэш ответов с TTL экономит лимиты API и ускоряет «Распознать
заново»/«Уточнить». При желании — кэш ответов LLM по хешу входа (но он менее
полезен, т.к. вход меняется подсказками).
Связано: [recognition.md](specs/recognition.md) (сверка с базой), пакеты
`metadata`, `llm`.
### guessit как сервис-спутник _(идея)_
`go-ptn` слабее питоновского `guessit`. Если точности пред-парса не
хватит — завернуть `guessit` в крошечный HTTP-сервис (один файл,
поставляется рядом с бинарём jellybit) и спрашивать его на шаге
пред-парса. Сохраняет «доставку копированием»: два файла вместо одного.
Связано: [recognition.md](specs/recognition.md) → «На будущее» (пред-парс).
### Завершение загрузки через webhook _(идея)_
Сейчас завершение ловим поллингом qBittorrent раз в несколько секунд.
Альтернатива: «Run external program on torrent completion» в qBittorrent
дёргает эндпоинт jellybit. Реагирует быстрее, но связывает нас с конфигом
qBittorrent. Решим по опыту эксплуатации.
Связано: [architecture.md](specs/architecture.md) → «Отслеживание загрузки»,
пакет `worker`.
### Авторизация веб-UI (на будущее)
Решено для v1: без авторизации в доверенной LAN, опц. allowlist подсетей
(`http.trusted_subnets`) — как умеет qBittorrent. Если понадобится защита:
токен/Basic в самом приложении или вынос за reverse-proxy с
аутентификацией.
Связано: [architecture.md](specs/architecture.md) → «Транспорты» (доступ к
веб-UI), пакет `httpapi`.
### Современный Web-UI как PWA
Переделать веб-интерфейс в современное PWA-приложение (устанавливаемое,
отзывчивое, удобное с телефона). Текущий server-rendered UI функционален,
поэтому это улучшение, а не блокер; большой объём работы.
Связано: [review-ux.md](specs/review-ux.md) (веб = точные правки),
пакет `httpapi`.