REST API для системи відстеження артеріального тиску. Виконує OCR знімків тонометра локально через aivm-photo-api (з резервним fallback на Gemini AI) та асинхронно пересилає фото у сервіс збору датасету для подальшого навчання моделей.
graph TD
User((Користувач)) --> Frontend[Vue 3 PWA]
Frontend <--> Backend[ASP.NET Core 10 API]
Backend <--> DB[(PostgreSQL 16)]
Backend -- OCR (primary) / Fire-and-forget --> PhotoApi[aivm-photo-api]
Backend -- OCR (fallback) --> Gemini[Gemini AI API]
PhotoApi --> NAS[(TrueNAS SMB Share)]
Backend -- structured logs --> Seq[Seq]
subgraph Background Workers
Backend --> EmailWorker[EmailOutboxWorker]
Backend --> CleanupWorker[CleanupWorker]
end
Приклад — додавання вимірювання з фото:
-
Фронтенд робить фото через камеру, стискає його (~1024px довша сторона, JPEG 0.85) і відправляє
POST /api/v1/measurements/analyze(multipart, полеimage). -
SessionMiddleware читає
__Host-sessioncookie, валідує сесію в БД, наповнюєHttpContext.User. Без валідної сесії — 401. -
AnalyzeEndpoints запускає OCR-пайплайн:
- Якщо
PHOTO_API_ENABLED=true—PhotoApiService.RecognizeAsync()шле синхронний HTTP-запит наaivm-photo-api/images/recognize(таймаут 5с). При успіху повертається{sys, dia, pulse, confidence, source: "local"}. - Якщо local OCR недоступний, повернув помилку або вимкнений — лог
Warning "Local recognition unavailable or failed, falling back to Gemini", виклик Gemini API. Результат:{sys, dia, pulse, confidence: null, source: "gemini"}. - Якщо обидва не доступні — 422 з повідомленням про невдачу.
- Якщо
-
Фронтенд показує користувачу
{sys, dia, pulse}для підтвердження. Користувач може виправити значення. -
При підтвердженні —
POST /api/v1/measurements/with-photo(multipart): поляimage,sys,dia,pulse, опційноgeminiSys/geminiDia/geminiPulse(історична назва — насправді AI-підказка незалежно від джерела),sourceEngine("local"/"gemini"). -
MeasurementEndpoints валідує діапазони (sys 40-300, dia 20-200, pulse 30-250), зберігає замір у БД через
MeasurementService.CreateAsync(), повертає 201 користувачу. -
Після відповіді (fire-and-forget) —
PhotoApiService.UploadAsync()шле фото + метадані наaivm-photo-api/images/uploadдля архіву датасету. Збій upload логується, але не впливає на користувача (замір вже в БД).
Структуровані логи кожного кроку доступні в Seq — спільні поля для трасування одного запиту: RequestPath, user_id, request_id, ConnectionId.
- .NET 10 Minimal APIs
- PostgreSQL 16 + Entity Framework Core 10.0 (Npgsql)
- Fido2NetLib — Passkey (WebAuthn) автентифікація
- MailKit — відправка email (magic link, CSV export)
- Serilog — структуроване JSON-логування + Seq sink
- Scalar UI — інтерактивна документація (
/scalar/v1) - Docker + Docker Compose — розгортання
- aivm-photo-api — основний OCR (локальна YOLOv8-модель)
- Gemini AI — резервний OCR (fallback)
Структуровані JSON-логи через Serilog. Перегляд — через Seq на http://treehouse.lan:5341 (локальна мережа).
В Seq доступні фільтри по полях: RequestPath, user_id, request_id, SourceContext,
а також по будь-якому полю обʼєктів, що логуються з {@...} синтаксисом.
Корисні запити:
RequestPath = '/api/v1/measurements/analyze'— всі OCR-запити@Level = 'Warning'— попередження (наприклад, fallback на Gemini)Result.Source = 'gemini'— запити, що пішли через Gemini fallbackSourceContext like 'BpTracker%'— тільки прикладні логи (без шуму від EF/ASP.NET)
INFO-логи Microsoft.* стеку відфільтровані до Warning у Program.cs через MinimumLevel.Override — щоб SQL-запити EF Core і per-request трасування ASP.NET не засмічували Seq.
├── Data/ AppDbContext, EF Core migrations
├── Models/ Measurement, TreatmentSchema, User, UserSetting,
│ UserCredential, UserSession, MagicLink, EmailOutbox
├── Services/ IMeasurementService, ISchemaService, IGeminiService,
│ IAuthService, IEmailSender, IPhotoApiService,
│ EmailOutboxWorker, CleanupWorker
├── BpTracker.Api.Tests/ Інтеграційні тести (xUnit + Testcontainers)
├── Endpoints/ MeasurementEndpoints, SchemaEndpoints, AnalyzeEndpoints,
│ AuthEndpoints, SettingsEndpoints, ExportEndpoints
├── Middleware/ SessionMiddleware (читає __Host-session cookie → HttpContext.User)
├── Program.cs Startup, DI, CORS, Rate Limiting, Serilog, автоміграції, UseForwardedHeaders (X-Forwarded-For/Proto від Cloudflare Tunnel)
└── Dockerfile Multi-stage build (sdk → publish → runtime)
| Метод | URL | Опис |
|---|---|---|
GET |
/api/v1/health |
Стан БД, aivm-photo-api та Gemini API |
| Метод | URL | Опис |
|---|---|---|
GET |
/api/v1/auth/me |
Поточний користувач (сесія) |
POST |
/api/v1/auth/logout |
Вихід (видаляє сесію) |
POST |
/api/v1/auth/passkey/register/begin |
Ініціює реєстрацію Passkey (потребує авторизації) |
POST |
/api/v1/auth/passkey/register/complete |
Завершує реєстрацію Passkey (потребує авторизації) |
POST |
/api/v1/auth/login/begin |
Ініціює вхід через Passkey |
POST |
/api/v1/auth/login/complete |
Завершує вхід через Passkey |
POST |
/api/v1/auth/magic-link/request |
Надсилає magic link на email (rate limit: 3/15хв) |
POST |
/api/v1/auth/magic-link/consume |
Споживає magic link {token} → встановлює сесію |
POST |
/api/v1/auth/dev-login |
Швидкий вхід під дев-користувачем (лише в Development режимі, див. README_DEV.md) |
| Метод | URL | Опис |
|---|---|---|
GET |
/api/v1/measurements?days=90 |
Вимірювання за N днів (default 90, max 365) |
POST |
/api/v1/measurements |
Додати вимірювання {sys, dia, pulse} (JSON) |
POST |
/api/v1/measurements/with-photo |
Додати замір з фото (multipart/form-data). Поля: image, sys, dia, pulse, geminiSys, geminiDia, geminiPulse (історична назва — AI-підказка незалежно від джерела), sourceEngine ("local"/"gemini"). Зберігає в БД та асинхронно шле в photo-api. |
DELETE |
/api/v1/measurements/{id} |
Видалити вимірювання |
POST |
/api/v1/measurements/analyze |
OCR фото тонометра → {sys, dia, pulse, source, confidence} (rate limit: 10/хв). source = "local" або "gemini". confidence для local — мін. впевненість серед розпізнаних цифр, для gemini — null. |
| Метод | URL | Опис |
|---|---|---|
GET |
/api/v1/settings |
Отримати налаштування (створює рядок при першому GET) |
PATCH |
/api/v1/settings |
Оновити налаштування {exportEmail, geminiUrl} |
| Метод | URL | Опис |
|---|---|---|
POST |
/api/v1/export/csv |
Поставити CSV у чергу на відправку (rate limit: 1/10хв) |
| Метод | URL | Опис |
|---|---|---|
GET |
/api/v1/schemas/active |
Активна схема лікування поточного користувача |
GET |
/api/v1/schemas |
Список усіх схем користувача |
POST |
/api/v1/schemas |
Створити схему {doctor, prescribedOn?, schedule, setActive} |
PUT |
/api/v1/schemas/{id} |
Оновити схему |
POST |
/api/v1/schemas/{id}/activate |
Зробити схему активною |
Активною може бути максимум одна схема для кожного користувача; setActive при створенні та activate атомарно (в транзакції) знімають прапорець з попередньої активної.
| Метод | URL | Опис |
|---|---|---|
POST |
/api/v1/reminders/template |
Створити або перезаписати шаблон розкладу прийому (ініціює генерацію прийомів на сьогодні/майбутнє) |
PATCH |
/api/v1/reminders/template/{id} |
Частково оновити шаблон розкладу за його ID |
GET |
/api/v1/reminders/template/active |
Отримати поточний активний шаблон розкладу прийому ліків |
POST |
/api/v1/reminders/confirm |
Підтвердити прийом ліків для певного періоду (Morning, Day, Evening) із таймзоною клієнта. Тіло: {period, timezone}. |
GET |
/api/v1/reminders/today |
Отримати розклад прийомів ліків на сьогодні з таймзоною клієнта (?timezone=Europe/Kyiv) та їх статусами. |
GET |
/api/v1/reminders/reports |
Отримати звіт про виконання прийомів ліків за останні N днів (?days=30). |
| Метод | URL | Опис |
|---|---|---|
GET |
/api/v1/push/vapid-public-key |
Отримати публічний ключ VAPID (публічний ендпоінт) |
POST |
/api/v1/push/subscribe |
Підписатися на push-повідомлення (надсилається об'єкт підписки браузера) |
POST |
/api/v1/push/unsubscribe |
Скасувати підписку на push-повідомлення |
POST |
/api/v1/push/test |
Надіслати тестове push-повідомлення поточному користувачу |
Інтерактивна документація (dev mode): http://localhost:5000/scalar/v1
Локальний ML-сервіс. Виконує дві ролі:
- Основний OCR — на ендпоінті
/measurements/analyzeбекенд шле синхронний запит на/images/recognize. YOLOv8-пайплайн розпізнає цифри з фото тонометра на CPU (~50 мс). - Збір датасету — при
/measurements/with-photoбекенд асинхронно (черезPhotoApiService, fire-and-forget) пересилає фото + метадані на/images/upload. Файли йдуть на SMB-mounted NAS share.
Тонкощі:
- Стійкість: Збої
photo-apiлогуються. При відмові розпізнавання — fallback на Gemini. При відмові upload — замір все одно зберігається для користувача в БД. - Таймаут: 5 секунд на запит до photo-api. Якщо не відповідає — на Gemini.
- Вимкнення:
PHOTO_API_ENABLED=falseпропускає local-крок повністю і йде одразу на Gemini.
Змінні: PHOTO_API_ENABLED, PHOTO_API_URL, PHOTO_API_TOKEN, PHOTO_API_DEVICE_MODEL.
Документація: ../aivm-photo-api/README.md
Резервний (fallback) OCR. Викликається коли local недоступний, повернув помилку або вимкнений.
- Змінні:
GEMINI_API_KEY,GEMINI_MODEL. - Ліміти: 10 запитів на хвилину на користувача.
Локальний агрегатор структурованих логів. Працює на сусідньому контейнері (порт 5341). Бекенд пише туди через Serilog.Sinks.Seq. Консольний sink залишений як страховка (docker logs все ще працює).
SPA застосунок на Vue 3, що є єдиним клієнтом цього API. Документація: ../bptracker-frontend/README.md
| Змінна | Обов'язкова | За замовчуванням | Опис |
|---|---|---|---|
POSTGRES_PASSWORD |
Так | — | Пароль PostgreSQL |
GEMINI_API_KEY |
Так | — | API ключ Google Gemini |
GEMINI_MODEL |
Ні | gemini-flash-latest |
Назва моделі Gemini |
ALLOWED_EMAILS |
Ні | — | Список дозволених email-адрес (через кому) для входу. Якщо порожній або відсутній — вхід заборонений для всіх. |
CORS_ORIGINS |
Ні | https://bptracker.home.vn.ua |
Дозволені origins (через кому) |
FIDO2_DOMAIN |
Ні | bptracker.home.vn.ua |
Domain для Passkeys (RP ID) |
ASPNETCORE_ENVIRONMENT |
Ні | Production |
Production або Development |
APP_URL |
Ні | https://bptracker.home.vn.ua |
URL фронтенду (для magic link в email) |
SEQ_URL |
Ні | http://seq:80 |
URL Seq ingestion endpoint (всередині docker network) |
SMTP_HOST |
Ні | — | SMTP сервер |
SMTP_PORT |
Ні | 587 |
SMTP порт (587 = StartTLS, 465 = SSL) |
SMTP_USER |
Ні | — | SMTP логін |
SMTP_PASSWORD |
Ні | — | SMTP пароль / app password |
SMTP_FROM |
Ні | — | Адреса відправника |
SMTP_FROM_NAME |
Ні | BP Tracker |
Ім'я відправника |
SMTP_TLS |
Ні | true |
StartTLS для порту 587 |
PHOTO_API_ENABLED |
Ні | false |
Увімкнути локальний ML-сервіс (основний шлях OCR + збір датасету). Якщо вимкнено — OCR іде напряму через Gemini. |
PHOTO_API_URL |
Так* | — | URL сервісу aivm-photo-api (*якщо увімкнено) |
PHOTO_API_TOKEN |
Так* | — | Bearer token для aivm-photo-api (*якщо увімкнено) |
PHOTO_API_DEVICE_MODEL |
Ні | Paramed Expert-X |
Модель приладу для метаданих |
Для налаштування середовища використовуються такі файли:
.env— змінні для продакшену (ігнорується Git)..env.example— зразок заповнення, містить вичерпний перелік змінних, необхідних для запуску додатка (публікується в Git)..env.local.dev— конфігураційні змінні для локальної розробки (ігнорується Git).
- Автентифікація: HttpOnly
__Host-sessioncookie (Secure, SameSite=Lax). Passkeys черезFido2NetLib. Magic link — SHA-256 хеш у БД, TTL 15 хв. - Безпечна реєстрація Passkey: реєстрація Passkey вимагає активної сесії (
RequireAuthorization). Прив'язка нового ключа здійснюється виключно до ідентифікатора користувача з поточної сесії, ігноруючи будь-який email з тіла запиту (запобігання account takeover). - Список дозволених адрес (Email Allowlist): вхід/реєстрація обмежені списком дозволених адрес у змінній
ALLOWED_EMAILS. Перевірка здійснюється уmagic-link/consumeбез розкриття інформації про статус перевірки (повертається стандартна успішна відповідь без створення користувача чи сесії для запобігання перебору email-адрес). - Проксі та HTTPS:
UseForwardedHeadersвProgram.csчитаєX-Forwarded-For/X-Forwarded-Protoвід Cloudflare Tunnel, щоб ASP.NET Core коректно визначав HTTPS-схему (необхідно дляSecurecookie та WebAuthn RP ID). - Інвалідація сесій: при кожному логіні видаляються всі сесії цього користувача старіші за 90 днів.
- CORS: дозволено лише origins з
CORS_ORIGINS+.AllowCredentials() - Rate limiting:
/measurements/analyzeта/measurements/with-photo— 10 req/хв per user;/magic-link/request— 3 req/15хв per email;/export/csv— 1 req/10хв per user. - Ізоляція даних: всі запити вимірювань фільтруються по
UserIdпоточної сесії. - Обробка помилок: глобальний
UseExceptionHandler→ RFC 7807 ProblemDetails.
PostgreSQL CHECK constraints + Data Annotations:
- Систолічний: 40–300
- Діастолічний: 20–200
- Пульс: 30–250
Інтеграційний набір у BpTracker.Api.Tests/. Потребує Docker Desktop (Testcontainers піднімає postgres:16 автоматично).
cd bptracker-backend
# Запустити всі тести (потребує Docker)
dotnet test
# Запустити тільки юніт-тести (без Docker)
dotnet test --filter PhotoApiServiceTestsПокриває: auth (magic link, session), measurements (CRUD, IDOR, with-photo), export (outbox, rate limit), health, photo-api service (upload, recognize: local success, fallback на Gemini, обидва fail, disabled short-circuit), schemas (CRUD, інваріант "активна рівно одна", валідація розкладу, Amount-as-string, unicode).
💡 Рекомендований спосіб розробки: Дивіться README_DEV.md у корені проекту для запуску повного локального середовища розробки (включаючи бекенд, базу даних з авто-сідером та проксі-налаштування для фронтенду).
Потрібно: .NET 10 SDK, Docker, dotnet-ef (dotnet tool install -g dotnet-ef)
# Запустити БД
docker-compose up db -d
# Запустити API (міграції застосуються автоматично)
GEMINI_API_KEY=your_key dotnet run --project BpTracker.Api.csproj# Нова міграція після зміни моделей
dotnet ef migrations add <Name> --project BpTracker.Api.csprojРозгортання — вручну на сервері командами нижче.
Раніше деплой був автоматизований через Portainer GitOps Webhook на гілку
main. Webhook вимкнено — авто-деплой спрацьовував надто неявно; тепер розгортання свідоме й ручне.
cp .env.example .env
# відредагувати .env
docker-compose up --build -dСтек:
api— порт 5000db— PostgreSQL на 5436seq— Seq UI на 5341
Автоматичний бекап (контейнер pg-backup) було прибрано як зайвий. Для ручного бекапу:
# Створити бекап
docker exec bptracker-db pg_dump -U bp_user bp_tracker | gzip > backup-$(date +%Y%m%d).sql.gz
# Відновити
docker compose stop api
gunzip -c backup-YYYYMMDD.sql.gz | docker exec -i bptracker-db psql -U bp_user bp_tracker
docker compose start apiАльтернативи на майбутнє: ZFS-снапшоти на TrueNAS для папки де лежить Postgres volume, або періодичний cron хоста з командою вище.
- Додати ендпоінт: створити/оновити клас у
Endpoints/, додати метод розширення та зареєструвати його уProgram.cs. - Зміна БД: оновити модель у
Models/, виконатиdotnet ef migrations add <Name> --project BpTracker.Api.csproj. Міграції застосовуються автоматично при старті. - Бізнес-логіка: переважно у
Services/. Використовуйте DI (Constructor Injection).
- C#: PascalCase для класів, методів та публічних властивостей. camelCase для приватних полів (з префіксом
_). - JSON: camelCase (стандарт System.Text.Json).
- Database: PascalCase для імен таблиць та колонок (стандарт EF Core).
- CSRF: наразі відсутній спеціальний middleware для CSRF захисту (частково нівелюється
SameSite=LaxтаCustom Headerвимогами). - Cleanup:
CleanupWorkerвидаляє старі сесії, але наразі немає автоматичного очищення "Dead" листів уEmailOutbox. - Files: фотографії обробляються в пам'яті (
MemoryStream), що може бути неефективним для дуже великих файлів при високому навантаженні. - Семантика form fields: поля
geminiSys/geminiDia/geminiPulseу/measurements/with-photo— історична назва. Семантично це "AI suggestion" незалежно від двигуна. Не перейменовано для уникнення синхронної зміни фронтенду.
- Session context:
HttpContext.Userзаповнюється уMiddleware/SessionMiddleware.cs. КлаймNameIdentifierміститьGuidкористувача. - API Versioning: всі маршрути починаються з
/api/v1/. - Database: використовується
Npgsql.EntityFrameworkCore.PostgreSQL.TreatmentSchema.Id—Guid. Поля:Doctor(text),PrescribedOn(date),CreatedAt(timestamptz, default now()),IsActive(bool),ScheduleDocument(JsonDocument?, jsonb),UserId(uuid, FK наUsers). ФорматScheduleDocument: обʼєкт із ключамиMorning/Day/Evening, кожен — масив{Amount, Medicine, Condition}, усі значення рядки (зокремаAmount— рядок, напр."0.5"). У БД ключі канонічний PascalCase; вхід API приймає їх case-insensitive і нормалізує. Усі схеми та вимірювання ізольовані на рівні користувачів черезUserId, а часові поля (зокремаCreatedAtу схемах таRecordedAtу замірах) використовують типtimestamptzу БД таDateTimeOffsetу C#. - Логування: Serilog → Console + Seq. INFO-логи
Microsoft.*стеку відфільтровані до Warning для зменшення шуму. Власні логи писати черезILogger<T>injection. Для структурованих обʼєктів —{@Obj}синтаксис.