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’ы

Почему:

  1. Семантика разная — clinical timeline отвечает на «что было в моей медицинской истории», system на «что наш сервис знает обо мне и когда». Это два разных вопроса, разные view’ы естественно их разделяют.
  2. AI-generated артефакты не путают clinical history. Композиции / AI-сводки / generations — не события «жизни пациента», а события «жизни данных у нас в системе». Им место на system axis, не на clinical.
  3. Multi-date documents правильно ложатся: один загруженный документ = одно событие в system (была загрузка), но N событий в clinical (если document содержал данные за N разных дат).
  4. 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)effectiveDateTimecategory: vital-signs
ConditiononsetDateTime если есть, иначе recordedDate (но c пометкой что это inferred)category: problem-list-item
MedicationStatementeffectivePeriod.start если естьcategory (TBD coverage — см. [[category-coverage]])
ProcedureperformedDateTime если parsing успешен; иначе performedString displayed без точной позицииcategory (TBD)
AllergyIntolerancerecordedDate (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 PUTmeta.lastUpdated«План обновлён»
Medical context updatemeta.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.

Сноски

  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.