---
name: zemlya_lotbot_web
description: Лента лотов ТоргиРадара на сайте zbrcheck.ru — бронь + тестовая Robokassa (redirect-only)
metadata: 
  node_type: memory
  type: project
  originSessionId: d0c6c5aa-4d05-42e2-a1f9-034482b88603
---

LotBot вынесен на главную zbrcheck.ru (2026-06-14, Changelog ч.42 / Decision D-lotbot): окно с лентой горячих земельных лотов с торгов + бронь + раскрытие адреса/кадастра/ссылки по оплате. **Только сайт, бота ТоргиРадар не трогали** (схему БД тоже — переиспользуем `booking.try_book`/`mark_paid`).

**Топология:** изолированный read+booking API над БД ТоргиРадара `torgiradar/web/api.py` (FastAPI `127.0.0.1:20152`, launchd `local.lotbot-web-api`, под `~/Services/zemlya-bot/venv`) → BFF zemlya-backend `app/routes/lots.py` (`/api/lots/*`, в antibot-bypass) → виджет `frontend/components/LotFeed.tsx` на `app/page.tsx`. torgiradar-API наружу НЕ торчит.

**Изоляция:** тизер ленты = `anon_card` 1:1 (регион/площадь/ВРИ/категория/цена/рынок/дисконт), фильтр дисконт ∈ [0.35, 0.90]. Адрес/кадастр/ссылка torgi — ТОЛЬКО по `status==paid`.

**Web-юзер:** бронь = владелец `-abs(claims.telegram_id)` (sentinel как OAuth zemlya `telegram_id=-users.id`), не сталкивается с положительными бот-бронями. Чистка тест-броней: `DELETE FROM bookings WHERE telegram_id<0` (положительные = реальные бот-брони, не трогать).

**БД ТоргиРадара перенесена** `/Volumes/Ext` → `~/Services/torgiradar/data/db/torgiradar.db` (launchd без Full Disk Access → `authorization denied` на /Volumes; TCC). plist'ы pipeline+bot держат inline `TORGIRADAR_DB_PATH`, backup-скрипт обновлён. См. [[torgiradar_overview]].

**Robokassa (тест):** InvId-диапазон броней `WEB_BOOKING_OFFSET=3_000_000` (>отчётов 1M, >пакетов 2M). `robokassa.build_booking_payment_url` (IsTest, без Shp_). **Два пути подтверждения (оба активны, mark_paid идемпотентен):** (1) redirect — возврат `/payment/success` → `/api/lots/pay/verify` (гейт `MD5(OutSum:InvId:Password#1)`+владелец+сумма); (2) **webhook-confirm в живом `robokassa_server.py` бота** — ветка InvId≥3M ПЕРВОЙ (до пакетов 2M), `_handle_lot_booking` → `POST :20152/bookings/{id}/confirm` + алерт `get_admin_ids()` (OWNER 1005954706); **fail-safe: любая ошибка → всё равно OK<inv>**, существующие ветки не тронуты. **Demo-confirm из BFF убран** (был дырой).

**Проверено реальной оплатой (Playwright на боевом zbrcheck.ru):** бронь → Оплатить → демо Robokassa (липовый email + СБП) → `Успешно` → `/payment/success` раскрыл кадастр; лог бота `lot booking N PAID` (webhook И redirect сработали). Фаза-1/2 на localhost тоже зелёные. Cookie `zw_session` secure=False; **антибот ловит headless-UA → слать десктоп-Chrome UA + locale ru-RU** (иначе score≥3 → 429 на не-bypass путях типа /api/auth/me). Бот = Python 3.9 (f-строки без бэкслэшей!). Имена паролей бота: `ROBOKASSA_TEST_PASSWORD2` (без подчёркивания перед 2), у web-backend — `ROBOKASSA_TEST_PASSWORD_1/2`.

