Reviewed-on: #38
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 и поменяйте секреты:
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.
Локальный запуск
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
docker compose up -d --build --remove-orphans
По умолчанию:
- API и админка:
http://localhost:8000 - PostgreSQL:
postgres:5432внутри сети Compose; порт на хост не опубликован.
Compose запускает api, worker и postgres. API привязан к localhost; для внешнего доступа нужен настроенный reverse proxy. MCP удалён, endpoint /mcp отсутствует; 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:
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.
Обслуживание
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
Проверки
После установки зависимостей из requirements.txt выполните:
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.1. Админка всегда показывает версии backend и frontend в footer.