139 lines
13 KiB
Markdown
139 lines
13 KiB
Markdown
# MIEM Employees Server
|
||
|
||
Сервис собирает сотрудников МИЭМ с сайта ВШЭ, хранит карточки и историю обновлений в Postgres и показывает минимальную админку.
|
||
|
||
## Архитектура
|
||
|
||
- `api`: FastAPI, REST API, HTML-админка и healthcheck.
|
||
- `worker`: weekly scheduler, который запускает парсинг по `CRAWL_CRON`.
|
||
- `postgres`: основная БД.
|
||
|
||
Парсер использует фиксированный источник сотрудников, по умолчанию `https://miem.hse.ru/persons`. Для каждой карточки сохраняются ФИО, должности, год начала работы, контакты, идентификаторы, вкладки профиля, секции, публикации, курсы, ВКР, новости, JSON-снапшот и сжатый HTML-снапшот. Детальные публикации дополнительно нормализуются в отдельную таблицу `employee_publications`, а новости из блока «В новостях» — в `employee_news_links`. Ссылки обходятся только из меню профиля самого сотрудника (`person-menu`), например `#sci`, `#teaching`, `#main`.
|
||
|
||
## Переменные окружения
|
||
|
||
Скопируйте `.env.example` в `.env` и поменяйте секреты:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
Основные настройки:
|
||
|
||
- `DATABASE_URL`: строка подключения SQLAlchemy.
|
||
- `SOURCE_URL`: список сотрудников МИЭМ.
|
||
- `CRAWL_CRON`: расписание в формате crontab, по умолчанию `0 3 * * 1`.
|
||
- `CRAWL_LIMIT`: опциональный лимит профилей для тестового запуска.
|
||
- `ADMIN_USERNAME`, `ADMIN_PASSWORD`: логин и пароль админки.
|
||
- `SESSION_SECRET`: секрет подписи cookie.
|
||
- `PARSER_USE_PLAYWRIGHT`: включение Playwright-рендера динамических вкладок.
|
||
- `DISMISSAL_CONFIRMATION_RUNS`: сколько последовательных проверок недоступности нужно для увольнения, по умолчанию `3`.
|
||
- `MAX_AUTO_DISMISSALS_PER_RUN`: защитный лимит массовых автоматических увольнений за один запуск, по умолчанию `25`.
|
||
|
||
## Локальный запуск
|
||
|
||
```bash
|
||
python -m venv .venv
|
||
.venv\Scripts\activate
|
||
pip install -r requirements.txt
|
||
uvicorn app.main:app --reload
|
||
```
|
||
|
||
Админка: `http://localhost:8000/admin`.
|
||
|
||
В админке доступны:
|
||
|
||
- «Обзор»: статистика, последний добавленный сотрудник, прогресс парсинга и ручной запуск.
|
||
- «Сотрудники»: поиск, фильтры, сортировка, пагинация и выбор колонок. По умолчанию показаны ФИО, статус, должности, дата последнего обнаружения и внешний профиль. В диалоге колонок доступны наборы «Проверка», «Контакты» и «Все поля»; ранее сохранённый выбор сохраняется.
|
||
- «Запуски»: история обходов, ошибки и доступный индикатор прогресса.
|
||
|
||
Все фильтры применяются кнопкой «Применить фильтры» с переходом на первую страницу. «Сбросить» очищает условия; при устаревшем номере страницы каталог возвращает первую. Подписи полей показывают выбранные условия, над таблицей указан диапазон результатов. Пояснения дат и статусов находятся под фильтрами.
|
||
|
||
Каталог поддерживает навигацию с клавиатуры и системный диалог колонок с Escape и возвратом фокуса. На узких экранах таблица прокручивается внутри страницы, колонка ФИО закреплена. Используется системный шрифт без внешних загрузок.
|
||
|
||
Каталог формируется сервером: отдельная клиентская загрузка списка не нужна, переход показывает браузер. Пустой результат предлагает сбросить фильтры, пустая база — запустить парсинг. При ошибке базы каталог возвращает HTTP 503 и предлагает повторить загрузку с теми же фильтрами. При обновлении прогресса показывается состояние загрузки; при ошибке сохраняются последние значения с предупреждением об их актуальности и кнопкой «Повторить». Автоматические попытки продолжаются каждые 4 секунды, запрос ограничен 15 секундами; параллельные запросы не запускаются.
|
||
|
||
## Docker Compose
|
||
|
||
```bash
|
||
docker compose up -d --build --remove-orphans
|
||
```
|
||
|
||
По умолчанию:
|
||
|
||
- API и админка: `http://localhost:8000`
|
||
- PostgreSQL: `postgres:5432` внутри сети Compose; порт на хост не опубликован.
|
||
|
||
Compose запускает `api`, `worker` и `postgres`. API привязан к localhost; для внешнего доступа нужен настроенный reverse proxy. REST API данных требует сессию администратора.
|
||
|
||
Таблицы создаются приложением при старте. При обновлении существующей базы приложение также добавляет недостающие runtime-колонки, например `crawl_runs.skipped_count`. SQL-миграции для ручного применения лежат в `migrations/`.
|
||
|
||
## Наполнение БД
|
||
|
||
Основная карточка сотрудника хранится в `employees`: профиль, статус, даты обнаружения/увольнения, текущий JSON `current_data`, checksum и версия парсера. История успешных изменений сохраняется в `employee_snapshots` вместе с JSON-снимком и сжатым HTML профиля.
|
||
|
||
Публикации теперь хранятся в двух видах:
|
||
|
||
- краткий список остается внутри `employees.current_data.sections[].publications` для обратной совместимости;
|
||
- детальные записи сохраняются в `employee_publications` и связываются с сотрудником через `employee_id`.
|
||
|
||
`employee_publications` содержит `publication_id`, название, год, тип публикации, язык, статус, ссылку на карточку HSE Publications, DOI, внешние/document-ссылки, citation text, аннотацию, описание, авторов, raw JSON ответа `searchPubs` и `source_hash` для безопасного повторного upsert. Уникальность поддерживается по `(employee_id, publication_id)` и `(employee_id, source_hash)`, поэтому повторный crawl не должен создавать дубликаты.
|
||
|
||
Новости сотрудников также хранятся в двух видах:
|
||
|
||
- краткий список остается внутри `employees.current_data.sections[].news_links`;
|
||
- нормализованные карточки из вкладки «В новостях» сохраняются в `employee_news_links`.
|
||
|
||
`employee_news_links` содержит название новости, ссылку, краткое описание, дату публикации, год публикации, raw JSON карточки и `source_hash`. Уникальность поддерживается по `(employee_id, url)` и `(employee_id, source_hash)`, поэтому повторный crawl не создает дубликаты.
|
||
|
||
## Парсинг
|
||
|
||
Worker запускает обход по `CRAWL_CRON`. Ручной запуск также доступен в админке на `Dashboard` и странице `Runs` или через REST:
|
||
|
||
```bash
|
||
curl -X POST http://localhost:8000/api/crawl-runs --cookie "miem_admin_session=..."
|
||
```
|
||
|
||
Алгоритм обновления:
|
||
|
||
- найденные сотрудники получают статус `active` и обновленный `last_seen_at`;
|
||
- новые сотрудники добавляются в `employees`;
|
||
- если профиль перенесен на другой URL, он сопоставляется с прежней записью по единственному точному совпадению ФИО;
|
||
- старые URL сохраняются в истории `employee_profile_urls`;
|
||
- количество новых сотрудников за запуск сохраняется в `crawl_runs.new_count`;
|
||
- публикации из HSE Publications записываются в `employee_publications`, а краткий список остается в JSON профиля;
|
||
- новости из блока «В новостях» записываются в `employee_news_links`, а краткий список остается в JSON профиля;
|
||
- один `404` старого профиля переводит сотрудника в статус `verification_required`, а не в `dismissed`;
|
||
- статус `dismissed` устанавливается только после нескольких последовательных проверок `404`/`410`;
|
||
- сетевые ошибки и ответы `5xx` не считаются подтверждением увольнения;
|
||
- если число кандидатов на увольнение превышает защитный лимит, автоматическое увольнение приостанавливается;
|
||
- кнопка «Проверить уволенных» сверяет только их profile_key с текущим списком источника и возвращает найденных сотрудников в `active` без обновления содержимого профиля;
|
||
- каждый успешный новый или измененный разбор сохраняет запись в `employee_snapshots`;
|
||
- неизмененные профили учитываются в `crawl_runs.skipped_count` и не получают новый snapshot.
|
||
|
||
Во время выполнения парсинга `found_count`, `parsed_count`, `skipped_count` и `error_count` обновляются в базе. Админка опрашивает `/api/crawl-runs/latest` и показывает прогресс как `(parsed_count + skipped_count + error_count) / found_count`.
|
||
|
||
## Обслуживание
|
||
|
||
```bash
|
||
docker compose logs -f api
|
||
docker compose logs -f worker
|
||
docker compose exec postgres pg_dump -U miem miem_workers > backup.sql
|
||
docker compose down
|
||
```
|
||
|
||
Production deploy выполняется job `deploy` workflow `CI` после успешного `test` на push в `main` или вручную. Для него нужны Actions secrets `PROD_HOST`, `PROD_USER`, `PROD_SSH_KEY` и `PROD_KNOWN_HOSTS`; `.env` и ключи в репозиторий не добавляются. Перед обновлением workflow сохраняет PostgreSQL backup в домашний каталог deploy-пользователя, пересобирает `api` и `worker` и проверяет `/api/health`. Одновременно выполняется только один production deploy.
|
||
|
||
## Проверки
|
||
|
||
После установки зависимостей из `requirements.txt` выполните:
|
||
|
||
```bash
|
||
python -m pytest -q
|
||
node --check app/static/admin.js
|
||
```
|
||
|
||
Тесты с данными используют временную SQLite; отдельные API smoke-тесты запускают приложение с его текущей конфигурацией. Браузерная проверка: `pip install playwright`, `python -m playwright install chromium`, затем `python tests/browser_admin.py`. Она проверяет реальный рендеринг страниц, клавиатуру, диалог, фильтры и восстановление прогресса после ошибки. Скриншоты сохраняются во временную папку; путь выводится в конце.
|
||
|
||
Версия сервиса: `0.8.4`. Админка всегда показывает версии backend и frontend в footer.
|