Версия цифровой рецепции с резализованным механизмом отслеживания трека пациента по зонам
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
Алексей Разорвин 9e45179629 docs: rewrite README in digital-reception template style 5 days ago
apps Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
docker Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
packages Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
.env.example Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
.gitignore Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
README.md docs: rewrite README in digital-reception template style 5 days ago
TZ (1).md Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
package.json Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
pnpm-lock.yaml Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
pnpm-workspace.yaml Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago
turbo.json Initial commit: digital reception monorepo (M1-M11 + demo extensions) 5 days ago

README.md

Digital Reception — Фаза 1

Фаза 1 цифровой рецепции клиники ПЦС: фиксация треков людей по зонам, узнавание пациентов по лицу, ручной enrollment с бумажным согласием, история визитов, аудит биометрического доступа.

Проект — монорепо на pnpm + Turborepo. Backend — NestJS (apps/api, apps/polimed-mock) и Python/FastAPI (apps/face-service). Frontend — Next.js 15 App Router + shadcn/ui (apps/web-admin). База — PostgreSQL 16 + pgvector через Prisma. Очереди — BullMQ поверх Redis. Хранилище кадров — MinIO. Распознавание лиц — InsightFace buffalo_l, 512-d, cosine.

ТЗ: TZ (1).md.

Что реализовано

  • Камеры и зоны на уровне БД/API: 8 камер, 5 зон (A вход, B коридор, C стойка ×4, D перед кабинетом, E в кабинете).
  • Эмбеддинги лиц через InsightFace buffalo_l (512-d, cosine).
  • Cross-camera re-id: top-K ближайших эмбеддингов с других камер в окне T минут, отдельный порог REID_THRESHOLD.
  • Узнавание пациента по лицу: поиск ближайшего эмбеддинга только среди записей с привязанным patient_id и активным согласием.
  • Auto-enrollment на /capture: страница ловит кадр с веб-камеры, отправляет в API, та проксирует во face-service.
  • Pgvector-индекс по 512-d эмбеддингам с фильтрами по времени/камере/пациенту.
  • Ручной enrollment: SENIOR_ADMIN сопоставляет трек с пациентом из Полимед-мока и регистрирует ссылку на бумажное согласие.
  • Отзыв согласия → отложенное удаление эмбеддингов: BullMQ-джоба consent-revocation с задержкой CONSENT_REVOKE_DELAY_MS (prod 24 ч, e2e 5 с).
  • История визитов пациента: события arrived/waiting/service_started/service_ended/left_without_service по треку, агрегаты длительности ожидания и обслуживания.
  • Dashboard управляющего: KPI за день, события по часам, journey-timeline активных треков, аватары распознанных пациентов, bbox overlay лиц.
  • Аудит биометрического доступа: BiometryAccessLog фиксирует каждый просмотр трека/визита, enrollment, отзыв согласия и публичный recognition-probe.
  • Авторизация через HttpOnly cookies (access 15 мин, refresh 30 дн), refresh-токены хранятся в БД с возможностью отзыва.
  • RBAC по ролям: MANAGER, SENIOR_ADMIN, SECURITY, SYSADMIN — на уровне декораторов NestJS.
  • Полимед-мок как отдельный NestJS-сервис: поиск пациентов, расписание приёмов на дату, карточка приёма.
  • Evidence-кадры: MinIO bucket с lifecycle-правилом --expire-days 30, presigned URL с TTL EVIDENCE_PRESIGN_TTL_SECONDS.
  • Fixtures-runner: e2e-сценарии треков, событий и эмбеддингов через публичные ingest-эндпоинты.
  • Prisma 5, миграции, сид (4 пользователя, 5 зон, 8 камер).

Пока не реализованы: реальная МИС-интеграция Полимед, реальный RTSP/GPU/ByteTrack, поведенческие алерты (Max-бот), production compose, тесты ниже smoke-e2e.

Стек

  • pnpm workspaces 9 + Turborepo
  • NestJS 10 (apps/api, apps/polimed-mock)
  • Python 3.11 + FastAPI + Uvicorn (apps/face-service, apps/video-ingest)
  • Next.js 15 App Router + Tailwind + shadcn/ui + Recharts (apps/web-admin)
  • Prisma 5
  • PostgreSQL 16 + pgvector (pgvector/pgvector:pg16)
  • Redis 7 + BullMQ
  • MinIO (S3-совместимое)
  • InsightFace buffalo_l
  • JWT (jsonwebtoken) + bcrypt + HttpOnly cookies

