36 Commits
Author SHA1 Message Date
avandClaude Opus 4.8 5948f4d219 Веб-UI: режиссёр в блоке «Распознано как» на странице загрузки
На /download/{id} поле «Режиссёр» было захардкожено прочерком, хотя экран
ревью режиссёра уже выводит: слоистое разрешение полей ярлыка было заперто в
неэкспортируемом worker.effectiveDisplayName. Из-за этого билдеры вью видели
только слой распознавания+матч (rd.Plan.Director) без слоя контекста — то же
на экране ревью.

Вынес разрешение в экспортируемую naming.EffectiveFields(parsedContext, plan)
LabelFields с методом Label(): выбор слоя по сырым значениям (как прежде),
выбранные скаляры возвращаются очищенными (sanitize идемпотентен, display_name
побайтно тот же). effectiveDisplayName стал тонкой обёрткой; страница загрузки
и экран ревью берут режиссёра из той же функции — согласованно с заголовком.

OpenSpec: web-ui (ADDED «Режиссёр в блоке распознавания страницы загрузки»),
review (MODIFIED «Инфо и предпросмотр выбранного источника» — слоистое
разрешение с фолбэком на контекст). Change заархивирован. Беклог: закрыта
rezhisser-v-kartochke-zagruzki.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 21:21:27 +03:00
avandClaude Opus 4.8 17d9f23630 Веб-UI: единый вес кнопок, убрана невидимая btn-ghost из применений
Класс .btn-ghost прозрачен (нет аффорданса без hover, на тач невидим), плюс
веса действий рассогласованы между списком (card.html) и страницей
(download_main.html). Назначаем семантику весов явно:

- «Откатить» = btn-danger везде (в списке был btn-ghost); в список добавлен
  префикс ↩, чтобы список и страница совпадали.
- «Отменить/Отклонить» = btn-danger везде (на странице был btn-ghost).
- «Обновить имя» = нейтральный .btn (был btn-ghost).

Определение .btn-ghost в jellybit.css оставлено на случай легитимного
использования — правились только применения. Косметический nit-фикс, htmx-пути
и деградация без JS не затронуты.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 20:35:14 +03:00
avandClaude Opus 4.8 bd7bf60b33 Веб-UI: перенос длинных значений .kv (фикс горизонтального скролла на мобильном)
Блок «Информация о торренте» на /download/{id} на узком экране выталкивал
страницу за вьюпорт: grid-blowout на `.kv` (grid-template-columns:auto 1fr)
из-за неразрывного 40-символьного infohash — дефолтный min-width:auto грид-
элемента не давал колонке 1fr сжаться уже min-content хэша.

Точечный фикс (вариант «а»): `.kv dd{overflow-wrap:anywhere;min-width:0}` +
`.kv dd.mono{word-break:break-all}`. Правка штатно чинит и потенциально
длинный RecTitle в блоке «Распознано как» (тот же `.kv`). На обычной ширине
вёрстка не меняется.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-17 20:35:14 +03:00
avandClaude Opus 4.8 02d4ecc2aa display_name: слоистое разрешение полей + сохранение режиссёра из контекста
Единый источник полей отображаемого имени и один рендер полного ярлыка на
всех путях (старт и «Обновить имя»/авто-перелив). Раньше старт давал полный
«Название (режиссёр, год). Сезон N» но выбрасывал структуру, а перелив по
распознаванию — усечённый «Title (Year)».

- Слоистое разрешение скаляров имени: override → recognition(+match) →
  новый базовый слой «контекст» (download.parsed_context, JSON naming.Fields).
- naming: публичные Fields/Label/Derive, вынесен единый рендер; удалён
  FormatTitleYear. Сводка сезонов вынесена в recognize.SeasonSummary.
- Режиссёр из метабазы (решение A2): TMDB/TVDB credits через опциональный
  metadata.DirectorProvider; авто-матч кладёт в plan.Director, ручной выбор
  кандидата тянет credits и пиннит ovrDirector. Метабаза бьёт контекст.
