feat(web): patient web app with ЕМИАС (demo) and anonymous sign-in

A second channel to the same dialog engine as the Telegram bot: consent,
onboarding, scales and check-ins come from app/dialog; the bot is unchanged.

- API: /auth/emias (doctor's one-time connect code via EmiasMock),
  /auth/emias/demo (synthetic personas), /auth/anonymous (random token,
  nothing stored in the identity DB), /crisis/resources from crisis.yaml
- web/: React + TS + Vite, mobile-first, safe areas, hash routing,
  tokens and texts isolated for a Capacitor mobile build

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 07:57:00 +03:00
co-authored by Claude Opus 5.5
parent 4d331e4c05
commit b8a93ce9d4
28 changed files with 4697 additions and 5 deletions
+103
View File
@@ -0,0 +1,103 @@
/*
* All interface texts of the patient app. No user-facing strings inside components (same rule as config/texts/
* for the bot). Dialog content (consent, questions, scales) comes from the engine and is NOT duplicated here.
*/
export const ru = {
product: {
name: "Сопровождение пациента",
tagline: "между визитами к врачу",
prototype: "Прототип · синтетические данные",
},
help: {
button: "Нужна помощь",
title: "Если вам сейчас очень тяжело",
lead: "Не ждите ответа врача, если помощь нужна прямо сейчас. Позвоните:",
call: "Позвонить",
close: "Закрыть",
fallbackName: "Экстренные службы",
fallbackNote: "если есть угроза жизни прямо сейчас",
},
welcome: {
title: "Лечение продолжается и между приёмами",
lead: "Напомним о лекарствах, раз в день спросим о самочувствии и сообщим врачу, если состоянию нужно внимание. На ответы уходит пара минут.",
day: {
caption: "Обычный день с сервисом",
meds: { time: "09:00", track: "Лекарства", text: "Утренний приём", done: "Принято" },
mood: { time: "14:00", track: "Самочувствие", text: "Как вы сегодня?", low: "Тяжело", high: "Хорошо" },
doctor: { time: "20:00", track: "Врач", text: "Динамика за неделю видна врачу" },
},
choose: "Как вы хотите войти?",
emias: {
title: "Через ЕМИАС",
badge: "Рекомендуем",
facts: [
{ ok: true, text: "Назначения врача подгружаются автоматически" },
{ ok: true, text: "Врач видит ваши ответы и получает сигнал при ухудшении" },
{ ok: true, text: "История доступна с любого устройства" },
],
action: "Войти через ЕМИАС",
},
anonymous: {
title: "Анонимно",
facts: [
{ ok: true, text: "Не нужны имя, полис и телефон" },
{ ok: true, text: "Опросы самочувствия и помощь в кризисе доступны" },
{ ok: false, text: "Врач не видит ваши ответы и не получает сигналов" },
{ ok: false, text: "История доступна только на этом устройстве" },
],
action: "Продолжить анонимно",
},
disclaimer: "Сервис не ставит диагнозы и не назначает лечение. Решения о лечении принимает ваш врач.",
},
emias: {
back: "Способы входа",
title: "Вход через ЕМИАС",
demoNotice:
"Демонстрационный режим. Авторизация в ЕМИАС не выполняется: вход подтверждается одноразовым кодом, который врач выдаёт на приёме.",
codeLabel: "Код подключения от врача",
codeHint: "Код указан в памятке или под QR-кодом, который вы получили на приёме.",
codePlaceholder: "Код из памятки",
submit: "Войти",
submitting: "Проверяем код…",
codeError: "Код не подошёл. Проверьте написание или попросите врача выдать новый код.",
networkError: "Сервис временно недоступен. Попробуйте ещё раз через минуту.",
or: "или",
demoTitle: "Демо-пациенты",
demoLead: "Нет кода? Войдите под синтетической учётной записью, чтобы посмотреть сценарий.",
demoDay: (day: number) => `${day}-й день терапии`,
demoAction: "Войти",
demoEmpty: "Демо-пациенты недоступны.",
},
anonymous: {
back: "Способы входа",
title: "Анонимный вход",
lead: "Мы не спрашиваем имя, полис и телефон. Ваши ответы хранятся под случайным идентификатором, по которому вас нельзя узнать.",
points: [
"Доступны опросы самочувствия, дневник и помощь в кризисной ситуации.",
"Лечащий врач не видит ваши ответы и не получает сигналов об ухудшении.",
"Перед началом вы дадите согласие на обработку ответов — без него ничего не сохраняется.",
],
warning:
"История привязана к этому устройству. Если вы выйдете или очистите данные браузера, восстановить её будет нельзя.",
submit: "Начать анонимно",
submitting: "Создаём анонимный профиль…",
},
chat: {
modeEmias: "ЕМИАС",
modeAnonymous: "Анонимно",
logout: "Выйти",
logoutConfirm: "Выйти? Анонимную историю восстановить будет нельзя.",
inputLabel: "Сообщение",
inputPlaceholder: "Напишите сообщение",
send: "Отправить",
loading: "Подключаемся…",
sendError: "Сообщение не отправлено. Проверьте соединение и попробуйте ещё раз.",
you: "Вы",
assistant: "Помощник",
crisisLabel: "Важно",
},
footer: {
law: "Обработка данных о здоровье — только с вашего согласия (152‑ФЗ).",
prototype: "Прототип для хакатона MedITron. Используются только синтетические данные.",
},
} as const;