Структура

digital-reception/
├── apps/
│   ├── api/             # NestJS — auth, RBAC, ingest, enrollment, consents, visits, dashboard, audit
│   ├── face-service/    # Python FastAPI — InsightFace + pgvector + re-id + recognize
│   ├── polimed-mock/    # NestJS — мок МИС Полимед
│   ├── video-ingest/    # Python — mp4-консьюмер (скаффолд под Ф0)
│   ├── fixtures-runner/ # Node — e2e сценарии треков/событий/эмбеддингов
│   └── web-admin/       # Next.js 15 — админка
├── packages/
│   ├── db/              # Prisma schema, миграции, seed, экспорт типов
│   ├── tsconfig/        # base / nest / next tsconfig пресеты
│   └── eslint-config/   # общий ESLint
├── docker/
│   ├── docker-compose.yml  # postgres + redis + minio + minio-init (lifecycle)
│   └── init.sql            # CREATE EXTENSION vector
├── package.json
├── pnpm-workspace.yaml
├── turbo.json
└── README.md

Быстрый запуск

  1. Создайте .env:
cp .env.example .env
  1. Поднимите инфраструктуру:
pnpm docker:up                   # postgres:5434 + redis:6380 + minio:9000/9001
  1. Установите зависимости и накатите схему:
pnpm install
pnpm db:migrate                  # prisma migrate dev
pnpm db:seed                     # 4 пользователя, 5 зон, 8 камер
  1. Запустите сервисы (каждый в своём терминале):
pnpm --filter=@reception/api dev               # http://localhost:4000
pnpm --filter=@reception/polimed-mock dev      # http://localhost:4100
pnpm --filter=@reception/web-admin dev         # http://localhost:3000
cd apps/face-service && uvicorn main:app --reload --port 8001
  1. Прогон e2e-сценариев:
pnpm fixtures:run

URL

Контроль доступа

Сидер создаёт 4 dev-пользователя (пароли — из SEED_PASSWORD_* в .env.example):

Email Роль Что видит и делает
manager@local MANAGER Dashboard, история визитов, пациенты
senior@local SENIOR_ADMIN Enrollment, отзыв согласий, всё что у MANAGER
security@local SECURITY Лента инцидентов (под Ф2, сейчас заглушка)
admin@local SYSADMIN Аудит биометрического доступа, всё что у MANAGER

Логин — POST /auth/login, ставит access_token (15 мин) и refresh_token (30 дн) как HttpOnly cookies. Refresh-токены хранятся в RefreshToken и могут быть отозваны на logout.

Backend API

Базовый префикс — http://localhost:4000. Если не указано Public, эндпоинт требует валидный access-cookie.

Auth:

  • POST /auth/login
  • POST /auth/refresh
  • POST /auth/logout
  • GET /auth/me

Камеры и зоны:

  • GET /cameras — список из БД с кодом и названием зоны.

Ingest (публичные, для fixtures-runner / video-ingest):

  • POST /ingest/tracks — создать трек по имени камеры и firstSeenAt.
  • POST /ingest/track-events — добавить событие (arrived/waiting/service_started/service_ended/left_without_service), опциональный zoneCode и evidenceKey.
  • POST /ingest/capture-frame — положить base64-кадр в MinIO и привязать к треку.

Recognition (публичный, но логируется в BiometryAccessLog):

  • POST /recognition/probe — кадр + cameraId + occurredAt, проксируется во face-service /recognize и возвращает patientId/confidence/distance, если узнан.

Треки и визиты (RBAC):

  • GET /tracks?status=&shiftDate= — список треков по фильтру (MANAGER/SENIOR_ADMIN/SYSADMIN).
  • GET /tracks/:id — карточка трека, пишется audit view_track.
  • GET /patients — пациенты, у которых есть визиты.
  • GET /patients/:patientId/visits — история визитов, пишется audit view_patient_visits.