- refreshDisplayNameLocked строит полный ярлык из эффективных полей;
  инфо-панель ревью показывает загруженного режиссёра.
- Миграция 0011_parsed_context + ER-схема. Всё косметика: на пути/раскладку
  не влияет, приём/вывод имени не валятся (best-effort).

Закрывает беклог-задачу «Кнопка „Обновить имя“: полный формат ярлыка».
OpenSpec: archive/2026-07-11-field-resolution-display-name (ingest,
recognition, metadata-match, review).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 11:50:08 +03:00
avandClaude Opus 4.8 3f80d36727 Веб-UI: правило .type-ico для значка типа в карточке (ревью-nit)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 09:35:32 +03:00
av 49747e1281 Слияние WT-3: oob-обновление панели действий ревью 2026-07-11 09:32:53 +03:00
av 3465c1ea62 Слияние WT-2: список загрузок и форма приёма (cancelled hide, значок типа, ellipsis) 2026-07-11 09:32:52 +03:00
avandClaude Opus 4.8 44c5952e49 Список: скрытие cancelled, значок типа, ellipsis имени .torrent
Три мелкие правки веб-UI списка загрузок:

- Скрываем cancelled в общем списке наравне с deleted: под группой all
  без IncludeDeleted теперь `state NOT IN (deleted, cancelled)`. reverted
  остаётся видимым. Тумблер и комментарии-инварианты приведены в
  соответствие («включая отменённые и удалённые»).
- Значок типа (🎬 фильм / 📺 сериал) в строке списка: media_type текущей
  попытки распознавания протянут через LEFT JOIN в ListDownloadsPage
  (Download.RecMediaType, симметрично RecTitle) и downloadView.MediaType;
  значок рендерится в card.html, при нераспознанном типе значка нет.
- Ellipsis для длинного имени .torrent-файла: у .btn-file max-width +
  overflow/ellipsis на лейбле, чтобы длинное имя не распирало .add-row;
  показываем базовое имя без .torrent, полное — в title (JS-энхансмент).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 09:31:27 +03:00
avandClaude Opus 4.8 847d471852 Ревью: oob-обновление панели действий при свопе источника
Кнопка «Применить» (завязана на HasLinks) лежит вне #source-block и не
обновлялась при htmx-свопе выбора источника — в краевом случае (пустой
предпросмотр из-за коллизии) рассинхронивалась с превью до полной
перезагрузки. Выделил панель в партиал review_action_bar с id=action-bar;
reviewBlockAction теперь рендерит review_source_swap — свежий #source-block
плюс oob-копию панели (hx-swap-oob), так кнопка синхронно отражает HasLinks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 09:28:45 +03:00
avandClaude Opus 4.8 2545d64e8b Веб-UI: фавиконка (SVG) для вкладки/PWA
SVG-иконка (jelly-фиолетовый бейдж с play-треугольником и «битами»),
подключена во все три страничных шаблона через asset-хелпер; статика
уже раздаётся и embed берёт web/static целиком.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-11 09:25:50 +03:00
avandClaude Opus 4.8 1369a9cabe Приём: дедуп по target_missing/orphaned + стоп-кран «Закрыть»
Два дубля-близнеца на один инфохэш рождались, когда повторный приём
попадал на запись в target_missing: дедуп искал только активную задачу,
а target_missing терминален → заводилась новая загрузка, воркер усыновлял
уже присутствующий торрент и раскладывал его.

- Приём: критерий дедупа расширен до «блокирующей повторный приём» =
  активные ∪ {target_missing, orphaned}. Повторный приём такого инфохэша
  привязывается к существующей записи (спящей, без обращения к qBittorrent),
  а не плодит близнеца. Прочие терминальные (done/cancelled/failed/reverted/
  deleted) повторный приём не блокируют — осознанная свежая попытка. Новый
  read-метод FindReingestBlockingByInfohash (приоритет активной над desync);
  общий active-гард не тронут.
