10 Commits
Author SHA1 Message Date
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 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 4cc4de4269 OpenSpec: архивация трёх параллельных changes + синк спек
Итог параллельной волны фиксов (worktree-изоляция, cherry-pick в master):
- ingest-dedup-integrity (F1, F6) → спека ingest
- retry-stall-basis (MAJOR-1, MAJOR-2) → спека state-reconciliation
- linking-transition-robustness (MAJOR-4, MINOR-7) → спеки file-layout
  и state-reconciliation

Дельты влиты в openspec/specs, changes перенесены в
openspec/changes/archive/2026-07-08-*. Беклог не трогаю (по решению).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-08 17:21:22 +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 e9ec26d09a Ingest: контекст распознавания из полей magnet-ссылки
Синтезируем контекст из полей самой magnet-ссылки (dn, xl, tr/xs, kt) без
сети и дополняем им текст от транспорта: пользовательский текст первым, при
пустом — синтез единственный. Обогащённый контекст идёт только в
download.Context (его читают recognition и веб-UI); вход namer и отображаемое
имя не меняются — строки-факты (Размер:/Трекер:) в display_name не текут.

- magnet.Info: поля ExactLength/Sources/Keywords + Info.Context() (синтез,
  отсев dn-заглушек *-topic-<id>, домен трекера, человекочитаемый размер)
- Parse устойчив к ссылке в процент-кодировке (разовый QueryUnescape)
- ingest: mergeContext → download.Context, namer на сыром req.Context
- Влита дельта capability ingest в openspec/specs, change заархивирован

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 20:38:01 +03:00
avandClaude Opus 4.8 512567c8ba Рефакторинг границ capabilities: цепочка загрузка→матч→ревью→раскладка (openspec)
Привёл набор capabilities в OpenSpec к цепочке обработки, чтобы имя capability
отвечало одному поведению. Чисто по спекам, код и поведение системы не меняются.

Change refactor-capability-boundaries (архивирован):
- recognition разделён на recognition (разбор LLM) + metadata-match (сверка с базами)
- review выделен из web-ui + мигрирован из docs/specs/review-ux.md
- новые capability из docs/specs: file-layout, download-tracking, notifications
- identity очищен до инфра-id; приём (инфохэши, дедуп, ядро приёма) — в ingest
- уведомление о рассинхроне перенесено из state-reconciliation в notifications
- дубль владения путём и безопасного undo оставлен в state-reconciliation

Итог: 11 capabilities, openspec validate --strict проходит (+37/−11 требований).
Источник истины по мигрированным темам переехал в openspec/specs (шапки в docs).
Снят пункт беклога «Пересмотр набора capabilities».

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 21:17:51 +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
av 694a9f4bef Архивация спеки 2026-06-28 12:27:50 +03:00