Enrollment и согласия:

  • POST /enrollment (SENIOR_ADMIN) — связать трек с пациентом Полимеда по polimedPatientId + paperConsentRef; audit enroll.
  • POST /consents/:patientId/revoke (SENIOR_ADMIN) — ставит ConsentRevocationJob со статусом pending и BullMQ-задачу с задержкой CONSENT_REVOKE_DELAY_MS; audit consent_revoke.

Полимед-мок (RBAC):

  • GET /polimed/patients/search?q=&limit=
  • GET /polimed/appointments?date=YYYY-MM-DD
  • GET /polimed/appointments/:id

Dashboard и аудит:

  • GET /dashboard/overview?date=YYYY-MM-DD (MANAGER/SENIOR_ADMIN/SYSADMIN).
  • GET /audit/biometry?actorUserId=&subjectPatientId=&from=&to=&limit= (SYSADMIN) — максимум 500 записей за один запрос.

Health:

  • GET /health

Face-service

Отдельный Python FastAPI-сервис на InsightFace buffalo_l. Модель грузится в lifespan. Если задан SKIP_MODEL_LOAD=true, frame-эндпоинты возвращают null — режим для CI/локальных тестов без CPU-нагрузки.

Эндпоинты:

GET    /health
POST   /embed
POST   /track-embeddings
POST   /track-embeddings/raw
POST   /reid/search
POST   /recognize
POST   /recognize/embedding
POST   /enroll
DELETE /patient/{patient_id}/embeddings
GET    /patient/{patient_id}/count

Логика:

  • /embed — возвращает 512-d вектор лучшего лица на кадре без записи в БД.
  • /track-embeddings — сохраняет эмбеддинг с привязкой к треку/камере/моменту, опционально patient_id.
  • /track-embeddings/raw — сохраняет готовый вектор без детекции лица (только для fixtures-runner с синтетикой).
  • /reid/search — cross-camera re-id: top-K ближайших эмбеддингов за окно window_minutes (по умолчанию REID_WINDOW_MINUTES=5), фильтрует по REID_THRESHOLD, по умолчанию исключает ту же камеру.
  • /recognize — узнавание пациента: ближайший эмбеддинг среди записей с непустым patient_id, фильтр по RECOGNITION_THRESHOLD.
  • /recognize/embedding — то же, но по готовому вектору (для fixtures).
  • /enroll — привязывает все эмбеддинги трека к пациенту после согласия.
  • DELETE /patient/{id}/embeddings — физическое удаление при отзыве согласия (вызывается из BullMQ-джобы).

Пороги настраиваются в .env:

  • RECOGNITION_THRESHOLD=0.5 — узнавание пациента (после первой партии данных тюним по ТЗ §4.3).
  • REID_THRESHOLD=0.35 — склейка треков cross-camera, строже узнавания, иначе ложные склейки.
  • REID_WINDOW_MINUTES=5 — окно cross-camera re-id.

Камеры и зоны

Сидер создаёт 5 зон и 8 камер:

Камера Зона
cam-entrance A — Вход в клинику
cam-corridor B — Коридор / зона ожидания
cam-reception-1 C — Стойка рецепции (рабочее место 1)
cam-reception-2 C — Стойка рецепции (рабочее место 2)
cam-reception-3 C — Стойка рецепции (рабочее место 3)
cam-reception-4 C — Стойка рецепции (рабочее место 4)
cam-doctor-waiting D — Перед кабинетом врача
cam-doctor-office E — В кабинете врача

На рецепции 4 рабочих места — по камере на каждое, поэтому зона C содержит 4 камеры. Реальный RTSP-инжест и ByteTrack — в Ф0; в Ф1 потоки эмулируются fixtures-runner и страницей /capture.

Auto-enrollment и /capture

/capture — страница для демо в браузере без RTSP. Берёт кадр с веб-камеры, отправляет в POST /api/capture на стороне Next, та проксирует в POST /recognition/probe. Backend кладёт пробу во face-service /recognize. Если confidence выше RECOGNITION_THRESHOLD, отрисовывается аватар-кружок и bbox распознанного лица; иначе — кадр идёт как новая точка трека и попадает в enrollment.

Маршрут API проксирования: apps/web-admin/src/app/api/capture/.