- Команда «Закрыть» (Dismiss) — универсальный стоп-кран из любого состояния,
  кроме deleted → cancelled (error_code=user_dismiss). Только меняет статус:
  файлы (в т.ч. хардлинки done/orphaned) и раздачу qBittorrent не трогает,
  в отличие от «Удалить». Веб — danger-зона внизу страницы; Telegram —
  кнопка с подтверждением; из cancelled — идемпотентный no-op.
- Транспорты при дедупе на desync-запись сообщают адресно (target_missing —
  привязать заново/закрыть; orphaned — закрыть и добавить заново); веб при
  дедупе ведёт на страницу существующей записи.

Спеки: ingest (дедуп), state-reconciliation (стоп-кран); граф переходов
допополнен рёбрами <терминал>→cancelled. OpenSpec change
dedup-target-missing-and-dismiss заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 20:15:37 +03:00
avandClaude Opus 4.8 0c9421f4c1 Имя: восстановление display_name после распознавания + гард пустого входа
Голый magnet без dn/контекста заставлял namer звать LLM на пустом входе,
и модель галлюцинировала мусорное имя («Unknown»), которое писалось и в
display_name, и в rename qBittorrent, а заодно ломало UI-фолбэк на
распознанное название. Верное каноническое имя, вычисляемое позже при
распознавании, никуда не переливалось.

- naming: гард пустого входа в DeriveName (нет контекста и подсказки → ""
  без вызова LLM) + детерминированный форматтер FormatTitleYear.
- qbt: операция RenameTorrent (переименование существующей раздачи).
- store: SetDisplayName — обновление имени постфактум без гарда состояния.
- worker: refreshDisplayNameLocked/RefreshDisplayName — перелив канонического
  имени (эффективный план) в display_name + best-effort rename раздачи по
  реальному t.Hash; авто-триггер при подтверждении матча (choose/manual add).
- web-ui: кнопка «Обновить имя» на странице загрузки (htmx-своп заголовка,
  деградация без JS), видимая при наличии распознавания (вкл. done/orphaned).

Спека: дельты ingest/review/web-ui влиты в openspec/specs; change
refresh-display-name заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 16:51:04 +03:00
avandClaude Opus 4.8 3f1a928000 Единое окно: полное пользовательское удаление загрузки (delete)
Вторая половина «единого окна»: команда «Удалить» снимает наши библиотечные
хардлинки (гард последней копии осознанно выключен, в отличие от Undo) и сносит
раздачу с файлами из qBittorrent (deleteFiles=true) → терминальный deleted.
Доступна из done/orphaned/target_missing, идемпотентна к отсутствующей стороне;
инициатор различается через error_code=user_delete. Подтверждение обязательно:
веб — danger-секция внизу страницы (hx-confirm + details), Telegram — двухшаговый
inline-confirm. qbt.Delete + layout.Remove (unlink без ErrLastCopy, только свои
ссылки под movies/series). Граф переходов не менялся — рёбра уже были.

OpenSpec: state-reconciliation +1 требование; синк workflow.md; беклог закрыт.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 12:17:51 +03:00
avandClaude Opus 4.8 14d615a7c2 Приём: добавление загрузки по .torrent-файлу
Принимаем .torrent как загруженные байты — через файл-пикер в веб-форме и
Telegram-документ, наряду с magnet. Файл несёт полные метаданные: работает
там, где magnet не резолвится (закрытые трекеры, без DHT), и даёт максимум
контекста для распознавания без сети.

- internal/torrent: парсер поверх anacrolix/torrent/metainfo — инфохэш(и)
  (v1 SHA1 исходных байтов info; v2 BEP52 при наличии) + Context() из имени,
  дерева файлов, размера, трекеров. Извлечение файлов панико-безопасно
  (недоверенный вход).
