Status: draft
Кратко: на /health/{profileId} строим две вертикальных timeline’а — clinical (что происходило с пациентом в реальной жизни) и system (история нашего сервиса для этого пациента). Отображаются раздельно (toggle / отдельные таб’ы), не одновременно.
Контекст
У нас на /health нужна страница агрегирующая всю историю пациента. Источников событий несколько:
- Внешние клинические — загруженные документы (lab reports, discharge summaries, выписки), хроники из medical-context (Conditions), медикаменты из history (MedicationStatement), процедуры (Procedure), allergies, vital-signs
- Системные / наши — каждая загрузка файла (DocumentReference create), каждый AI-run (новая версия
Compositionс инкрементомversionId), каждое обновление medical-context
Эти два класса событий имеют две разных временных оси:
- Clinical date (
effectiveDateTime,onsetDate,recordedDateесли он = дате события) — когда событие произошло у пациента - System date (
meta.lastUpdated, ingestion time) — когда мы об этом узнали
Расхождение реальное: пациент загружает выписку 2018 года → clinical=2018, system=сегодня. Pure-system событие (AI-сводка обновилась) вообще не имеет clinical date.
См. предметный контекст и survey паттернов: [[../domain/patient-timeline-visualization]].
Рассматривали
A. Hybrid: один timeline, clinical primary axis + system как activity strip сверху
Главная ось — clinical date. Над ней тонкая полоса с system events (Composition versions, AI runs, document uploads).
Activity ──────────────●──●─────●──── (Composition v1 v2 v3, AI runs)
│ │ │
Clinical ──●───────●───┴──┴─●───┴──── (lab events, conditions)
2018 2024 2025 today
Плюс: два класса видны вместе, корреляция «после загрузки появилась версия сводки» очевидна. Минус: визуально перегруженно. Юзер должен переключаться между двумя «уровнями» внимания. Отвергнуто (Ильдар: «слишком сложно»).
B. Two timelines side-by-side (split view)
Слева — clinical, справа — system. Линки между ними показывают «этот document upload породил эту AI-сводку».
Плюс: оба класса видны полно, без потерь. Минус: ещё перегруженнее A. На mobile невозможно. Отвергнуто.
C. Один timeline, toggle clinical ↔ system
Один visual layout, switcher вверху. Один и тот же список, разные сортировки и фильтры под двумя режимами.
Плюс: дёшево по UX, отвечает обоим вопросам. Минус: теряется чёткое разделение «жизнь пациента» vs «жизнь данных в нашем сервисе» — юзер должен сам понимать что он сейчас смотрит.
D. Раздельные view’ы (выбранное)
Два полностью отдельных таба / страницы / view’а. Clinical = по умолчанию, system = secondary (для администратора, debug, audit). Контент разный, не просто переотсортированный.
Выбрали: D, раздельные view’ы
Почему:
- Семантика разная — clinical timeline отвечает на «что было в моей медицинской истории», system на «что наш сервис знает обо мне и когда». Это два разных вопроса, разные view’ы естественно их разделяют.
- AI-generated артефакты не путают clinical history. Композиции / AI-сводки / generations — не события «жизни пациента», а события «жизни данных у нас в системе». Им место на system axis, не на clinical.
- Multi-date documents правильно ложатся: один загруженный документ = одно событие в system (была загрузка), но N событий в clinical (если document содержал данные за N разных дат).
- Verticals pattern matches mobile-first (см. orientation в
[[../domain/patient-timeline-visualization]]).
Что показываем на каждом view
Clinical timeline
События из реальной жизни пациента. Источники:
| Источник | Событие на оси по | Иконка через |
|---|---|---|
DocumentReference (загрузка реальной выписки/лаб-отчёта) | дате документа (effectiveDateTime если есть, иначе LLM-extracted test_date) | type LOINC document class |
Observation (lab) | effectiveDateTime (день когда сдан анализ) | category: laboratory |
Observation (vital-signs) | effectiveDateTime | category: vital-signs |
Condition | onsetDateTime если есть, иначе recordedDate (но c пометкой что это inferred) | category: problem-list-item |
MedicationStatement | effectivePeriod.start если есть | category (TBD coverage — см. [[category-coverage]]) |
Procedure | performedDateTime если parsing успешен; иначе performedString displayed без точной позиции | category (TBD) |
AllergyIntolerance | recordedDate (allergy не имеет clinical onset обычно) | category (food/medication/environment/biologic) |
НЕ показываем на clinical timeline:
Composition(наши AI-сводки) — это не клинические события- AI run events — это о работе системы, не о пациенте
CarePlan(наши follow-up рекомендации) — TBD, может быть граница (рекомендация = part of care, может относиться к clinical?)
System timeline
История нашего сервиса для этого пациента. События:
| Событие | Ось по | Что показываем |
|---|---|---|
| Загрузка документа | meta.lastUpdated или upload time | «Загружен файл X» — клик → DocumentReference detail |
| AI run (Composition created/updated) | meta.lastUpdated | «Сводка обновлена → версия N» — клик → snapshot view (см. open вопрос про versioning UX) |
| CarePlan PUT | meta.lastUpdated | «План обновлён» |
| Medical context update | meta.lastUpdated | «Добавлены / обновлены X состояний / медикаментов» |
System timeline = activity log нашего сервиса. Ценен для audit (когда что появилось) + debug (Ильдар, support).
Multi-date document — правило N+1
Документ с N датами (например discharge summary с лабами за 3 даты) = 3 события в clinical timeline + 1 событие в system timeline.
Логика:
- В clinical: одна точка на ось за каждую уникальную clinical date событий внутри документа (3 DiagnosticReport-а / 3 группы Observation’ов с разными
effectiveDateTime) - В system: одно событие «загружен документ» в момент upload
Это согласуется с тем как мы уже emit’им FHIR ресурсы — recognize.orchestrator.ts создаёт N DiagnosticReport на N уникальных test_date. Просто визуализация это honor’ит.
«Patient brought old discharge» — двойное отображение
Сценарий: пациент загружает выписку 2018 года в 2026.
- На clinical timeline: события появляются в исторической позиции (2018) — на оси они среди других событий 2018-го года
- На system timeline: одно событие «загружен документ X» появляется в позиции «сегодня»
Это работает потому что clinical timeline сортируется по effectiveDateTime события, а system по meta.lastUpdated.
Следствия
- Двухтрековый рендеринг — одна и та же resource (например DocumentReference) может появиться на обеих timeline’ах с разной semantic меткой. На clinical — как «выписка из 2018», на system — как «загружен сегодня». Это feature, не дубль.
- CarePlan / Composition граница не закрыта — TBD что считать клиническим: если AI-рекомендация повлияла на medication change у врача, это часть clinical history? Сейчас выносим в system, но это может пересмотреться когда появятся integrations с реальной practice.
- Pre-conditions для иконок — нужен полный category coverage (см.
[[category-coverage]]). Сейчас MedicationStatement / Procedure не имеют category — иконка fallback на resource type. - Versioning UX TBD — что показывать когда юзер кликает на «Composition v3 created» событие. Варианты:
- Modal с diff между v3 и v2 (показывает «что изменилось»)
- Full snapshot view (показывает сводку как была в момент v3)
- Side-by-side comparison v2 vs v3
- Простая нав на read-only страницу
/health/.../snapshot/v3
Открытые вопросы
- Versioning UX (выше) — что отображаем при клике на Composition version event. Зависит от того, важнее «что изменилось» (diff) или «как было раньше» (snapshot). Скорее всего — full snapshot view, snapshot пользователю важнее
- CarePlan на каком timeline? — наши рекомендации это «жизнь данных» или «часть care»? Зависит от того, считаем ли AI-рекомендации частью медицинского журнала
- System timeline visibility для пациента — показывать ли B2C пользователю или только в admin/debug view? Сейчас идея — показывать (это transparency про то что AI делал когда), но требует careful UX
- Толщина / criticality на clinical timeline — выделять ли event’ы с triage codes (urgent_doctor) визуально? Связано с
[[../domain/parameter-triage-codes]] - Событие «Composition deleted / superseded» — как отображать? Strikethrough на старом event’е, или просто отсутствие новых версий
- Density — fallback на time-grouped headers — для long-history пациентов (10+ лет) нужен collapse «2018 (5 events)». Не в scope v1, но direction понятен
- Filtering по Resource.category — UI toggle’ы «показать только labs / только conditions». В scope v1 или v2?
Связано
[[../domain/patient-timeline-visualization]]— survey индустрии и академики, pattern taxonomy[[../domain/fhir-resource-categories]]— какие category выставлены / gap’ы[[../domain/fhir-composition]]— versionId mechanic для system timeline[[../domain/fhir-document-reference]]— корневое событие на обеих timeline’ах[[../domain/category-coverage]](TBD) — какие иконки берём[[../technical/health-report-versioning-model]]— canonical PUT решение даёт нам versionId 1→2→3
Источники
Источники: 1.
Сноски
-
FHIR
_historyдля versioning navigation, accessed 2026-05-17, https://learn.microsoft.com/en-us/azure/healthcare-apis/fhir/fhir-versioning-policy-and-history-management. ↩