Htracker — разбор проекта
Личный кабинет здоровья, который я спроектировал и собрал в одиночку — от схемы базы до интерфейса. Здесь — решения, которые я принял, и почему именно такие.
Зачем
Htracker — это личный кабинет здоровья. Пациент ведёт ежемесячные снимки показателей (рост, вес, давление, пульс, температура, самочувствие, сон), видит динамику по ним, фиксирует обследования, анализы и консультации, описывает болезни и курсы лечения с таймлайном, прикладывает файлы и открывает доступ к карте лечащему врачу. Есть и небольшая админка для управления пользователями.
Для меня это был не только продукт, но и полигон: реальная предметная область, на которой можно честно проверить, как живёт продуманная архитектура от схемы базы до интерфейса, когда всё делаешь сам и отвечаешь за каждое решение.
Демо и код — по запросу
Htracker развёрнут и работает на тестовых данных. Есть демо-аккаунты для трёх ролей — пациента, врача и админа, чтобы посмотреть систему с каждой стороны. Доступ к демо и к коду (приватный репозиторий) не выкладываю в открытую, чтобы его не растащили боты — напишите, и я дам логины или проведу по проекту и архитектуре лично.
Как это устроено
Бэкенд — FastAPI и PostgreSQL, выстроенные по Domain-Driven Design. Система разрезана на восемь bounded-контекстов (Identity, MedicalRecord, Examinations, Analyses, Consultations, Conditions, DoctorAccess, References), внутри каждого — четыре слоя: domain (агрегаты и инварианты, без единого импорта фреймворка), application (use cases), infrastructure (SQLAlchemy и репозитории), api (HTTP-обвязка). Зависимости идут строго в одну сторону, а контексты общаются только через application-use case'ы соседа и через lazy-import — так каждый контекст остаётся отдельно бутабельным.
Фронт — Vue 3 / TypeScript по Feature-Sliced Design: это фронтовое зеркало того же правила «связи между модулями — только через публичные точки входа». Типы фронтенда генерируются из OpenAPI-спеки, поэтому контракт API и клиент физически не могут разойтись. Авторизация — JWT в httpOnly-cookie с тихим refresh на 401. Есть самодокументирующаяся страница дизайн-системы /design.
Решения, которые я принял на этом проекте
Динамика показателей — это read-model, а не таблица
Месячный снимок (HealthCardStatic) — обычная строка в базе, immutable после закрытия месяца. А вот динамика (средние, тренд, отклонения от нормы, sparkline) нигде не хранится — она считается на лету чистой функцией по запрошенному окну.
Почему так: окно настраивается клиентом (1–36 месяцев), материализовать пришлось бы под каждое. Данных мало — агрегация в Python мгновенна, а калькулятор остаётся pure-функцией, которую тривиально покрыть юнит-тестом. База при этом не усложняется. Когда данных станет много — точка эволюции уже намечена: materialized view с fallback на ту же функцию.
Privacy by design: админ не видит медданные
Единственная точка авторизации для всех медицинских ресурсов — гард require_patient_access(pid). Пациент проходит, если это его карта; врач — если есть активная связь с пациентом; админ получает 403 всегда.
Почему так: админ — операционная роль (заводит пользователей, смотрит связи), а не надзорная. Доступ к диагнозам не должен быть побочным эффектом административных прав. Вся auth-логика собрана в одном месте — любое изменение правил идёт туда, а не размазывается по роутам.
Вложения — один flat-роут вместо роута на каждый тип записи
Файлы (PDF/JPEG/PNG) живут метаданными в JSONB прямо на родительской записи, а отдаются через единый /files/{fid}: общий резолвер находит владельца по attachment_id и применяет ту же проверку доступа.
Почему так: альтернатива — свой download-эндпоинт у каждого из трёх владельцев с дублированием auth-логики. Flat-роут даёт фронту один URL независимо от типа записи. А бинарь намеренно не в базе и не в отдельной polymorphic-таблице: сам файл лежит за Storage-протоколом (local FS сейчас, S3/MinIO — тривиальный swap), метаданные читаются атомарно вместе с агрегатом.
Границы архитектуры проверяет линтер, а не ревьюер
И на фронте, и на бэке направленность зависимостей — не договорённость на словах. На фронте её держат ESLint с eslint-plugin-boundaries и Steiger, на гейте pre-commit + CI.
Почему так: архитектура, которую соблюдают «по доброй воле», деградирует на первом дедлайне. Когда нарушение слоя — это красный CI, а не замечание на ревью, правило живёт само. Стилевые правила я при этом намеренно не включал — линтер сторожит только структуру.
Что получилось и что осталось за бортом
Ядро MVP доведено до рабочего состояния и развёрнуто: регистрация с авто-кабинетом, месячные карточки и динамика, CRUD обследований/анализов/консультаций с вложениями, болезни и курсы лечения со связями, invite-flow «врач ↔ пациент», админка, дизайн-система, OpenAPI с тремя UI.
Сознательно вне скоупа MVP: S3-хранилище (протокол готов, но один хост его пока не требует), возраст- и пол-зависимые нормы, уведомления, 2FA, multi-tenant. Это не «не успел», а отложенные по приоритету решения — для каждого в архитектуре намечена точка расширения. Честно: архитектура фронта ещё дозревает (FSD-рефактор шёл итерациями), и не всё из задуманного про дизайн-систему доехало до конца.
Как это деплоится
Прод — Docker-стек на моём сервере: PostgreSQL, FastAPI (миграции накатываются на старте) и nginx со статикой SPA. Снаружи — host-nginx с TLS от certbot, single-origin (всё под /api, без CORS). Выкат запускается не пушем, а публикацией релиза: CD-раннер собирает образы из тега и поднимает стек. Между «готово» и «выкатано» остаётся человеческий гейт — мне так спокойнее.
Этот же подход — собственный managed-сервер, Docker, nginx, CD — я считаю частью работы инженера, а не чем-то «не моим»: продукт целиком включает и то, как он живёт в проде.