- Персистентность байтов: таблица-спутник download_torrent (миграция 0009);
  пишется в транзакции создания загрузки, только на ветке создания (не при
  дедупе). Байты живут весь срок строки — нужны для повторного добавления
  при retry.
- ingest: Request.TorrentData/TorrentName, диспетч парсера; source_ref —
  человекочитаемый референс (имя раздачи/файла), не адрес добавления.
- worker: общий sourceAddParts ветвит по source_type в ОБОИХ add-путях —
  processCatched и Retry (torrent добавляется файлом, не magnet-хешем).
- Транспорты: multipart-форма с файл-пикером (деградация без JS) и приём
  Telegram-документа (скачивание с редактированием токена из ошибок — секрет
  не в логи; обработка до ветки pending/текста).

Разработка по OpenSpec (SDD): change torrent-file-ingest, два чекпоинта ревью
(дизайн до кода, код до архива) сабагентами; дельты влиты в спеки, change
архивирован. Ручная проверка на живом qBittorrent (7.3) — за деплоем.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 10:52:31 +03:00
avandClaude Opus 4.8 0d263270cb Быстрый приём: сохранение в catched, добавление в qBittorrent — шаг worker'а
Приём (Ingest) стал быстрым: синхронно только парс magnet, синтез контекста из
полей ссылки, атомарный дедуп и запись загрузки в новое состояние `catched` —
ответ клиенту сразу. Медленный вывод имени (LLM) и добавление в qBittorrent
вынесены в асинхронный шаг машины состояний, который двигает worker.

- store: состояние `catched` (нетерминальное, активная группа); атомарный
  переход PromoteCatched (catched → downloading + display_name) с гардом
  state='catched' (ре-валидация после сетевых вызовов вне блокировки)
- ingest: убраны namer/qbt из пути приёма; пишем `catched`, отвечаем сразу
- worker.processCatched: вне w.mu выводит имя и qbt.Add, под w.mu — короткий
  переход; сбой add оставляет catched (ретрай тиком); предохранитель
  catch_timeout → failed(qbit_add)+notify; catched исключён из проверок пропажи
- config: worker.catch_timeout (дефолт 10m)
- веб-UI: бейдж catched, активная группа, самозавершающийся htmx-поллинг
  карточки/страницы до перехода в downloading; Telegram-текст без сырого catched
- OpenSpec: дельты ingest/download-tracking/web-ui влиты в спеки, change
  заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 21:29:28 +03:00
