diff --git a/internal/httpapi/download.go b/internal/httpapi/download.go index 52a11f1..ce61fe2 100644 --- a/internal/httpapi/download.go +++ b/internal/httpapi/download.go @@ -12,19 +12,17 @@ import ( // --- Страница просмотра одной загрузки --- type downloadDetailView struct { - ID string - Title string - SourceType string // тип источника (magnet/torrent/url) — блок «Информация о торренте» - SourceFull string // полный источник (magnet) — блок «Информация о торренте» - Infohash string // первый хеш (для шапки) - InfohashShort string - Infohashes []string // все хеши загрузки (блок «Информация о торренте») - Context string - State string - Error string - Note string - CreatedAt string - UpdatedAt string + ID string + Title string + SourceType string // тип источника (magnet/torrent/url) — блок «Информация о торренте» + SourceFull string // полный источник (magnet) — блок «Информация о торренте» + Infohashes []string // все хеши загрузки (блок «Информация о торренте») + Context string + State string + Error string + Note string + CreatedAt string + UpdatedAt string // Распознавание (если есть план). HasPlan bool @@ -84,22 +82,20 @@ func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) { d := rd.Download view := downloadDetailView{ - ID: id, - Title: detailTitle(d, rd), - SourceType: string(d.SourceType), - SourceFull: d.SourceRef, - Infohash: d.PrimaryInfohash(), - InfohashShort: shortenHash(d.PrimaryInfohash()), - Infohashes: d.HashList(), - Context: d.Context, - State: string(d.State), - Error: d.ErrorMsg.String, - Note: desyncNote(d.State), - CreatedAt: d.CreatedAt, - UpdatedAt: d.UpdatedAt, - Terminal: d.State.IsTerminal(), - Reviewable: d.State == store.StateReview || d.State == store.StateDeferred, - Undoable: d.State == store.StateDone, + ID: id, + Title: detailTitle(d, rd), + SourceType: string(d.SourceType), + SourceFull: d.SourceRef, + Infohashes: d.HashList(), + Context: d.Context, + State: string(d.State), + Error: d.ErrorMsg.String, + Note: desyncNote(d.State), + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + Terminal: d.State.IsTerminal(), + Reviewable: d.State == store.StateReview || d.State == store.StateDeferred, + Undoable: d.State == store.StateDone, Relinkable: d.State == store.StateReverted || d.State == store.StateCancelled || d.State == store.StateTargetMissing, Retriable: d.State == store.StateFailed || d.State == store.StateStuck, diff --git a/internal/httpapi/httpapi.go b/internal/httpapi/httpapi.go index 61bc688..0dfbcce 100644 --- a/internal/httpapi/httpapi.go +++ b/internal/httpapi/httpapi.go @@ -181,8 +181,6 @@ type pageLink struct { type downloadView struct { ID string Title string // отображаемый заголовок карточки - Infohash string // полный (для копирования) - InfohashShort string // усечённый (для показа) Context string State string Error string @@ -524,8 +522,6 @@ func toView(d store.Download) downloadView { return downloadView{ ID: d.ID, Title: downloadTitle(d), - Infohash: d.PrimaryInfohash(), - InfohashShort: shortenHash(d.PrimaryInfohash()), Context: d.Context, State: state, Error: d.ErrorMsg.String, @@ -560,14 +556,6 @@ func oneLine(s string) string { return strings.Join(strings.Fields(s), " ") } -// shortenHash усекает infohash до вида "a1b2c3d4e5…f0" для компактного показа. -func shortenHash(h string) string { - if len(h) <= 12 { - return h - } - return h[:10] + "…" + h[len(h)-2:] -} - // desyncNote — пояснение состояния рассинхрона для UI (см. state-reconciliation). func desyncNote(s store.State) string { switch s { diff --git a/internal/store/list.go b/internal/store/list.go index e378666..3c1bb24 100644 --- a/internal/store/list.go +++ b/internal/store/list.go @@ -41,7 +41,7 @@ func statesInGroup(g StateGroup) []State { // ListFilter — параметры страницы списка загрузок. type ListFilter struct { Group StateGroup // группа состояний ("" / all — без сужения) - Query string // поиск по названию/infohash/контексту (подстрока) + Query string // поиск по id/названию/infohash/контексту (подстрока) IncludeDeleted bool // показывать состояние deleted (только под all) Limit int // размер страницы Offset int // смещение страницы @@ -71,12 +71,13 @@ func listWhere(f ListFilter) (string, []any) { // ICU в чистом modernc-драйвере нет. Достаточно для инфохэша/латиницы; // кириллические названия ищем как введены. like := "%" + q + "%" - conds = append(conds, "(source_ref LIKE ? COLLATE NOCASE "+ + conds = append(conds, "(download.id LIKE ? COLLATE NOCASE "+ + "OR source_ref LIKE ? COLLATE NOCASE "+ "OR display_name LIKE ? COLLATE NOCASE "+ "OR context LIKE ? COLLATE NOCASE "+ "OR EXISTS (SELECT 1 FROM download_infohash dh "+ "WHERE dh.download_id = download.id AND dh.infohash LIKE ? COLLATE NOCASE))") - args = append(args, like, like, like, like) + args = append(args, like, like, like, like, like) } if len(conds) == 0 { diff --git a/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/.openspec.yaml b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/.openspec.yaml new file mode 100644 index 0000000..d86f152 --- /dev/null +++ b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-07-04 diff --git a/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/design.md b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/design.md new file mode 100644 index 0000000..603428b --- /dev/null +++ b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/design.md @@ -0,0 +1,38 @@ +## Context + +Изменение презентационное и уже реализовано в коде (шаблоны, поиск, view-структуры). +Дизайн-документ здесь короткий и нужен лишь как зависимость для `tasks` и чтобы +зафиксировать одно продуктовое решение. Полноценного двухчекпоинтного ревью +задача не требует (тривиальная — достаточно прохода по коду). + +## Goals / Non-Goals + +**Goals:** +- Показывать и копировать в карточках `download.id` — тот же ключ, что в логах + (`download_id`), чтобы «скопировал → нашёл в логах» работало без перекодировки. +- Поиск по списку — по любому идентификатору загрузки (`download.id` ИЛИ infohash). + +**Non-Goals:** +- Не убираем infohash из UI совсем: он остаётся в блоке «Информация о торренте». +- Не меняем модель данных, API-DTO и формат логов. + +## Decisions + +- **Идентификатор карточки — `download.id`, не infohash.** В логах основной ключ + связывания — `download_id` (см. `internal/worker/*`), поэтому именно его нужно + копировать и грепать. Infohash второстепенен и уместен только в детальном блоке + о торренте. Альтернатива (оставить infohash в шапке) отвергнута: заставляет + пользователя вручную сопоставлять хэш с id при чтении логов. +- **Поиск матчит любой идентификатор.** В LIKE-условие `internal/store/list.go` + добавлен `download.id` рядом с уже имевшимся infohash; название/контекст + сохранены. Так скопированный из карточки id находится обратным поиском. +- **Чистка мёртвого кода.** Поля `Infohash`/`InfohashShort` во view-структурах и + хелпер `shortenHash` стали не нужны — удалены, чтобы не копить неиспользуемое. + +## Risks / Trade-offs + +- [ULID менее узнаваем на глаз, чем hex-infohash] → показываем полностью (26 + символов помещаются в строку), рядом кнопка копирования; в детальном блоке + infohash остаётся для тех, кому он нужен. +- [Проза `## Purpose` в live-спеке упоминает «копирование infohash»] → правится + прямой правкой live-спеки при archive (Purpose не покрывается дельта-операциями). diff --git a/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/proposal.md b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/proposal.md new file mode 100644 index 0000000..20b851f --- /dev/null +++ b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/proposal.md @@ -0,0 +1,37 @@ +## Why + +Веб-UI показывал в карточках (список + шапка страницы просмотра) infohash с +кнопкой копирования. Но в логах основной ключ загрузки — `download_id` (ULID), +именно по нему удобно грепать и связывать события; infohash в логах второстепенен. +Пользователю нужно копировать из карточки то, что реально ищется в логах. + +## What Changes + +- В карточке списка и в шапке `/download/{id}` показываем и копируем + `download.id` (ULID) вместо infohash — как основной идентификатор загрузки. +- Infohash остаётся видимым только в блоке «Информация о торренте» (без + изменений), где ему место рядом с сырым источником. +- Поиск по списку расширяем: искать можно по **любому идентификатору** + (`download.id` ИЛИ infohash), плюс прежние название/контекст. Плейсхолдер + строки поиска обновлён. + +## Capabilities + +### New Capabilities + + +### Modified Capabilities +- `web-ui`: основной копируемый идентификатор карточек — `download.id`, а не + infohash (Requirement «Клиентские взаимодействия без сборки» и Scenario + копирования); поиск по списку матчит любой идентификатор загрузки (Scenario + «Серверный фильтр и поиск»). + +## Impact + +- Шаблоны: `web/templates/index.html`, `web/templates/download.html`. +- Поиск: `internal/store/list.go` (в LIKE-условие добавлен `download.id`). +- View: `internal/httpapi/httpapi.go`, `internal/httpapi/download.go` (удалены + осиротевшие поля `Infohash`/`InfohashShort` и хелпер `shortenHash`). +- Клиент: `web/static/js/app.js` (комментарий — функция копирования теперь про + идентификатор загрузки, не только infohash). +- Совместимость: чисто презентационно; API-DTO и модель данных не меняются. diff --git a/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/specs/web-ui/spec.md b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/specs/web-ui/spec.md new file mode 100644 index 0000000..e3c27aa --- /dev/null +++ b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/specs/web-ui/spec.md @@ -0,0 +1,66 @@ +## MODIFIED Requirements + +### Requirement: Страницы веб-UI + +Веб-UI SHALL предоставлять страницы: список загрузок с единым окном +добавления, **серверными фильтром по группе состояний, поиском и постраничной +выдачей (пагинацией)** (`/`), экран ревью одной загрузки (`/review/{id}`) +и страницу просмотра одной загрузки (`/download/{id}`) с распознаванием, +файлами→раскладкой, историей, блоком информации о торренте и — для сидирующих +задач — секцией живой статистики раздачи. Карточки активных (downloading) +загрузок в списке SHALL содержать индикатор прогресса. Фильтр, поиск и номер +страницы SHALL передаваться GET-параметрами запроса (например `f`, `q`, `page`) +и SHALL работать без клиентского JavaScript. Состояние `deleted` SHALL быть +скрыто в списке по умолчанию (с переключателем «показать всё»). Механика живого +обновления прогресса и наполнение секции раздачи определяются capability +`live-status`. + +#### Scenario: Просмотр одной загрузки + +- **WHEN** клиент открывает `GET /download/{id}` существующей загрузки +- **THEN** отрисовывается страница с её распознаванием, файлами, раскладкой и + историей + +#### Scenario: Прогресс активной загрузки в списке + +- **WHEN** в списке есть загрузка в состоянии `downloading` +- **THEN** её карточка содержит индикатор прогресса (прогресс-бар со скоростью + и ETA) + +#### Scenario: Удалённые скрыты по умолчанию + +- **WHEN** в списке есть загрузки в состоянии `deleted` и фильтр «показать + всё» не включён +- **THEN** они не отображаются, но доступны при включённом переключателе + +#### Scenario: Пагинация списка + +- **WHEN** загрузок под текущим фильтром больше, чем помещается на одну + страницу, и клиент запрашивает `GET /?page=N` +- **THEN** возвращается N-я страница результатов и элементы навигации по + страницам, сохраняющие текущие фильтр и поисковый запрос + +#### Scenario: Серверный фильтр и поиск + +- **WHEN** клиент запрашивает список с параметрами фильтра по состоянию и/или + строкой поиска (`GET /?f=review&q=дюна`) +- **THEN** сервер возвращает только подходящие загрузки (по группе состояний и + совпадению строки в названии, любом идентификаторе загрузки — `download.id` + ИЛИ infohash — и контексте), отфильтрованные на стороне БД, а не на клиенте + +### Requirement: Клиентские взаимодействия без сборки + +Веб-UI SHALL реализовывать клиентскую логику без шага сборки и без реактивных +фреймворков: копирование идентификатора загрузки (vanilla JS) и раскрытие +контекста нативным `
`. Основной копируемый идентификатор в карточке +списка и в шапке страницы просмотра SHALL быть `download.id` (ULID) — тот же +ключ, что пишется в логи (`download_id`). Все действия над загрузкой SHALL +выполняться через формы/htmx (раундтрип на сервер), без клиентского пересчёта +доменного состояния. + +#### Scenario: Копирование идентификатора загрузки + +- **WHEN** пользователь нажимает кнопку копирования рядом с идентификатором + загрузки (`download.id`) в карточке списка или шапке страницы просмотра +- **THEN** значение `download.id` копируется в буфер обмена без перезагрузки + страницы diff --git a/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/tasks.md b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/tasks.md new file mode 100644 index 0000000..06742cc --- /dev/null +++ b/openspec/changes/archive/2026-07-04-web-ui-download-id-identifier/tasks.md @@ -0,0 +1,25 @@ +## 1. Карточки: идентификатор = download.id + +- [x] 1.1 `web/templates/index.html` — в карточке списка показать и копировать + `{{.ID}}` вместо infohash; убрать `{{if .Infohash}}` +- [x] 1.2 `web/templates/download.html` — в шапке страницы просмотра показать и + копировать `{{.ID}}` вместо infohash +- [x] 1.3 `internal/httpapi/httpapi.go`, `internal/httpapi/download.go` — удалить + осиротевшие поля `Infohash`/`InfohashShort` и хелпер `shortenHash` +- [x] 1.4 `web/static/js/app.js` — комментарий: функция копирования теперь про + идентификатор загрузки (не только infohash) + +## 2. Поиск по любому идентификатору + +- [x] 2.1 `internal/store/list.go` — добавить `download.id` в LIKE-условие + поиска (рядом с infohash/названием/контекстом); обновить комментарий `Query` +- [x] 2.2 `web/templates/index.html` — обновить плейсхолдер строки поиска + +## 3. Спека и проверки + +- [x] 3.1 Дельта-спека `web-ui`: MODIFIED «Страницы веб-UI» (поле поиска) и + «Клиентские взаимодействия без сборки» (копирование идентификатора) +- [x] 3.2 Обновить прозу `## Purpose` в live-спеке `openspec/specs/web-ui/spec.md` + (упоминание «копирование infohash») — при archive +- [x] 3.3 `openspec validate --strict` для change +- [x] 3.4 `task test` / `task lint` — зелёные diff --git a/openspec/specs/web-ui/spec.md b/openspec/specs/web-ui/spec.md index 2579097..74f7e97 100644 --- a/openspec/specs/web-ui/spec.md +++ b/openspec/specs/web-ui/spec.md @@ -5,8 +5,8 @@ Презентационный слой веб-интерфейса: встроенная (`go:embed`) отдача статики и self-hosted шрифтов, единая дизайн-система (`jellybit.css`, тёмная тема по настройке ОС), рендеринг страниц (список загрузок, ревью, просмотр) с бейджами -состояний и клиентскими взаимодействиями без сборки (копирование infohash, -спойлер контекста). Превью раскладки берётся из единой логики `internal/layout`, +состояний и клиентскими взаимодействиями без сборки (копирование идентификатора +загрузки, спойлер контекста). Превью раскладки берётся из единой логики `internal/layout`, а не дублируется в шаблонах. Тонкий транспорт над ядром (приём — `ingest`, команды — `worker`, чтение — `store`). ## Requirements @@ -130,8 +130,8 @@ self-hosted шрифтов, единая дизайн-система (`jellybit. - **WHEN** клиент запрашивает список с параметрами фильтра по состоянию и/или строкой поиска (`GET /?f=review&q=дюна`) - **THEN** сервер возвращает только подходящие загрузки (по группе состояний и - совпадению строки в названии/infohash/контексте), отфильтрованные на стороне - БД, а не на клиенте + совпадению строки в названии, любом идентификаторе загрузки — `download.id` + ИЛИ infohash — и контексте), отфильтрованные на стороне БД, а не на клиенте ### Requirement: Порядок списка загрузок @@ -262,14 +262,18 @@ jellybit (`created_at`). Порядок MUST быть согласован ме ### Requirement: Клиентские взаимодействия без сборки Веб-UI SHALL реализовывать клиентскую логику без шага сборки и без реактивных -фреймворков: копирование infohash (vanilla JS) и раскрытие контекста нативным -`
`. Все действия над загрузкой SHALL выполняться через формы/htmx -(раундтрип на сервер), без клиентского пересчёта доменного состояния. +фреймворков: копирование идентификатора загрузки (vanilla JS) и раскрытие +контекста нативным `
`. Основной копируемый идентификатор в карточке +списка и в шапке страницы просмотра SHALL быть `download.id` (ULID) — тот же +ключ, что пишется в логи (`download_id`). Все действия над загрузкой SHALL +выполняться через формы/htmx (раундтрип на сервер), без клиентского пересчёта +доменного состояния. -#### Scenario: Копирование infohash +#### Scenario: Копирование идентификатора загрузки -- **WHEN** пользователь нажимает кнопку копирования рядом с infohash -- **THEN** значение infohash копируется в буфер обмена без перезагрузки +- **WHEN** пользователь нажимает кнопку копирования рядом с идентификатором + загрузки (`download.id`) в карточке списка или шапке страницы просмотра +- **THEN** значение `download.id` копируется в буфер обмена без перезагрузки страницы #### Scenario: Раскрытие контекста без JS diff --git a/web/static/js/app.js b/web/static/js/app.js index ece57d1..1473781 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -1,9 +1,10 @@ // Клиентская мелочь без сборки и фреймворков. // htmx тянет серверные фрагменты, здесь — только то, что серверу знать не нужно. -// Копирование infohash в буфер обмена. На целевом деплое (домашний LAN по HTTP) -// navigator.clipboard недоступен — secure context только на HTTPS/localhost, -// поэтому есть fallback на execCommand, а индикация честная: «✓» лишь при успехе. +// Копирование идентификатора (download id / infohash) в буфер обмена. На целевом +// деплое (домашний LAN по HTTP) navigator.clipboard недоступен — secure context +// только на HTTPS/localhost, поэтому есть fallback на execCommand, а индикация +// честная: «✓» лишь при успехе. function copyHash(btn, full) { if (navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(full).then( diff --git a/web/templates/download.html b/web/templates/download.html index 8da502b..3b4cd49 100644 --- a/web/templates/download.html +++ b/web/templates/download.html @@ -23,8 +23,8 @@
{{.Title}}
{{template "status_badge" .State}} - {{if .Infohash}}{{.InfohashShort}} - {{end}} + {{.ID}} +
{{if .Note}}

{{.Note}}

{{end}} diff --git a/web/templates/index.html b/web/templates/index.html index b87f519..c79cffa 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -34,7 +34,7 @@ @@ -56,12 +56,10 @@
- {{if .Infohash}}
- {{.InfohashShort}} - + {{.ID}} +
- {{end}}
{{template "status_badge" .State}}