Enrollment

Ручное сопоставление выполняется на:

http://localhost:3000/enrollment
http://localhost:3000/enrollment/[trackId]

Шаги:

  1. SENIOR_ADMIN выбирает трек (по умолчанию — новые за смену).
  2. Видит соседние треки через cross-camera re-id (face-service /reid/search).
  3. Ищет пациента в Полимед-моке (GET /polimed/patients/search?q=).
  4. Подтверждает наличие бумажного согласия и вводит paperConsentRef.
  5. POST /enrollment со схемой { trackId, polimedPatientId, polimedAppointmentId?, paperConsentRef }.

Бэкенд:

  • создаёт/обновляет Patient (Polimed external id), PatientConsent (ConsentAction.granted) и Visit;
  • вызывает face-service /enroll — все эмбеддинги трека получают patient_id;
  • пишет BiometryAccessLog с action='enroll'.

Без paperConsentRef запрос валидно отклоняется (MinLength(1)).

Согласия и отзыв

Активное согласие хранится в PatientConsent с действием granted и ссылкой на бумажный документ. Отзыв:

POST /consents/:patientId/revoke

Поведение:

  • сразу создаётся запись PatientConsent (ConsentAction.revoked) и ConsentRevocationJob со статусом pending;
  • в BullMQ ставится задача с задержкой CONSENT_REVOKE_DELAY_MS (prod 86_400_000 = 24 ч, e2e 5000);
  • consent-revocation.processor дожидается задержки, вызывает face-service DELETE /patient/{id}/embeddings, выставляет ConsentRevocationJob.status = completed и пишет audit;
  • при ошибке статус становится failed, задача повторно ставится в очередь.

Прим.: до завершения задержки распознавание для этого пациента ещё может срабатывать — это сознательное поведение под ТЗ (окно на отмену отзыва на бумаге).

Аудит биометрического доступа

BiometryAccessLog пишет каждое касание к биометрии:

  • recognition_probe — публичный probe из /capture;
  • view_track, view_patient_visits — просмотры в админке;
  • enroll — сопоставление трека с пациентом;
  • consent_revoke — старт процедуры отзыва.

Запись содержит actorUserId (если был JWT), subjectPatientId, action, metadata (JSON), occurredAt. Просмотр доступен только SYSADMIN:

GET /audit/biometry?actorUserId=&subjectPatientId=&from=&to=&limit=

Dashboard и история визитов

Dashboard:

http://localhost:3000/dashboard
GET /dashboard/overview?date=YYYY-MM-DD

Возвращает агрегаты по дате (по умолчанию — сегодня): количество визитов, средние ожидание/обслуживание, события по часам, активные треки для journey-timeline.

История визитов пациента:

http://localhost:3000/patients/[id]
GET /patients/:patientId/visits

В карточке пациента — визиты, длительность ожидания (от первого waiting/arrived до service_started), длительность обслуживания (от service_started до service_ended), эвиденс-кадры с presigned URL.

Полимед-мок

apps/polimed-mock — отдельный NestJS-сервис на :4100. Имитирует МИС: 200+ пациентов, расписание приёмов на день, врачей и кабинеты. API:

GET /polimed/patients/search?q=&limit=
GET /polimed/appointments?date=YYYY-MM-DD
GET /polimed/appointments/:id

API основного сервиса проксирует к мок-сервису через POLIMED_BASE_URL. Реальной интеграции в Ф1 нет.

Переменные окружения

Из .env.example:

Postgres / Redis / MinIO:

  • POSTGRES_HOST, POSTGRES_PORT=5434, POSTGRES_DB=reception, POSTGRES_USER, POSTGRES_PASSWORD
  • DATABASE_URL=postgresql://postgres:postgres@localhost:5434/reception
  • REDIS_HOST, REDIS_PORT=6380, REDIS_URL=redis://localhost:6380
  • MINIO_ENDPOINT=http://localhost:9000, MINIO_ROOT_USER, MINIO_ROOT_PASSWORD, MINIO_BUCKET=reception-evidence

Порты сервисов:

  • API_PORT=4000, POLIMED_MOCK_PORT=4100, FACE_SERVICE_PORT=8001, WEB_ADMIN_PORT=3000

