Перенос дизайна в server-rendered веб-UI (web-ui)
Новая capability web-ui: презентационный перенос готового дизайна
(семантический HTML + единый jellybit.css, тёмная тема по настройке ОС)
в html/template без шага сборки.
- Встроенная (go:embed) отдача статики под /static с Cache-Control и
cache-busting (?v=<hash> по содержимому css/js).
- Шрифты IBM Plex self-hosted (@font-face, cyrillic+latin), без CDN.
- Наколеночный менеджер зависимостей: вендор (htmx + шрифты) не хранится
в репо (gitignore), идемпотентно добывается `task assets` по
web/assets.manifest с проверкой sha256; task build/run зависят от assets.
- Общие партиалы: шапка, бейдж статуса (карта всех 14 состояний),
виджет «файл источника → раскладка» (общий для review и download).
- Страницы: список с фильтром/поиском, ревью, новая страница просмотра
загрузки (/download/{id}). deleted скрыт по умолчанию.
- Превью раскладки берётся из единой логики internal/layout
(buildFileRows), без дублирования правил имён в шаблонах.
- Убраны meta-refresh и инлайн-стили; copyHash на vanilla с fallback
на execCommand и честной индикацией (целевой деплой — HTTP LAN).
Вне scope (отдельный change): живые обновления прогресса и раздел
раздачи, клиентский режим ручной раскладки файл→серия (с Alpine.js).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"git.vakhrushev.me/av/jellybit/internal/store"
|
||||
)
|
||||
|
||||
// --- Страница просмотра одной загрузки ---
|
||||
|
||||
type downloadDetailView struct {
|
||||
ID int64
|
||||
Title string
|
||||
Source string
|
||||
Infohash string
|
||||
InfohashShort string
|
||||
Context string
|
||||
State string
|
||||
Error string
|
||||
Note string
|
||||
CreatedAt string
|
||||
UpdatedAt string
|
||||
|
||||
// Распознавание (если есть план).
|
||||
HasPlan bool
|
||||
MediaType string
|
||||
IsSeries bool
|
||||
RecTitle string
|
||||
OriginalTitle string
|
||||
Year int
|
||||
Provider string
|
||||
ProviderID string
|
||||
NoBase bool
|
||||
Confidence string
|
||||
Files []fileRow
|
||||
|
||||
// Действия по состоянию (как на главной).
|
||||
Terminal bool
|
||||
Reviewable bool
|
||||
Undoable bool
|
||||
Relinkable bool
|
||||
Retriable bool
|
||||
}
|
||||
|
||||
func (s *server) handleDownload(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := pathID(r)
|
||||
if err != nil {
|
||||
http.Error(w, "некорректный id", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
rd, err := s.deps.Reviewer.ReviewData(r.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
http.Error(w, "задача не найдена", http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
s.deps.Logger.Error("download detail data", "id", id, "error", err)
|
||||
http.Error(w, "внутренняя ошибка", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
d := rd.Download
|
||||
view := downloadDetailView{
|
||||
ID: id,
|
||||
Title: d.SourceRef,
|
||||
Source: shorten(d.SourceRef, 120),
|
||||
Infohash: d.Infohash.String,
|
||||
InfohashShort: shortenHash(d.Infohash.String),
|
||||
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,
|
||||
}
|
||||
|
||||
if rd.Recognition != nil {
|
||||
view.HasPlan = len(rd.Plan.Files) > 0
|
||||
view.MediaType = string(rd.Plan.Type)
|
||||
view.IsSeries = rd.Plan.Type == "series"
|
||||
view.RecTitle = rd.Plan.Title
|
||||
view.OriginalTitle = rd.Plan.OriginalTitle
|
||||
view.Year = rd.Plan.Year
|
||||
switch rd.Provider {
|
||||
case "", "none":
|
||||
view.NoBase = rd.Provider == "none"
|
||||
default:
|
||||
view.Provider = rd.Provider
|
||||
view.ProviderID = rd.ProviderID
|
||||
}
|
||||
if rd.Recognition.Confidence.Valid {
|
||||
view.Confidence = strconv.FormatFloat(rd.Recognition.Confidence.Float64, 'f', 2, 64)
|
||||
}
|
||||
|
||||
// Файл источника → целевой путь из превью (единая логика layout).
|
||||
view.Files = buildFileRows(rd.Plan, rd.Preview)
|
||||
}
|
||||
|
||||
s.render(w, "download.html", view)
|
||||
}
|
||||
Reference in New Issue
Block a user