Files
Deal/.superpowers/sdd/channel-discovery/final-fix-report.md
T
Rustam Khalimov 27c7831910
ci / build-test (push) Canceled after 0s
Deal — единая кодовая база
SaaS-мониторинг Telegram: ядро (модули Cards/Kanban/Pipeline/Tenants/Settings/
Discovery, Api, Infrastructure), сервисы telegram/ai/ml/storage, фронт Vue,
контракты и grpc-hosting, деплой-конфиги (dev/prod/observability/CI-раннер),
Gitea Actions CI, документация (ТЗ, техдок, api-map, код-стайл, планы, бэклог).

Текущее состояние: все этапы роадмапа 0–12 закрыты, сборка 5 sln 0/0,
тесты 1340/130/52/38/9 зелёные.
2026-09-11 23:56:47 +03:00

8.0 KiB
Raw Blame History

Discovery — финальная волна правок: отчёт

Статус: ГОТОВО — все findings закрыты, проверки (компиляция, сборки, смоук) зелёные.

Что исправлено

Finding Файлы Суть
I1 + M1 + m2 (авто-join) db.py, discovery.py, discovery_worker.py Колонка disc_candidates.join_failures_SCHEMA + _MIGRATIONS), joinFailures во view кандидата. _join_step: после wait_join_delay() кандидат перечитывается (SELECT по dialog_id) и join выполняется только если запись есть, status='review', задача running+autoJoin, _we_are_in() False — иначе выход без join. Ошибка join (не flood): join_failures += 1; на 3-й неудаче delete_candidate + лог skip «не удалось вступить (3 попытки): {err}». FloodWaitError — как было (note_flood + лог flood, кандидат остаётся review). После успеха: mark_joinedadd_dialog_monitoredawait tg.backfill_dialog(dialog_id)remove_blacklist.
I2 (flood/ошибки поиска) discovery_worker.py tg.discovery_search обёрнут в try/except: FloodWaitError → note_flood() + лог flood + return; прочие Exception → лог error «поиск «{keyword}»: {err}» + return; advance_search только после успешного поиска.
I3 (backfill после join) discovery_worker.py, discovery_routes.py В обоих местах join: await tg.backfill_dialog(dialog_id) после add_dialog_monitored (best-effort: исключение не роняет шаг/API — log.warning).
I4 (отсев «чатов») discovery_worker.py В _search_step результаты с kind='чат' (люди/личные чаты/боты) пропускаются: continue + лог skip «{name}: личный чат/бот»; каналы/группы/форумы — как раньше.
M4/m1 (идемпотентность reject + ЧС) discovery.py, discovery_worker.py, discovery_routes.py mark_rejected: ранний return при status='rejected' (счётчик/лог/чёрный список не трогаются). После успешного join (worker и routes) — discovery.remove_blacklist(dialog_id).
M3 (фронт-чистота) store.js, DiscoveryView.vue Удалён мёртвый state.discCandidateStatus (проверено: читается только в docs; вью использует локальный activeTab) вместе с записями в loadDiscCandidates/resetLocal. resetLocal сбрасывает discCountsdiscQuota). Квоты переехали в store: state.discQuota {limit,delayMin,delayMax,paused} + loadDiscQuota() (GET /api/settings), saveDiscQuota(patch) (PATCH), toggleDiscPaused(); прямой import { api, ApiError } из вью убран.
M5 (инвариант пауз) settings_routes.py patch_settings: если пришли оба discJoinDelayMin/discJoinDelayMax — после клампов (5..600), при min>max значения меняются местами (как делает фронт).
m6 (потеря имени) discovery_worker.py _eval_step: name/username обновляются только при успешном резолве discovery_info (resolved); fallback (name=dialog_id) больше не затирает имя кандидата.
Синхронизация дизайн-дока docs/superpowers/specs/2026-09-04-channel-discovery-design.md §9: у кандидата lang_ru (не lang_detected), добавлен join_failures; topics = {topicId,title,fitCount,total,fitRatio,passed}; нет транзитного статуса evaluated (счётчик на задаче).

Проверки (команды и вывод)

  1. Компиляция бэкенда:
$ cd /c/telbase && python -m py_compile backend/app/services/discovery_worker.py \
    backend/app/services/discovery.py backend/app/routers/discovery_routes.py \
    backend/app/db.py backend/app/routers/settings_routes.py
PY_COMPILE_OK
  1. Сборка образа (обязательно после правок бэкенда/db.py):
$ docker compose build app
[+] build 1/1
 ✔ Image telbase-app Built   6.4s

(внутри образа повторно собран и фронтенд: vite build → 42 modules transformed, ok) 3. Сборка фронтенда:

$ cd /c/telbase/frontend && npm run build
vite v6.4.3 building for production...
✓ 42 modules transformed.
✓ built in 1.40s
  1. Смоук на временной БД в контейнере (все Telegram-вызовы замоканы):
$ MSYS_NO_PATHCONV=1 docker compose run --rm --no-deps -e PYTHONPATH=/srv \
    -e LEADRADAR_DATA=/tmp/lr_fix --entrypoint python app /data/lr_fix_smoke.py
A_REJECT_IDEMPOTENT_OK      # mark_rejected повторно: rejected=1, 1 reject-лог, 1 запись ЧС
B_JOIN_FAILURES_OK          # join_failures 1→2, на 3-й неудаче кандидат удалён + skip «3 попытки»
B_JOIN_RECHECK_OK           # задача на паузе во время delay → join не вызывается (action none)
C_SEARCH_ERROR_TICK_OK      # ошибка поиска: tick жив (action error), searchIdx не двигается
D_AUTOJOIN_OK               # успешный авто-join: joined/auto_joined, dialogs(monitor), backfill вызван
LR_FIX_SMOKE_OK

Временный скрипт data/lr_fix_smoke.py удалён после прогона; progress.md не трогался.

Concerns

  1. Ветка flood-ошибки поиска и реальные сетевые ошибки join офлайн не воспроизводятся (нужен живой аккаунт); покрыты код-ревью и логикой (в tg.discovery_join flood фиксируется и пробрасывается, worker дублирует note_flood идемпотентно).
  2. При отсеве «личный чат/бот» пишется skip-лог на каждого человека в выдаче поиска — история задачи может стать многословной на «широких» ключах (по ТЗ-рекомендации инструкции; при желании можно убрать лог, оставив continue).
  3. _join_step при нарушении условий re-check после паузы выходит молча ({"action":"none"}) без лога — намеренно (ситуация штатная: задача поставлена на паузу/кандидат отклонён человеком). Ошибки же join всегда логируются.
  4. В db.py и settings_routes.py остались предсуществующие замечания линтера, не связанные с этой волной: db.py — сортировка импортов (L7), try/except-pass и «голый» Exception в нетронутых местах Store (миграции/close/all_settings); settings_routes.py — неиспользуемые импорты json/HTTPException/tg и неиспользуемая provider_id в ai_check. Новые правки этих замечаний не добавляют (проверено: файлы воркера/сервиса/роута discovery чисты).
  5. join_failures не сбрасывается при переводе кандидата обратно в review после удачной паузы — не требуется: при повторном добавлении (после rejected) запись создаётся заново с DEFAULT 0, а успешный join переводит кандидата в joined.