Files
arazor72 9e45179629 docs: rewrite README in digital-reception template style
Расширенный README с разбивкой по фактическим эндпоинтам, моделям БД,
сценарию auto-enrollment и отзыва согласия, переменным окружения и
скоупу Фазы 1.
2026-05-25 13:07:34 +05:00

416 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`](./TZ%20(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
## Структура
```text
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`:
```bash
cp .env.example .env
```
2. Поднимите инфраструктуру:
```bash
pnpm docker:up # postgres:5434 + redis:6380 + minio:9000/9001
```
3. Установите зависимости и накатите схему:
```bash
pnpm install
pnpm db:migrate # prisma migrate dev
pnpm db:seed # 4 пользователя, 5 зон, 8 камер
```
4. Запустите сервисы (каждый в своём терминале):
```bash
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
```
5. Прогон e2e-сценариев:
```bash
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`):
| 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-нагрузки.
Эндпоинты:
```text
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
Ручное сопоставление выполняется на:
```text
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` и ссылкой на бумажный документ. Отзыв:
```text
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:
```text
GET /audit/biometry?actorUserId=&subjectPatientId=&from=&to=&limit=
```
## Dashboard и история визитов
Dashboard:
```text
http://localhost:3000/dashboard
GET /dashboard/overview?date=YYYY-MM-DD
```
Возвращает агрегаты по дате (по умолчанию — сегодня): количество визитов, средние ожидание/обслуживание, события по часам, активные треки для journey-timeline.
История визитов пациента:
```text
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:
```text
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` (`A``E`), `TrackStatus`, `TrackEventType`, `ConsentRevocationStatus`.
Схема и миграции — в `packages/db/prisma/`.
## Полезные команды
```bash
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 сценарии треков и эмбеддингов
```