codex f851c62b8a
CI / test (pull_request) Successful in 9m12s
CI / Fingerprint diagnostics (production deployment temporarily disabled) (pull_request) Has been skipped
docs: record K1 checksum portability fix
2026-10-08 15:09:05 +03:00
2026-04-28 16:18:07 +03:00

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. 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 не создает дубликаты.

Кандидаты в диссертационный совет

Страница /admin/council доступна без авторизации. Она показывает сотрудников с докторской степенью в профиле и хотя бы одной публикацией в CSV или таблице employee_publications; минимальный порог не применяется. Уволенные сотрудники отмечены отдельно. Доступны поиск по ФИО, сортировка по каждому счётчику и страницы по 25 сотрудников. XLSX по /admin/council/export.xlsx содержит всех найденных сотрудников с выбранной сортировкой, их публикации и параметры расчёта.

Счётчики Q1, Q2, К1 и ВСЕГО учитывают уникальные публикации за последние пять лет по году. Q1/Q2 берутся из отметок Scopus или Web of Science в CSV и сохранённом CSV-обогащении БД. К1 определяется по совпадению нормализованного названия журнала с итоговой категорией в предоставленном справочнике ВАК. ВСЕГО — объединение Q1 ИЛИ Q2 ИЛИ К1: работа в нескольких категориях учитывается один раз. При расхождении годов источников каждый критерий проверяется по году своей записи; неизвестный год наследуется только при единственном согласованном годе. Счётчики «В БД» и «Все публикации» включают все годы независимо от категорий.

Подготовленный JSON app/data/council_articles.json сопоставляется по полному ФИО только при единственном совпадении. Публикации объединяются по DOI, затем по нормализованным названию и году, если нет конфликтующих DOI. Импорт CSV в БД не выдаётся за данные сайта. Для К1 поддерживаются необязательные колонки CSV «Название журнала» или «Журнал»; в исходном CSV их нет. В БД название берётся из поля journal в raw_data или короткого описания ВШЭ (description.short/shortLeft, название перед годом). Название самой публикации для определения журнала не используется. Неизвестный или несовпавший журнал не считается К1; перевод и нечёткое сопоставление не применяются. Явно указанное в PDF прежнее название журнала сохраняется как проверенный алиас; служебное примечание о переименовании хранится отдельно от названия.

Справочник app/data/k1_journals.json содержит 773 журнала К1, номера строк, даты категорирования и SHA256 исходного «ВАК категории.pdf». При извлечении проверены все 3134 строки; для записей с историей смены категории взята последняя категория. PDF при запросах не читается. Для обновления справочника разово установите pdfplumber в среду разработки и выполните:

python scripts/extract_k1_journals.py "app/data/ВАК категории.pdf"

После замены CSV или JSON нужно пересобрать Docker-образ и пересоздать API: данные включены в образ и кешируются в процессе.

Счётчики предварительные: для пограничных лет нужно проверить точную дату, для неизвестного типа — подтвердить, что это статьи. Материалы с известным другим типом не входят в Q1/Q2/К1 и ВСЕГО. К1 означает итоговую категорию в предоставленном справочнике, а не историческую категорию на дату публикации. Отсутствие отметок не доказывает недостаточную активность. Публичная страница не открывает контакты и служебные операции.

Парсинг

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

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.

Ручной запуск с diagnostic_only=true (по умолчанию) выводит только SHA256 fingerprint ключа PROD_SSH_KEY, без подключения к VPS. Публичный ключ передаётся через pipe, не сохраняется в файл или artifacts; shell tracing отключён. При diagnostic_only=false диагностический job пропускается: это значение также не разрешает deployment. Возврат production deployment требует отдельной reviewed правки после установки нового доступа.

Проверки

После установки зависимостей из 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.15.4. Админка всегда показывает версии backend и frontend в footer.

Разовый импорт публикаций

Инструкция, минимальные права временной роли и ограниченный SSH-запуск: docs/publication-import.md. По умолчанию импортер выполняет dry-run; production-применение требует отдельного решения владельца.

