Files
MapMil/README.md
T
gitrusprusandCursor 1492576fd9 Add Telegram listener, scheduler, and extended parsers admin UI.
Introduce background scheduling and migrations for analytics ingest, expand parser management and map toolbar UX, and ignore local data/session files from version control.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-02 20:47:56 +03:00

205 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# MapMil Platform (ЦП → ЦА → ПИ)
Единая платформа: ЦА (аналитика и карта) + ЦП (парсинг Telegram).
## Архитектура
```mermaid
flowchart LR
CA[ЦА Analytics Center]
CP[ЦП Parsing Center]
PI[ПИ External Consumers]
CA -->|jobs via Redis| CP
CP -->|POST /internal/ingest| CA
CA -->|GET /api/v1/events| PI
```
| Центр | Контейнеры | Назначение |
|-------|------------|------------|
| **ЦА** | `ca-db`, `ca-api`, `ca-frontend` | PostgreSQL, ingest API, карта, distribution API |
| **ЦП** | `cp-workers` | Парсинг Telegram: real-time listener (Telethon) + batch-задания из Redis |
| **Общее** | `redis` | Очередь заданий ЦА → ЦП |
## Структура monorepo
```
MapMil/
├── centers/
│ ├── analytics/
│ │ ├── api/ # CA backend (FastAPI + PostgreSQL)
│ │ └── frontend/ # CA admin UI (Vue + Leaflet)
│ └── parsing/
│ └── workers/ # CP workers (Telethon)
├── contracts/ # Shared schemas (ingest, jobs)
├── data/ # telegram.session (локально, не в git)
├── docker-compose.yml
└── .env # TELEGRAM_API_ID, TELEGRAM_API_HASH, …
```
## Быстрый старт
1. Создайте `.env` из примера и укажите ключи Telegram:
```bash
cp .env.example .env
# отредактируйте TELEGRAM_API_ID и TELEGRAM_API_HASH
```
2. Положите сессию Telegram в `data/telegram.session` (файл Telethon SQLite). Если мигрируете со старого SocialParser:
```bash
cp ../SocialParser/data/telegram.session data/
```
3. Запуск:
```bash
docker compose up --build
```
4. Откройте UI: [http://localhost:8080](http://localhost:8080)
## Admin UI
Веб-интерфейс ЦА доступен по тем же адресу. Навигация в шапке:
| Раздел | Путь | Описание |
|--------|------|----------|
| **Карта** | `/` | Интерактивная карта событий: навигация по датам (flatpickr), пресеты периода, фильтры региона/темы/источника, подложки Яндекс/OSM/Topo/ESRI, линейка, полноэкранный режим, центрирование по координатам и городам, поиск населённых пунктов (Nominatim). CRUD для ручных объектов (ПКМ). Поддерживает `?eventId=` |
| **Парсеры** | `/parsers` | Telegram-парсеры с периодическим запуском, настройка интервала, редактирование и удаление; дубликаты по `source_url` не записываются |
| **События** | `/events` | Фильтрация, пагинация, просмотр деталей, ссылка «На карте» для событий с координатами |
| **Аналитика** | `/analytics` | KPI-карточки, график динамики ingest за 30 дней, топ населённых пунктов и регионов |
| **ПИ** | `/consumers` | CRUD подписчиков distribution API, ротация ключей, тест среза через `/api/v1/events` |
## Сохранение Telegram-сессии
**Важно:** существующий файл сессии **не удаляется и не пересоздаётся**.
- Локально: `data/telegram.session` (SQLite Telethon, в `.gitignore`)
- В Docker `cp-workers`: каталог `./data` монтируется как `/data` (read-write)
- Путь в контейнере: `/data/telegram.session`
- Переменные из `.env`: `TELEGRAM_API_ID`, `TELEGRAM_API_HASH`, `TELEGRAM_SESSION_PATH=/data/telegram.session`
Файл сессии и `.env` не коммитятся.
## API
### Карта
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/api/health` | Health check |
| GET | `/api/map/objects` | Объекты на карте с данными события; фильтры: `date_from`, `date_to`, `on_date`, `region`, `topic`, `source_type`, `search`, `event_id` |
| GET | `/api/map/filters` | Справочники для фильтров: `regions`, `topics`, `source_types`, `available_dates`, `date_bounds` |
| GET | `/api/objects` | Объекты (legacy, без фильтров события) |
| POST | `/api/objects` | Создать объект вручную |
#### Инструменты карты (UI)
- **Даты:** кнопки ❮❮/❮/❯/❯❯, календарь flatpickr, пресеты периода (1 неделя — 1 год / день)
- **Фильтры:** регион, тема, источник (из `/api/map/filters`)
- **Подложки:** Яндекс Карты / Спутник (ru_RU), OpenStreetMap, OpenTopoMap, ESRI Satellite
- **Инструменты:** линейка (PolylineMeasure), полноэкранный режим, координаты/города, поиск Nominatim
- **Точки:** маркеры из ingest + popup; ручные объекты — CRUD и drag
- **Обновление:** polling `/api/map/objects` каждые 30 с
### ЦА Admin
| Метод | Путь | Описание |
|-------|------|----------|
| POST | `/admin/jobs` | Создать парсер (сразу в очередь + периодический запуск) |
| GET | `/admin/jobs` | Список парсеров |
| PATCH | `/admin/jobs/{id}` | Изменить канал/лимит, `interval_seconds`, `is_active` |
| DELETE | `/admin/jobs/{id}` | Удалить парсер |
| POST | `/admin/jobs/{id}/retry` | Повторить failed-задание |
| GET | `/admin/events` | События с фильтрами (`{ items, total }`) |
| GET | `/admin/analytics/summary` | KPI-сводка |
| GET | `/admin/analytics/timeline?days=` | Динамика ingest по дням |
| GET | `/admin/analytics/top-localities?limit=` | Топ населённых пунктов |
| GET | `/admin/analytics/top-regions?limit=` | Топ регионов |
| GET | `/admin/consumers` | Список подписчиков ПИ |
| POST | `/admin/consumers` | Создать подписчика ПИ |
| PATCH | `/admin/consumers/{id}` | Обновить подписчика |
| POST | `/admin/consumers/{id}/rotate-key` | Сменить API-ключ |
Пример задания Telegram:
```bash
curl -X POST http://localhost:8080/admin/jobs \
-H 'Content-Type: application/json' \
-d '{"source_type":"telegram","source_config":{"channel":"creamy_caprice","limit":50}}'
```
### ЦП → ЦА (internal)
| Метод | Путь | Описание |
|-------|------|----------|
| POST | `/internal/ingest` | Приём batch событий (заголовок `X-Internal-Token`) |
### ПИ Distribution API
| Метод | Путь | Описание |
|-------|------|----------|
| GET | `/api/v1/events` | События с фильтром по API-ключу |
Тестовый consumer `test-pi` создаётся при старте с ключом из `TEST_PI_API_KEY` (по умолчанию `test-pi-api-key-change-me`):
```bash
curl http://localhost:8080/api/v1/events \
-H 'Authorization: Bearer test-pi-api-key-change-me'
```
## Парсинг Telegram
`cp-workers` объединяет два режима в **одном процессе** (общая сессия `telegram.session`):
| Режим | Как работает |
|-------|----------------|
| **Listener (real-time)** | Telethon `NewMessage` / `Album` на активных парсерах (`is_active=true`); новый пост сразу уходит в ingest |
| **Batch (по расписанию)** | Планировщик в `ca-api` ставит задание в Redis; воркер забирает последние N постов (`iter_messages`) |
Дубликаты по `source_url` при ingest пропускаются. Listener не меняет `status` парсера (флаг `listener: true` в ingest).
Переменные `cp-workers`:
| Переменная | По умолчанию | Описание |
|------------|--------------|----------|
| `TELEGRAM_LISTENER_ENABLED` | `true` | Включить real-time listener |
| `TELEGRAM_LISTENER_REFRESH_SECONDS` | `60` | Как часто обновлять список каналов из БД |
Internal API: `GET /internal/listener/subscriptions` — список активных каналов для listener.
## Поток данных
1. Аналитик создаёт парсер: `POST /admin/jobs` (канал, лимит, интервал в секундах)
2. **Listener** сразу подписывается на канал и ingest-ит новые посты
3. **Планировщик** `ca-api` периодически ставит batch-задание в Redis (`cp:jobs`)
4. `cp-workers` забирает batch-задание, парсит последние N постов
5. Результаты → `POST /internal/ingest` (дубликаты по `source_url` пропускаются)
6. Новые события с координатами появляются на карте как `MapObject`
7. Внешние ПИ получают срез через `/api/v1/events`
## Миграция EventRecord → Event
| SocialParser | CA Event |
|--------------|----------|
| `event` | `description` / `title` |
| `date` (dd.mm.yy) | `event_date` |
| `geolocation` | `latitude`, `longitude` |
| `locality` | `locality`, `region` |
| `source_url` | `source_url` |
| — | `source_type = "telegram"` |
## Остановка
```bash
docker compose down
```
Данные PostgreSQL сохраняются в volume `pgdata`.
## Legacy
Старые каталоги `backend/` и `frontend/` в корне оставлены для справки; активная разработка — в `centers/`.