Service URLs (сервисы видят друг друга):

  • FACE_SERVICE_URL=http://localhost:8001
  • POLIMED_BASE_URL=http://localhost:4100
  • WEB_ADMIN_ORIGIN=http://localhost:3000
  • API_BASE_URL=http://localhost:4000

Auth:

  • JWT_ACCESS_SECRET, JWT_REFRESH_SECRET — обязательно поменять в проде.
  • JWT_ACCESS_TTL=15m, JWT_REFRESH_TTL=30d.
  • COOKIE_SECURE=false — для прода true.

Сид и пороги:

  • SEED_PASSWORD_MANAGER, SEED_PASSWORD_SENIOR, SEED_PASSWORD_SECURITY, SEED_PASSWORD_SYSADMIN
  • REID_THRESHOLD=0.35 — cross-camera re-id (ТЗ §4.3).
  • RECOGNITION_THRESHOLD=0.5 — узнавание пациента (ТЗ §4.3).
  • CONSENT_REVOKE_DELAY_MS=86400000 — задержка перед удалением эмбеддингов; для e2e 5000.
  • EVIDENCE_PRESIGN_TTL_SECONDS=900 — TTL presigned URL evidence-кадров.

Не коммитьте .env, JWT_*_SECRET, реальные пароли, дампы БД и MinIO-данные.

Скоуп Фазы 1

  • face-service + pgvector + cross-camera re-id + recognize.
  • Web-admin: ручной enrollment, согласия, история визитов, аудит, dashboard.
  • Бумажные согласия, отзыв → отложенное удаление эмбеддингов (24 ч prod / 5 с e2e).
  • RBAC по ролям MANAGER / SENIOR_ADMIN / SECURITY / SYSADMIN.
  • Auto-enrollment в /capture, journey-timeline, bbox overlay, аватары распознанных.
  • 🚫 Полимед API — замокан apps/polimed-mock, реальной интеграции нет.
  • 🚫 RTSP / GPU / ByteTrack — Ф0; в Ф1 только mp4-скаффолд и fixtures-runner.
  • 🚫 Поведенческие алерты, Max-бот, инциденты — Ф2.
  • 🚫 Production compose, метрики, rate limits — Ф2.

Известные ограничения

  • InsightFace buffalo_l грузится ~1.5 ГБ моделей при первом старте; в CI используйте SKIP_MODEL_LOAD=true.
  • Пороги REID_THRESHOLD и RECOGNITION_THRESHOLD под ТЗ §4.3 — стартовые, тюним на реальных данных пилота.
  • evidence_key для кадров — base64 → MinIO; сырое видео не сохраняется.
  • Cross-camera re-id ограничен окном времени; вне окна треки не склеиваются.
  • Rate-limit и инциденты не реализованы — это работа Ф2.
  • В packages/ есть только db, eslint-config, tsconfig. Общих UI-компонентов в shared-пакете пока нет; shadcn/ui подтягивается напрямую в apps/web-admin.

Структура БД (Prisma)

Модели: User, RefreshToken, Patient, PatientConsent, Zone, Camera, Track, TrackEvent, FaceEmbedding, Visit, BiometryAccessLog, ConsentRevocationJob.

Энумы: Role, ConsentAction, ZoneCode (AE), TrackStatus, TrackEventType, ConsentRevocationStatus.

Схема и миграции — в packages/db/prisma/.

Полезные команды

pnpm dev                          # turbo dev для всех приложений
pnpm build                        # turbo build
pnpm lint                         # turbo lint
pnpm typecheck                    # turbo typecheck
pnpm test                         # turbo test

pnpm db:generate                  # prisma generate
pnpm db:migrate                   # prisma migrate dev
pnpm db:migrate:deploy            # prisma migrate deploy (CI/prod)
pnpm db:seed                      # 4 пользователя, 5 зон, 8 камер
pnpm db:studio                    # Prisma Studio

pnpm docker:up                    # postgres + redis + minio + minio-init
pnpm docker:down                  # остановить инфраструктуру
pnpm docker:logs                  # хвост логов compose-файла

pnpm fixtures:run                 # e2e сценарии треков и эмбеддингов