# 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.6`. Админка всегда показывает версии backend и frontend в footer.