**Развязка от бота (2026-06-14, ч.43, вариант А):** booking-ветку из живого бота УБРАЛ (бот не подтверждает брони). Подтверждение брони — целиком сайт: (1) redirect `/api/lots/pay/verify`; (2) **поллер** `backend/app/services/lot_payment_poller.py` = отдельный launchd `local.zemlya-lot-poller` (НЕ в uvicorn-воркерах — `--workers 2` плодил бы дубли; standalone+KeepAlive). Каждые 30с: lotbot-API `/bookings/pending-web` → Robokassa `OpStateExt(3M+id)` → confirm. Fail-safe.
**Тумблер test/live сайта:** `backend/app/services/payment_mode.py` свой файл-флаг `data/runtime/live_payments.flag` (независим от бота), `robokassa._effective_test_mode()`. Admin `/api/admin/payment-mode` + UI `/admin/lots` (+ таблица броней через `/api/admin/lot-bookings`→lotbot `/bookings/recent`).
**⚠️ OpStateExt НЕ видит демо-операции** (`Code=3`) → поллер работает ТОЛЬКО в боевом режиме (как поллер бота); в демо подтверждает только redirect. Success-redirect всё ещё через бота (Success URL в кабинете общий) — при падении бота бронь подтвердит поллер, авто-редирект деградирует.
**Лоты в кабинете (2026-06-15, ч.44):** новая страница `/dashboard/lots` (`app/dashboard/lots/page.tsx`) — полная лента за авторизацией (`api.me()` → `!authenticated` → `/login?next=`), переиспользует `LotFeed`. Пункт меню: HubButton «🔥 Горячие лоты» в дашборде + бургер `MobileMenu`. **Главная = чистая витрина:** `LotFeed` имеет режим `presentation` (`app/page.tsx` → `<LotFeed presentation />`) — карточки только с CTA «📌 Забронировать в кабинете» (→`/dashboard/lots`), без кнопки брони/состояний; бронь живёт ТОЛЬКО в кабинете (`<LotFeed />`). **Админ-трекинг «кому ушёл лот»:** `/api/admin/lot-bookings` обогащает бронь `user{user_id,telegram_id,username,full_name,email}`, колонка «Пользователь» в `/admin/lots`. **🐞 фикс брони OAuth/email:** BFF слал `web_user_id=claims.telegram_id` (для OAuth ОТРИЦАТЕЛЬНЫЙ = -users.id) → lotbot guard `must be positive` → 400, бронь для них была сломана. Теперь BFF шлёт `abs(claims.telegram_id)`; админ-маппинг обратно — `users.telegram_id IN (w,-w)`, ключ по `abs(telegram_id)` (реальный TG `+w`, OAuth `-w`). `_own` (`owner==-abs(...)`) консистентен. Проверено Playwright: TG-юзер→`CeBers/uid3`, OAuth(−149)→`uid149`; 38 кнопок «Забронировать» залогиненному, 0 «Войдите».

**Мини-тикер на дашборде (2026-06-15):** `components/LotTicker.tsx` вставлен в `/dashboard` под `#check-picker` — показывает ОДИН лот, авто-ротация 7с + авто-подтяжка свежих 60с (новые id вперёд с бейджем «🆕»), пауза при hover/активной броне; inline-бронь (Забронировать→лок+таймер→Оплатить→payUrl, «← к другим лотам»); хедер пульс «🔥 Новые лоты с торгов» + «Все лоты →»/dashboard/lots, счётчик «лот N из M»+←/→. Чтобы не следить постоянно в пункте лотов. **Грабли (фикс 2026-06-15): не объявлять компонент-обёртку ВНУТРИ рендера** — `Shell` как `const Shell=({children})=>…` давал новый тип на каждый рендер → React перемонтировал поддерево → при брони (тик таймера 1с) тикер моргал. Решение: `shell(children)` обычной функцией + `pull(refresh)` не переставляет ленту во время активной брони.

**Доработки по разбору сценария (2026-06-15, ч.45):** **«Мои брони»** (`MyBookings.tsx` вверху /dashboard/lots) — активные локи (таймер+Оплатить) + оплаченные (раскрыты адрес/кадастр/ссылка); BFF `/api/lots/my-bookings`→lotbot `/bookings/by-user?web_user_id=`. Бронь больше не теряется при перезагрузке. **Лок vs оплата:** pay-url дёргает lotbot `/bookings/{id}/extend` → лок продлевается на `BOOKING_PAYMENT_WINDOW_SECONDS`(1800); `mark_paid` отказывает если лот уже оплачен другой бронью (no-double-pay); лимит `MAX_ACTIVE_LOCKS_PER_USER=5` в try_book (статус `limit`). **UX:** тикер ⏸/▶+пауза по тачу; фильтры/сортировка в ленте (регион/дисконт-чипы/цена); цена брони показана у кнопки; дисконт «−X% к нашей оценке»; свежесть по `newest_lot_ts`(max first_seen_at). Env-флаги отката: `TORGIRADAR_BOOKING_PAYMENT_WINDOW`, `TORGIRADAR_MAX_ACTIVE_LOCKS`. **Отложено (продукт, не код):** пост-оплатный flow success-fee 18%/участие в торгах = заглушка «свяжется менеджер»+админ-алерт.

**Гейт «торги завершились» (2026-06-15):** лот с `bidd_end_time<=МСК-now` исчезает из ленты, бронь→`status=ended`, pay-url→409 «Торги завершились». Сравнение по МСК (`_now_msk()=utcnow()+3ч` в lotbot-API) — `bidd_end_time` хранится как МСК-наивное (offset срезан в `_parse_dt` torgi_poller). null end = не завершён. `get_booking`/`by-user` отдают `auction_ended`; фронт блокирует оплату в «Мои брони». Проверено врем. подменой даты с восстановлением.

**Открыто:** переключение Robokassa в боевой режим — юзером.