Интеграционная проверка импорта на отдельном PostgreSQL: docs/postgresql-import-verification.md.

Отдельный тестовый стенд на VPS и read-only проверка ресурсов: docs/postgresql-import-vps.md. Стенд запускает только владелец после оценки ресурсов.

Подбор по специальностям ВАК

На странице /admin/council доступны 12 специальностей. При выборе специальности показаны доктора наук с хотя бы одной учитываемой за последние пять лет публикацией в подходящем журнале К1. Публикации без года или названия журнала и материалы с известным типом, отличным от статьи, не подтверждают специальность. Счётчики Q1/Q2/К1 сохраняют прежний смысл; число работ по выбранной специальности указано внутри карточки сотрудника. Фильтр сохраняется при поиске, смене страницы и экспорте XLSX. На листах «Сотрудники» и «Публикации» XLSX отдельная колонка содержит коды и названия подтверждённых специальностей, включая выгрузку без фильтра. При выбранном фильтре добавлены число подходящих работ сотрудника и отметка «Да/Нет» для каждой публикации.

app/data/k1_specialties.json содержит все 773 журнала из k1_journals.json, коды действующих специальностей, ISSN, страницы и строки исходного перечня, историю назначений и SHA-256 источников. Специальности взяты из «ВАК Перечень.pdf» на 15.09.2026; категория К1 — из отдельного предоставленного справочника. Это связь по журналу, без оценки содержания статьи или исторической специальности на дату её публикации. Одинаковое название «Нефтегазовое дело» относится к двум ISSN: связь помечена как неоднозначная и не используется для автоматического подбора. Неполные даты начала из исходного PDF сохранены без восстановления даты; текущее назначение определяется по отсутствию истёкшего срока окончания в снимке перечня.

Для воспроизводимого обновления нужен pdfplumber только в среде разработки:

python scripts/extract_k1_specialties.py "app/data/ВАК Перечень.pdf"

Скрипт проверяет последовательность всех 3231 строк. Проверенные различия названий сохранены в scripts/k1_specialty_matches.json; каждое сопоставление проверяется по номеру строки и точному исходному названию. Нечёткий поиск не применяется. Сервис читает готовый JSON и не требует PDF или pdfplumber при запуске. Хеш k1_source_sha256 рассчитывается для исходного JSON с окончаниями строк LF; оба JSON-справочника записываются с LF для воспроизводимости на Windows и Linux.

Подготовка публикаций для портала

Портал читает app/data/council_articles.json: публикации представлены отдельными объектами с названием, годом, DOI, массивами авторов, квартилей и журналов. Дополнительные поля исходного экспорта сохранены в metadata; source_row связывает запись со строкой исходного CSV. by_author — готовый индекс нормализованных ФИО со ссылками на публикации, без копирования одной работы для каждого соавтора.

Исходный app/data/council_articles.csv сохранён для проверки и отдельного импорта в БД. Данные файла и индекс кэшируются на процесс, CSV не преобразуется при HTTP-запросах. Название источника «Файл CSV» и номера строк в интерфейсе и XLSX сохранены как происхождение данных. Перед разбором публикаций сотрудников из БД проверяется докторская степень; публикации остальных сотрудников не разбираются.

После обновления CSV пересоздайте JSON до сборки/развёртывания сервиса:

python scripts/prepare_council_articles.py app/data/council_articles.csv

Нужна только стандартная библиотека Python. Скрипт проверяет структуру CSV и индекс, сохраняет число записей и SHA-256 источника с окончаниями строк LF и заменяет JSON атомарно после проверки. CRLF нормализуются в LF только при чтении; исходный CSV не перезаписывается. JSON и хеш одинаковы на Windows и Linux; Git фиксирует эти файлы с LF. После замены файла перезапустите процессы API, чтобы сбросить их кэш. Обновления публикаций из БД продолжают отображаться без перезапуска.

S
Description
No description provided
Readme
5.5 MiB
1 Stars 1 Watchers 1 Forks
Languages
Python 80%
HTML 12.6%
JavaScript 3.9%
CSS 3.1%
Shell 0.3%