diff --git a/README.md b/README.md index 88cc33b..0e8ea34 100644 --- a/README.md +++ b/README.md @@ -1,143 +1,40 @@ -# DLD Monitor +# monitoring-pf -Внутренний инструмент для агентства недвижимости в Дубае: мониторит цены объявлений конкурентов на **PropertyFinder.ae** и **Bayut.com** по DLD Permit Number, шлёт уведомления в Telegram при: +Сервис мониторинга объявлений PropertyFinder/Bayut по DLD Permit Number для +портала. Он хранит проекты, конкурирующие объявления и историю цен, а UI +публикуется через portal по `/monitoring-pf`. -- 📈📉 изменении цены конкурента, -- ❌ удалении объявления (404 / withdrawn), -- 🆕 появлении нового объявления с тем же permit (новый брокер выставил ту же квартиру). +## Назначение -## Архитектура +- отслеживать изменение цены конкурента; +- фиксировать удаление/withdrawn объявлений; +- находить новые объявления с тем же DLD Permit Number; +- уведомлять ответственных сотрудников через Telegram. -``` -┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ -│ Web UI │ │ Scheduler │ │ Telegram Bot │ -│ (FastAPI) │ │ (APScheduler) │ │ (polling) │ -│ add project │ │ every N hours │ │ /start /check │ -└──────┬───────┘ └────────┬─────────┘ └────────┬────────┘ - │ │ │ - └──────────────┬──────┴───────────────────────┘ - ▼ - ┌────────────────┐ - │ monitor │ - │ service │ ← скрапит PF и Bayut, пишет в SQLite - └────────────────┘ - │ - ▼ уведомления в TG конкретному employee +## Развёртывание + +Сервис рассчитан на запуск внутри портала/k8s. Манифесты лежат в `k8s/`. +Перед применением заполните секреты в `k8s/secrets.yaml`. + +```bash +kubectl apply -k k8s ``` -## Локальный запуск (Windows) - -### 1. Создать виртуальное окружение и поставить зависимости - -```powershell -python -m venv .venv -.\.venv\Scripts\Activate.ps1 -pip install -r requirements.txt -``` - -### 2. Получить токен Telegram-бота - -Уже есть? Отлично. Если нет: -1. Откройте Telegram, найдите [@BotFather](https://t.me/BotFather). -2. Отправьте `/newbot`, придумайте имя. -3. Скопируйте токен вида `123456:ABC-DEF…`. - -### 3. Создать `.env` - -```powershell -Copy-Item .env.example .env -notepad .env -``` - -В `.env` вставьте: - -``` -TG_BOT_TOKEN=ваш_токен_от_botfather -SCRAPE_INTERVAL_HOURS=4 -ADMIN_CHAT_ID= # опционально — куда слать системные ошибки -``` - -### 4. Запустить три процесса (в **трёх** разных окнах PowerShell) - -**Окно 1 — веб-интерфейс:** -```powershell -.\.venv\Scripts\Activate.ps1 -python run_web.py -``` -Откройте http://127.0.0.1:8000 - -**Окно 2 — Telegram-бот:** -```powershell -.\.venv\Scripts\Activate.ps1 -python -m app.bot -``` - -**Окно 3 — фоновый сканер:** -```powershell -.\.venv\Scripts\Activate.ps1 -python -m app.scheduler -``` - -## Первое использование - -1. Откройте бота в Telegram и отправьте `/start` — он зарегистрирует ваш `chat_id`. -2. В веб-UI перейдите в **Сотрудники** — убедитесь, что вы там есть с ✓ TG. -3. Нажмите **+ Новый проект**, заполните: - - Название (например: «Marina Pinnacle 1502, 2BR») - - DLD Permit Number (Trakheesi) - - Тип сделки (продажа/аренда) - - Владелец = вы -4. На странице проекта нажмите **Проверить сейчас** — система найдёт все объявления конкурентов с этим permit на PF и Bayut. -5. Дальше фоновый сканер сам будет проверять каждые `SCRAPE_INTERVAL_HOURS` часов и слать уведомления в Telegram. - -## Команды бота - -- `/start` — подключить себя как сотрудника (запоминает chat_id) -- `/list` — список ваших проектов -- `/check` — запустить проверку всех ваших проектов сейчас -- `/whoami` — показать свой chat_id +Standalone-скрипты локального Windows-запуска и compose-обвязка удалены, чтобы +проект не дублировал инфраструктуру портала. ## Структура -``` +```text app/ -├── config.py настройки из .env -├── db.py SQLAlchemy engine + session +├── config.py настройки окружения +├── db.py SQLAlchemy engine/session ├── models.py Employee, Project, CompetitorListing, PriceHistory ├── web.py FastAPI роуты и UI ├── bot.py Telegram-бот -├── scheduler.py APScheduler фоновый сканер -├── scrapers/ -│ ├── base.py httpx + парсинг __NEXT_DATA__ -│ ├── propertyfinder.py -│ └── bayut.py -├── services/ -│ ├── monitor.py детект изменений, основная бизнес-логика -│ └── notifier.py отправка в TG -└── templates/ Jinja2 (Bootstrap 5) -data/ -└── monitor.db SQLite (создаётся автоматически) +├── scheduler.py фоновый сканер +├── scrapers/ PropertyFinder/Bayut парсеры +├── services/ бизнес-логика и уведомления +└── templates/ Jinja2 UI +k8s/ манифесты для портала ``` - -## Перенос на сервер - -Когда придёт время — нужно: -1. Поставить Python 3.11+ на сервер (Linux). -2. Скопировать репозиторий, `pip install -r requirements.txt`. -3. Создать `.env`. -4. Поставить три процесса под `systemd`: - - `dld-monitor-web.service` → `python run_web.py` - - `dld-monitor-bot.service` → `python -m app.bot` - - `dld-monitor-scheduler.service` → `python -m app.scheduler` -5. Поставить nginx + TLS перед веб-портом (8000). - -## Возможные проблемы скрапинга - -PF/Bayut могут начать блокировать запросы при частых обращениях. Признаки: -- В логах сканера видны `Blocked by site (403/429)`. -- Поиск возвращает 0 объявлений, хотя они должны быть. - -Что делать: -1. Уменьшите `SCRAPE_INTERVAL_HOURS` (реже = меньше риска). -2. Если не помогает — добавьте Playwright (headless-браузер). Заготовка: `pip install playwright && playwright install chromium`, затем заменить `fetch_html` на запуск через Playwright. -3. Опционально — прокси. diff --git a/SESSION_NOTES.md b/SESSION_NOTES.md deleted file mode 100644 index 5564646..0000000 --- a/SESSION_NOTES.md +++ /dev/null @@ -1,90 +0,0 @@ -# Журнал сессии (2026-05-25) - -## Что построили - -Внутренний мониторинг цен конкурентов на propertyfinder.ae и bayut.com для HOME LIGA REAL ESTATE. - -**Стек:** FastAPI + Jinja2 (Bootstrap 5) + SQLAlchemy/SQLite + APScheduler + python-telegram-bot + httpx/BS4. - -**Три процесса** (каждый — свой .bat-лаунчер): -- `run_web.bat` — веб-UI на http://127.0.0.1:8000 -- `run_bot.bat` — Telegram-бот (polling) -- `run_scheduler.bat` — фоновый сканер каждые `SCRAPE_INTERVAL_HOURS` часов - -## Эволюция архитектуры - -### Первая попытка (отвергнута) -**Идея:** сотрудник вводит DLD Permit Number своего объекта → система автоматически ищет «то же permit» на PF и Bayut → находит объявления конкурентов. - -**Почему не сработало:** -1. В Дубае **каждый брокер получает свой permit на свою публикацию**. Два брокера, рекламирующих одну квартиру = два разных permit. Permit не идентифицирует физический объект — он идентифицирует конкретное объявление конкретного брокера. -2. PF на странице объявления показывает permit **картинкой** через сервис верификации, не plain text (anti-scraping). -3. PF search `?q=` — free-text по названию/описанию, не структурированный фильтр. - -### Финальная архитектура -**Manual URL list + опциональные подсказки:** -1. Сотрудник создаёт проект (название, тип сделки, владелец, опц. building/bedrooms/sqft). -2. На странице проекта **вручную вставляет URL** объявления конкурента → система делает single-page fetch, парсит `__NEXT_DATA__`, добавляет в трекинг. -3. Если указано здание — кнопка «🔍 Подобрать похожие» ищет на PF/Bayut по `building + bedrooms` и предлагает кандидатов с кнопкой «+ Отслеживать». -4. Каждые 4 часа фоновый сканер делает refetch каждого отслеживаемого URL → детектит: - - 📈📉 изменение цены - - ❌ удаление (URL отдаёт 404) - - ♻️ возвращение из удалённого статуса -5. Уведомления — в Telegram личкой владельцу проекта. - -## Модель данных - -- `Employee` — name, tg_chat_id (опц.), tg_username -- `Project` — title, deal_type, owner_id, our_price, building, bedrooms, size_sqft, our_url, dld_permit (все после `owner` — опционально) -- `CompetitorListing` — source (PF|Bayut), external_id, url, current_price, status (active|removed), agent_name, agency_name, first_seen, last_seen -- `PriceHistory` — listing_id, price, recorded_at - -## Как подключиться сотруднику - -1. В TG найти бота → отправить `/start`. -2. Отправить `/whoami` → бот пришлёт chat_id. -3. В вебе http://127.0.0.1:8000/employees → найти/создать запись → вставить chat_id → Сохранить. - -## Известные ограничения - -- **PF/Bayut могут блокировать** при частых запросах (видно как `Blocked by site (403/429)` в логах). Решение — увеличить интервал; если уже не помогает — добавить Playwright fallback. -- **Подсказки эвристические**: ищем по совпадению building name в title + bedrooms-фильтр. Могут попасть «другие квартиры в том же здании» — поэтому добавление в трекинг через ручное подтверждение. -- **Permit как plain text** на PF не отдаётся. Если когда-нибудь понадобится — нужен OCR на verification-image. - -## Что в .env - -``` -TG_BOT_TOKEN=<токен от @BotFather> -SCRAPE_INTERVAL_HOURS=4 -ADMIN_CHAT_ID= # опц. -``` - -## Структура проекта - -``` -DLD Permit Number/ -├── run_web.bat, run_bot.bat, run_scheduler.bat -├── run_web.py -├── requirements.txt -├── .env (не в git — содержит токен) -├── data/monitor.db (создаётся автоматически) -├── app/ -│ ├── config.py ← settings + резолвит относительные SQLite-пути в абсолютные -│ ├── db.py, models.py -│ ├── web.py ← FastAPI: CRUD проектов, /listings add/delete, /suggest -│ ├── bot.py ← /start, /whoami, /list, /check -│ ├── scheduler.py -│ ├── scrapers/{base,propertyfinder,bayut}.py -│ ├── services/{monitor,notifier}.py -│ └── templates/ ← projects_list, project_form, project_detail, suggest, employees, base -``` - -## Что протестировать в первую очередь - -1. Удалить старый `data/monitor.db` (схема изменилась). -2. Запустить три .bat файла. -3. `/start` боту → `/whoami` → chat_id → вписать в Сотрудники. -4. Создать тестовый проект (Aykon City Tower B, 2BR). -5. Вставить URL реального объявления конкурента → проверить, что добавилось с ценой/брокером. -6. Жмякнуть «Подобрать похожие» → посмотреть кандидатов. -7. Жмякнуть «Проверить сейчас» → если цена не менялась, изменений не будет (это нормально); для теста алертов можно поменять цену в БД руками или подождать реального изменения. diff --git a/diagnose.py b/diagnose.py deleted file mode 100644 index 30f3362..0000000 --- a/diagnose.py +++ /dev/null @@ -1,89 +0,0 @@ -r"""Одноразовая диагностика: проверяет всю цепочку мониторинга. -Запуск: & "<...>\.venv\Scripts\python.exe" diagnose.py -Только чтение БД + проверка токена TG + живой re-fetch. Сообщения НЕ шлёт.""" -from __future__ import annotations - -import sys - -import httpx - -from app.config import settings -from app.db import SessionLocal -from app.models import CompetitorListing, Employee, PriceHistory, Project -from app.services.monitor import check_project - - -def line(c: str = "-") -> None: - print(c * 60) - - -def main() -> None: - db = SessionLocal() - try: - # --- 1. Содержимое БД --- - line("=") - print("1) ДАННЫЕ В БАЗЕ") - line("=") - projects = db.query(Project).all() - if not projects: - print(" ! В базе нет ни одного проекта.") - return - for p in projects: - owner = p.owner - chat = owner.tg_chat_id if owner else None - chat_str = chat if chat else "НЕ ЗАДАН (уведомления не дойдут!)" - print(f"\n Проект #{p.id}: {p.title}") - print(f" тип: {p.deal_type.value} | наша цена: {p.our_price}") - print(f" владелец: {owner.name if owner else '—'} | tg_chat_id: {chat_str}") - print(f" последняя проверка: {p.last_checked_at or 'ещё не было'}") - listings = p.listings - print(f" отслеживаемых объявлений конкурентов: {len(listings)}") - for l in listings: - hist = db.query(PriceHistory).filter(PriceHistory.listing_id == l.id).count() - price = f"{l.current_price:,.0f} {l.currency}".replace(",", " ") if l.current_price else "БЕЗ ЦЕНЫ" - print(f" - [{l.source.value}] {l.status.value} | {price} | история: {hist} зап.") - print(f" {(l.title or 'без названия')[:70]}") - print(f" брокер: {l.agency_name or '—'} | {l.url}") - - # --- 2. Telegram токен --- - line("=") - print("2) TELEGRAM") - line("=") - if not settings.tg_bot_token: - print(" ! TG_BOT_TOKEN не задан в .env") - else: - try: - r = httpx.get( - f"https://api.telegram.org/bot{settings.tg_bot_token}/getMe", - timeout=15.0, - ) - if r.status_code == 200 and r.json().get("ok"): - bot = r.json()["result"] - print(f" OK токен валиден: @{bot.get('username')} ({bot.get('first_name')})") - else: - print(f" ! getMe вернул {r.status_code}: {r.text}") - except Exception as e: - print(f" ! Ошибка связи с Telegram: {e}") - print(f" ADMIN_CHAT_ID: {settings.admin_chat_id or 'не задан'}") - - # --- 3. Живой re-fetch (то, что делает сканер раз в 4 ч) --- - line("=") - print("3) ЖИВАЯ ПЕРЕПРОВЕРКА (re-fetch всех объявлений)") - line("=") - for p in projects: - print(f"\n Проект #{p.id}: {p.title} — проверяю {len(p.listings)} объявл...") - changes = check_project(db, p) - if changes: - print(f" обнаружено изменений: {len(changes)} (в реальном прогоне ушли бы в TG):") - for c in changes: - print(" " + c.replace("\n", " | ").replace("", "").replace("", "")) - else: - print(" изменений нет — все объявления успешно перезагружены, цены прежние.") - line("=") - print("Готово. Если у объявлений есть цены и статус active, а токен валиден — цепочка рабочая.") - finally: - db.close() - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 6683ffe..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -services: - web: - build: . - image: dld-permit-monitor:latest - container_name: dld-web - restart: unless-stopped - env_file: .env - environment: - WEB_HOST: 0.0.0.0 - WEB_PORT: 8000 - ports: - - "8080:8000" - volumes: - - ./data:/app/data - command: ["python", "run_web.py"] - - bot: - image: dld-permit-monitor:latest - depends_on: - - web - container_name: dld-bot - restart: unless-stopped - env_file: .env - volumes: - - ./data:/app/data - command: ["python", "-m", "app.bot"] - - scheduler: - image: dld-permit-monitor:latest - depends_on: - - web - container_name: dld-scheduler - restart: unless-stopped - env_file: .env - volumes: - - ./data:/app/data - command: ["python", "-m", "app.scheduler"] diff --git a/run_bot.bat b/run_bot.bat deleted file mode 100644 index 4ebe5f9..0000000 --- a/run_bot.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -cd /d "%~dp0" -".venv\Scripts\python.exe" -m app.bot -pause diff --git a/run_scheduler.bat b/run_scheduler.bat deleted file mode 100644 index 4be836a..0000000 --- a/run_scheduler.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -cd /d "%~dp0" -".venv\Scripts\python.exe" -m app.scheduler -pause diff --git a/run_web.bat b/run_web.bat deleted file mode 100644 index c3a27e9..0000000 --- a/run_web.bat +++ /dev/null @@ -1,4 +0,0 @@ -@echo off -cd /d "%~dp0" -".venv\Scripts\python.exe" run_web.py -pause