diff --git a/README.md b/README.md index c3daab9..80c56cd 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,48 @@ -# MIEM Employees Server - +# 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. +- `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`: опциональный лимит профилей для тестового запуска. +- `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`. - -В админке доступны: - +- `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`. + +В админке доступны: + - «Обзор»: статистика, последний добавленный сотрудник, прогресс парсинга и ручной запуск. - «Сотрудники»: поиск, фильтры, сортировка, пагинация и выбор колонок. По умолчанию показаны ФИО, статус, должности, дата последнего обнаружения и внешний профиль. В диалоге колонок доступны наборы «Проверка», «Контакты» и «Все поля»; ранее сохранённый выбор сохраняется. - «Запуски»: история обходов, ошибки и доступный индикатор прогресса. @@ -52,7 +52,7 @@ uvicorn app.main:app --reload Каталог поддерживает навигацию с клавиатуры и системный диалог колонок с Escape и возвратом фокуса. На узких экранах таблица прокручивается внутри страницы, колонка ФИО закреплена. Используется системный шрифт без внешних загрузок. Каталог формируется сервером: отдельная клиентская загрузка списка не нужна, переход показывает браузер. Пустой результат предлагает сбросить фильтры, пустая база — запустить парсинг. При ошибке базы каталог возвращает HTTP 503 и предлагает повторить загрузку с теми же фильтрами. При обновлении прогресса показывается состояние загрузки; при ошибке сохраняются последние значения с предупреждением об их актуальности и кнопкой «Повторить». Автоматические попытки продолжаются каждые 4 секунды, запрос ограничен 15 секундами; параллельные запросы не запускаются. - + ## Пользователи и личный кабинет `/admin/users` доступен только администратору: создание учётных записей с ролями «Администратор»/«Пользователь», включение и отключение доступа, сброс пароля, первый и последний вход, дата смены пароля пользователем и выдачи временного пароля. Пользователь может читать служебные страницы и редактировать карточки, но не запускает парсинг (включая обновление одного сотрудника) и не управляет учётными записями. Проверки выполняются на сервере для HTML и API. Публичный просмотр диссовета и XLSX сохранён. @@ -71,37 +71,37 @@ uvicorn app.main:app --reload Проверки: `python -m pytest -q`, `python tests/browser_users.py`. Браузерный сценарий использует только синтетическую SQLite-БД и временный локальный HTTP-сервер без запуска миграций рабочего приложения; временные пароли на скриншотах скрыты. Для native PostgreSQL-проверок тестов пользователей задайте USERS_TEST_DATABASE_URL на отдельную одноразовую БД **miem_users_test**: эти тесты создают и удаляют в ней схему и не должны запускаться на рабочей БД. -## Docker Compose - -```bash +## Docker Compose + +```bash docker compose up -d --build --remove-orphans -``` - -По умолчанию: - -- API и админка: `http://localhost:8000` +``` + +По умолчанию: + +- 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`. - + +Таблицы создаются приложением при старте. При обновлении существующей базы приложение также добавляет недостающие 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`. - + +- краткий список остается внутри `employees.current_data.sections[].news_links`; +- нормализованные карточки из вкладки «В новостях» сохраняются в `employee_news_links`. + `employee_news_links` содержит название новости, ссылку, краткое описание, дату публикации, год публикации, raw JSON карточки и `source_hash`. Уникальность поддерживается по `(employee_id, url)` и `(employee_id, source_hash)`, поэтому повторный crawl не создает дубликаты. ## Кандидаты в диссертационный совет @@ -140,45 +140,45 @@ Compose запускает `api`, `worker` и `postgres`. API привязан После изменения JSON/PDF пересоберите образ и пересоздайте API: данные включены в образ и кешируются в процессе. -## Парсинг - +## Парсинг + Worker запускает обход по `CRAWL_CRON`. Ручной запуск также доступен в админке на `Dashboard` и странице `Runs` или через REST: - -```bash + +```bash curl -X POST http://localhost:8000/api/crawl-runs --cookie "miem_admin_session=..." -H "X-CSRF-Token: ..." -``` - -Алгоритм обновления: - -- найденные сотрудники получают статус `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` не считаются подтверждением увольнения; +``` + +Алгоритм обновления: + +- найденные сотрудники получают статус `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`. - +- неизмененные профили учитываются в `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 + +```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 exec postgres pg_dump -U miem miem_workers > backup.sql docker compose down ``` -Production deployment временно отключён до установки отдельного deployment-доступа Workers. Workflow `CI` запускает тесты на push в `main` и pull_request в `main`; шаги Upload release и Deploy and verify безусловно отключены для всех событий и значений input. Стандартные имена deployment secrets: `PROD_HOST`, `PROD_USER`, `PROD_SSH_KEY`, `PROD_KNOWN_HOSTS`; `.env` и ключи в репозиторий не добавляются. PostgreSQL backup выполняется на VPS раз в неделю user cron deploy-пользователя через `infra/postgres-backup.sh`: `pg_dumpall` сохраняет весь кластер в `~/backups/postgres/postgres-latest.sql`. Новый dump сначала создаётся во временный файл и заменяет старый только после успешного завершения; предыдущие dump-файлы `miem_workers` удаляются после успешного создания нового. Эта правка не меняет существующий backup cron. +Развёртывание на новом VPS ведётся через общую платформу МИЭМ. Канонический домен `workers.miem.space`. Выбор образа, восстановление PostgreSQL, доступ, healthcheck и откат описаны в [процедуре первого запуска](https://git.vakanaut.ru/admin/infra-platform/src/branch/main/docs/runbooks/REPLACEMENT_VPS_FIRST_ROLLOUT.md). Фактические статусы публикуются в Infrastructure Registry. [Карточка проекта](https://git.vakanaut.ru/admin/infrastructure-registry/src/branch/main/environments/miem/projects/miem_workers/index.md). -Ручной запуск с `diagnostic_only=true` (по умолчанию) выводит только SHA256 fingerprint ключа `PROD_SSH_KEY`, без подключения к VPS. Публичный ключ передаётся через pipe, не сохраняется в файл или artifacts; shell tracing отключён. При `diagnostic_only=false` диагностический job пропускается: это значение также не разрешает deployment. Возврат production deployment требует отдельной reviewed правки после установки нового доступа. +Ручной запуск с `diagnostic_only=true` (по умолчанию) выводит только SHA256 fingerprint ключа `PROD_SSH_KEY`, без подключения к VPS. Публичный ключ передаётся через pipe, не сохраняется в файл или artifacts; shell tracing отключён. При `diagnostic_only=false` диагностический job пропускается: это значение также не разрешает deployment. Этот job относится к диагностике ключа. Выбор и публикация production-релиза выполняются в infra-platform. ## Проверки