avandClaude Opus 4.8 2f8e6e3576 Веб-UI: htmx-своп действий на месте вместо PRG-редиректа
Мутирующие действия больше не уводят со страницы: undo/relink/retry/cancel
в списке свопят карточку (#card-{id}), на /download/{id} — #download-main;
петля ревью (refine/rerecognize) свопит #review-main и допалливает
recognizing до готового плана (fragment /fragments/downloads/{id}/review,
every 2s). Выходы ревью (apply/defer/cancel) остаются навигацией. Без htmx —
прежний PRG-редирект (деградация). Ошибка действия на htmx-пути — HTTP 200
с сообщением в фрагменте (ActionError), иначе htmx не свопит DOM.

Разметка вынесена в партиалы card/download_main/review_main (корень = элемент
с целевым id), различение поверхности — скрытым полем surface=list|download.
Извлечены buildCardView/buildDownloadView. handleSetProvider переведён на
reviewBlockAction (консистентность source-действий).

Реализация change htmx-action-swap (OpenSpec).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 14:37:51 +03:00
avandClaude Opus 4.8 280204db18 Правки: дата на стр. загрузки, ссылка TVDB по типу, заголовки в Telegram
- Веб-UI: под шапкой страницы загрузки — дата добавления и относительная
  давность («N дней назад»), как в карточках списка.
- Баг: ссылка-dereferrer TVDB для фильма вела на /series/; теперь строится
  по типу запроса (/movie/ либо /series/). Тест + правка спеки metadata-match.
- Telegram: заголовки уведомлений (готово/ошибка/рассинхрон) берутся из
  display_name — консистентно с веб-UI; сезон подтягивается автоматически.
- Telegram: в сообщениях об ошибке — и заголовок, и #id загрузки (ULID)
  для быстрого поиска по логам.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 11:50:04 +03:00
avandClaude Opus 4.8 bb245a90a3 Веб-UI: обзор жизненного цикла в карточке загрузки
Карточка списка на главной теперь даёт краткий обзор «от загрузки до
решения об удалении»: метка «ID:» перед идентификатором, дата добавления
(абсолютная + относительная, всегда), размер раздачи и рейтинг отдачи.
Спойлер контекста убран — контекст смотрят на /download/{id}.

Данные:
- рейтинг и общий размер — из живого снимка воркера (qbt total_size →
  worker.Live.TotalSize); размер доступен для любой раздачи в снимке;
- размер-фолбэк, когда торрента нет в qBittorrent (orphaned) — сумма
  размеров разложенных файлов: новая колонка file_link.size, layouter
  пишет размер при линковке, ридер LayoutSizeByDownload суммирует по
  странице одним запросом (дедуп по dst_path);
- дата — source_added_at → фолбэк created_at, показ в TZ сервера.

handleIndex читает снимок для всех карточек (map-lookup), рейтинг/размер
статичны на рендере (без поллинга). Миграция 0007, ER-схема обновлена.
Change download-card-lifecycle-overview влит в спеки и заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 10:34:45 +03:00
avandClaude Opus 4.8 9ed732e49e Веб-UI: основной идентификатор карточек — download.id вместо infohash
В карточке списка и шапке /download/{id} показываем и копируем download.id
(ULID) — тот же ключ, что в логах (download_id), удобно грепать. Infohash
остаётся в блоке «Информация о торренте». Поиск по списку расширен: матчит
любой идентификатор (download.id ИЛИ infohash), плюс название/контекст.
Удалены осиротевшие поля Infohash/InfohashShort и хелпер shortenHash.

Дельта web-ui влита в спеки, change заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 09:47:27 +03:00
avandClaude Opus 4.8 80e725eb9a Просмотр: ориг. название обычным шрифтом, не mono
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 08:29:29 +03:00
avandClaude Opus 4.8 1387afe94f Просмотр: блок «Распознано как» — таблица поле→значение, +сезон/режиссёр
Блок распознавания на /download/{id} переведён из карточки с постером в
единую таблицу kv (поле слева, значение справа) — убирает неоднозначность
трёх одинаковых названий. Поля: название, ориг. название, тип, сезон
(для сериала), год, режиссёр (зарезервировано «—»), база, уверенность.
Мёртвый CSS карточки/постера удалён.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 08:26:43 +03:00
avandClaude Opus 4.8 1f4267a046 Ревью: единый блок выбора источника, клик = выбор (review-unified-source-block)
Три секции экрана ревью (Догадка/Источник/Раскладка) слиты в один блок:
список вариантов (радио) → инфо о выбранном → предпросмотр раскладки. Клик
по варианту сразу выбирает и сохраняет источник и обновляет инфо+раскладку
частичным htmx-свопом блока, без полной перезагрузки и без кнопки «выбрать».

- httpapi: reviewBlockAction (htmx-aware, детект HX-Request) для
  candidate/nobase/source; вынос buildReviewView; поля SeasonSummary и
  BlockError; сводка сезонов (seasonSummary/seasonRanges)
- тип movie↔series убран из UI (read-only); удалён веб-роут /type и
  handleSetType, метод SetType из интерфейса httpapi (worker/Telegram не тронуты)
- шаблон: партиал review_source_block, ссылка «запись ↗» вне кликабельного
  label, фокус радио с клавиатуры; чистка мёртвого sourceView.Files/IsSeries
- тесты: htmx-своп выбора, htmx-путь ошибки, юнит-тесты сводки сезонов
- openspec: спеки review/web-ui синхронизированы, change заархивирован
- беклог: сложные сериальные раздачи; oob-обновление панели действий

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 08:12:28 +03:00
avandClaude Opus 4.8 aac3ced262 Реализовал выбор источника и предпросмотр в ревью (review-source-selection)
Экран ревью теперь показывает единый список источников совпадения: строка
«распознано нейронкой» наравне с кандидатами баз; выбор/переключение/снятие
в пользу нейронки; ручное добавление по id или URL (TMDB/IMDb — по URL,
TVDB — по числовому id); предпросмотр полей и целевых путей каждого источника
до применения (место под режиссёра зарезервировано). «Раскладка» осталась
отдельной секцией для активного источника, инлайн-превью неактивных — по клику.

Ядро: единая деривация «источник → overrides» (sourcePins), общая для превью
и коммита → preview == apply; заодно чинит латентный залипший override
title/year при переключении источника. Превью считается эфемерно, без записи
в БД; пользовательский URL только парсится (SSRF нет).

Ревью дизайна и кода пройдены; правки ревьюеров учтены (сообщение об ошибке
ручного ввода доходит до пользователя, URL без схемы принимается).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 10:29:27 +03:00
avandClaude Fable 5 37f2f6481a Идентичность на ULID: download_infohash, guarded-дедуп, миграция (ulid-identity)
Все сущности переехали с INTEGER AUTOINCREMENT на TEXT ULID (lowercase,
internal/ident — единая точка генерации и разбора; oklog/ulid). Инфохэши
загрузки — множество (download_infohash, v1/v2 гибридных торрентов): дедуп
и сопоставление в поллинге по любому из хешей, magnet-парсер отдаёт оба
хеша гибридной ссылки, усечённый v2-хеш v2-only раздач не хранится.

Инвариант «не более одной активной загрузки на infohash» вместо снятого
unique-индекса держат guarded-методы store в одной write-транзакции
(_txlock=immediate): CreateDownloadIfNoActive (приём/adopt, с доносом
недостающих хешей), ActivateIfNoOtherActive (retry/recovery/relink, отказ
до побочных эффектов), guarded AddInfohashes; SetDownloadState отклоняет
терминал→активное как механический бэкстоп.

Миграция 0006 — первая Go-миграция goose: пересоздание таблиц при
включённых FK, backfill ULID с timestamp из created_at (хронология id
сохранена), разнос infohash, удаление idempotency_key. BREAKING: формат id
в URL/логах/Telegram, REST-поля id (string) и infohashes (список).

Новая конвенция docs/conventions/database.md (без числовых PK), корреляция
в логах grep'ом по голому ULID, ER-схема обновлена. Спеки: новая capability
identity, MODIFIED в state-reconciliation; change заархивирован. Пройдены
ревью дизайна и кода (по 8 углов), все находки исправлены с
регрессионными тестами.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:25:00 +03:00
avandClaude Opus 4.8 c0b5ab7295 UI/UX списка и карточки загрузки: серверные фильтр/поиск/пагинация, матч-ссылка, имя раздачи (web-ui-list-detail)
- Список: серверные фильтр по группе состояний, поиск и пагинация (GET
  f/q/page/all, по 25), сортировка по времени добавления в qBittorrent
  (added_on) с фолбеком на created_at и tie-break по id.
- Заголовок загрузки = имя раздачи (display_name) → распознанное название →
  усечённый источник; сырой magnet вынесен в блок «Информация о торренте».
- Матч метабазы показан ссылкой на запись (страница загрузки и ревью);
  URL берётся у выбранного кандидата либо строится по provider+id и типу.
- Полировка вёрстки; клиентская JS-фильтрация убрана (всё серверное, без JS).
- Миграция 0005 (display_name, source_added_at); воркер однократно
  фиксирует source_added_at при поллинге/усыновлении; ER-схема обновлена.
- OpenSpec: дельты влиты в specs/{web-ui,ingest}, change заархивирован.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 09:50:24 +03:00
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
avandClaude Opus 4.8 4e2593ac31 Перенос дизайна в 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>
2026-06-30 19:59:19 +03:00
avandClaude Opus 4.8 70d8758646 Восстановление зависших загрузок и уведомления о падении (state-reconciliation)
Долгий metaDL больше не убивается агрессивным таймаутом: дефолт
magnet_timeout 30m → 24h (страховочный предохранитель), базис отсчёта —
added_on из qBittorrent, а не created_at (переживает retry/усыновление).

Авто-восстановление: фоновая сверка возвращает в поток задачи, упавшие по
нашей нетерпеливости (magnet_timeout/stalled), когда источник ожил и
продвинулся за условие падения (downloading/completed по статусу торрента);
qbit_error не воскрешается. Конфликт idempotency (infohash занят другой
активной задачей) — оставляем в failed.

Уведомления: любой переход в failed/stuck пингует автора (включая приёмный
qbit_add через ingest), с дебаунсом против спама при флаппинге stalled.
Ручной retry добавлен в веб-UI и Telegram; Retry перецепляется к живому
торренту вместо слепого Add.

Дельта state-reconciliation влита в живые спеки; обновлён workflow.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 14:51:57 +03:00
avandClaude dfa182a5a9 Ссылки на внешние базы в ревью (recognition)
Каждый кандидат внешней базы метаданных (TMDB/TVDB/TVMaze) теперь несёт
URL на страницу элемента — при ревью можно кликнуть и проверить матч.
URL формируется клиентом провайдера при поиске, сохраняется в БД
(metadata_candidate.url) и отображается ссылкой в веб-интерфейсе.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-29 20:09:20 +03:00
avandClaude Opus 4.8 cc7e51b3a4 Обработка рассинхрона состояния с реальностью (state-reconciliation)
Распознаём ручное удаление источника (раздача в qBittorrent) и/или цели
(разложенные хардлинки) и отражаем его в состоянии задачи, без автодействий.

- Новая capability state-reconciliation (OpenSpec): фоновая сверка по матрице
  «источник × цель» → состояния target_missing/orphaned/deleted, переходы и
  самовосстановление (healing).
- worker: reconcileDesync в Poll (только разложенные/desync-задачи), дебаунс
  пропажи источника (порог [worker].source_missing_threshold) и синхронный
  preflight перед действиями (relink/recognize/apply/undo) — не доверяем
  state в БД.
- layout.Undo: отказ снять последнюю копию (nlink<=1 или нет источника),
  отказ всего батча без частичного отката (ErrLastCopy).
- store: единый список terminalStates для IsTerminal и FindActiveByInfohash
  (иначе семантика «активности» разъезжается), столбец source_miss_count,
  миграция 0003.
- httpapi/web и Telegram: показ новых состояний и уведомления о рассинхроне.
- Доки: workflow.md, jellyfin-layout.md, database.md (+0003), config.

Change заархивирован в openspec/changes/archive, дельта влита в openspec/specs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 11:07:09 +03:00
av e297f0fb84 Фикс повторного распознавания 2026-06-15 07:57:22 +03:00
av 16a82572e7 Добавил ручную перепривязку 2026-06-15 07:42:50 +03:00
av 7f7f5f69d4 Добавил выбор из кандидатов, если LLM не уверена в раскладке 2026-06-14 16:43:50 +03:00
av 5087f35861 Добавил поиск метаданных по каталогам 2026-06-14 15:21:01 +03:00
av 9c1b178e46 Раскладка файлов после распознавния 2026-06-14 14:53:40 +03:00
av 883148787a Реализация, фаза 1: добавление данных в qbittorrent 2026-06-14 12:10:48 +03:00