Воркер ведёт 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>
105 lines
4.9 KiB
HTML
105 lines
4.9 KiB
HTML
<!doctype html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>jellybit — загрузки</title>
|
||
<link rel="stylesheet" href="{{asset "css/jellybit.css"}}">
|
||
<script src="{{asset "vendor/htmx.min.js"}}" defer></script>
|
||
<script src="{{asset "js/app.js"}}" defer></script>
|
||
</head>
|
||
<body>
|
||
|
||
{{template "header" "downloads"}}
|
||
|
||
<main class="main">
|
||
<h1 class="page-title">Загрузки</h1>
|
||
<p class="page-sub">Кинь magnet, ссылку, .torrent или сообщение бота — остальное jellybit сделает сам.</p>
|
||
|
||
{{if .Error}}<div class="result-banner" style="background:var(--st-err-bg);border-color:color-mix(in srgb,var(--st-err) 35%,transparent)"><h2 style="color:var(--st-err)">{{.Error}}</h2></div>{{end}}
|
||
|
||
<!-- Единое окно добавления -->
|
||
<form class="add-box" method="post" action="/ui/downloads">
|
||
<div class="add-row">
|
||
<input class="input" type="text" name="source" autocomplete="off" required
|
||
placeholder="magnet:?xt=urn:btih:… · ссылка · .torrent · или вставь текст бота">
|
||
<button class="btn btn-primary btn-lg" type="submit">Добавить</button>
|
||
</div>
|
||
<p class="add-hint">Можно вставить целиком сообщение трекер-бота — magnet и контекст разберутся автоматически.</p>
|
||
</form>
|
||
|
||
{{if .Downloads}}
|
||
<!-- Панель фильтров -->
|
||
<div class="toolbar" id="list-toolbar">
|
||
<div class="search">
|
||
<span class="ico">⌕</span>
|
||
<input class="input" type="search" placeholder="Поиск по названию или infohash…" oninput="applyFilters()">
|
||
</div>
|
||
<div class="filter-chips" id="chips">
|
||
<button class="chip active" type="button" data-f="all" onclick="setFilter(this)">Все</button>
|
||
<button class="chip" type="button" data-f="review" onclick="setFilter(this)">Ждут меня</button>
|
||
<button class="chip" type="button" data-f="active" onclick="setFilter(this)">В работе</button>
|
||
<button class="chip" type="button" data-f="done" onclick="setFilter(this)">Готово</button>
|
||
<button class="chip" type="button" data-f="problem" onclick="setFilter(this)">Проблемы</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="toggle-line">
|
||
<label><input type="checkbox" id="showAll" onchange="applyFilters()"> Показать всё, включая удалённые</label>
|
||
</div>
|
||
|
||
<!-- Список карточек -->
|
||
<div id="list">
|
||
{{range .Downloads}}
|
||
<article class="card" data-group="{{.Group}}" data-text="{{.SearchText}}"{{if .Deleted}} data-deleted="1"{{end}}>
|
||
<div class="card-top">
|
||
<div>
|
||
<div class="card-title"><a href="/download/{{.ID}}" style="color:inherit">{{.Title}}</a></div>
|
||
{{if .Infohash}}
|
||
<div class="hash-row">
|
||
<span class="hash">{{.InfohashShort}}</span>
|
||
<button class="copy-btn" type="button" onclick="copyHash(this,'{{.Infohash}}')">⧉ копировать</button>
|
||
</div>
|
||
{{end}}
|
||
</div>
|
||
{{template "status_badge" .State}}
|
||
</div>
|
||
|
||
{{if .IsDownloading}}{{template "progress" .Progress}}{{end}}
|
||
|
||
{{if .Context}}
|
||
<details class="spoiler">
|
||
<summary>контекст</summary>
|
||
<div class="spoiler-body">«{{.Context}}»</div>
|
||
</details>
|
||
{{end}}
|
||
|
||
<div class="card-foot">
|
||
<span class="card-meta">{{if .Note}}{{.Note}}{{else if .Error}}{{.Error}}{{end}}</span>
|
||
<div class="card-acts">
|
||
{{if .Reviewable}}<a class="btn btn-primary btn-sm" href="/review/{{.ID}}">Ревью →</a>{{end}}
|
||
{{if .Undoable}}<form method="post" action="/ui/downloads/{{.ID}}/undo"><button class="btn btn-sm btn-ghost" type="submit">Откатить</button></form>{{end}}
|
||
{{if .Relinkable}}<form method="post" action="/ui/downloads/{{.ID}}/relink"><button class="btn btn-sm" type="submit">Привязать заново</button></form>{{end}}
|
||
{{if .Retriable}}<form method="post" action="/ui/downloads/{{.ID}}/retry"><button class="btn btn-sm" type="submit">Повторить</button></form>{{end}}
|
||
{{if not .Terminal}}<form method="post" action="/ui/downloads/{{.ID}}/cancel"><button class="btn btn-sm btn-danger" type="submit">{{if .Reviewable}}Отклонить{{else}}Отменить{{end}}</button></form>{{end}}
|
||
</div>
|
||
</div>
|
||
</article>
|
||
{{end}}
|
||
</div>
|
||
|
||
<div class="empty-state" id="empty" style="display:none">
|
||
<div class="big">🍿</div>
|
||
Ничего не найдено. Поменяй фильтр или запрос.
|
||
</div>
|
||
{{else}}
|
||
<div class="empty-state">
|
||
<div class="big">🍿</div>
|
||
Пока пусто. Добавь первую загрузку выше.
|
||
</div>
|
||
{{end}}
|
||
|
||
</main>
|
||
</body>
|
||
</html>
|