|
|
5 days ago | |
|---|---|---|
| apps | 5 days ago | |
| docker | 5 days ago | |
| packages | 5 days ago | |
| .env.example | 5 days ago | |
| .gitignore | 5 days ago | |
| README.md | 5 days ago | |
| TZ (1).md | 5 days ago | |
| package.json | 5 days ago | |
| pnpm-lock.yaml | 5 days ago | |
| pnpm-workspace.yaml | 5 days ago | |
| turbo.json | 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 с TTLEVIDENCE_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
Быстрый запуск
- Создайте
.env:
cp .env.example .env
- Поднимите инфраструктуру:
pnpm docker:up # postgres:5434 + redis:6380 + minio:9000/9001
- Установите зависимости и накатите схему:
pnpm install
pnpm db:migrate # prisma migrate dev
pnpm db:seed # 4 пользователя, 5 зон, 8 камер
- Запустите сервисы (каждый в своём терминале):
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
- Прогон e2e-сценариев:
pnpm fixtures:run
URL
- Web-admin: http://localhost:3000
- Login: http://localhost:3000/login
- Capture (веб-камера → recognition probe): http://localhost:3000/capture
- Пациенты: http://localhost:3000/patients
- Карточка пациента: http://localhost:3000/patients/[id]
- Enrollment: http://localhost:3000/enrollment
- Enrollment по треку: http://localhost:3000/enrollment/[trackId]
- Dashboard: http://localhost:3000/dashboard
- Аудит: http://localhost:3000/audit
- Инциденты (заглушка под Ф2): http://localhost:3000/incidents
- API: http://localhost:4000
- API health: http://localhost:4000/health
- Polimed mock: http://localhost:4100
- Face-service: http://localhost:8001
- Face-service health: http://localhost:8001/health
- MinIO console: http://localhost:9001 (login:
minioadmin/minioadmin)
Контроль доступа
Сидер создаёт 4 dev-пользователя (пароли — из SEED_PASSWORD_* в .env.example):
| Роль | Что видит и делает | |
|---|---|---|
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/loginPOST /auth/refreshPOST /auth/logoutGET /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— карточка трека, пишется auditview_track.GET /patients— пациенты, у которых есть визиты.GET /patients/:patientId/visits— история визитов, пишется auditview_patient_visits.
Enrollment и согласия:
POST /enrollment(SENIOR_ADMIN) — связать трек с пациентом Полимеда поpolimedPatientId+paperConsentRef; auditenroll.POST /consents/:patientId/revoke(SENIOR_ADMIN) — ставитConsentRevocationJobсо статусомpendingи BullMQ-задачу с задержкойCONSENT_REVOKE_DELAY_MS; auditconsent_revoke.
Полимед-мок (RBAC):
GET /polimed/patients/search?q=&limit=GET /polimed/appointments?date=YYYY-MM-DDGET /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]
Шаги:
- SENIOR_ADMIN выбирает трек (по умолчанию — новые за смену).
- Видит соседние треки через cross-camera re-id (face-service
/reid/search). - Ищет пациента в Полимед-моке (
GET /polimed/patients/search?q=). - Подтверждает наличие бумажного согласия и вводит
paperConsentRef. 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(prod86_400_000= 24 ч, e2e5000); consent-revocation.processorдожидается задержки, вызывает face-serviceDELETE /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_PASSWORDDATABASE_URL=postgresql://postgres:postgres@localhost:5434/receptionREDIS_HOST,REDIS_PORT=6380,REDIS_URL=redis://localhost:6380MINIO_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:8001POLIMED_BASE_URL=http://localhost:4100WEB_ADMIN_ORIGIN=http://localhost:3000API_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_SYSADMINREID_THRESHOLD=0.35— cross-camera re-id (ТЗ §4.3).RECOGNITION_THRESHOLD=0.5— узнавание пациента (ТЗ §4.3).CONSENT_REVOKE_DELAY_MS=86400000— задержка перед удалением эмбеддингов; для e2e5000.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 (A–E), 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 сценарии треков и эмбеддингов