feat: presentation demo mode — one-click private demo patient, screening on/off switch
CI / ruff · pytest (pull_request) Failing after 2m33s

- POST /auth/demo/fresh + «Демо: войти как Андрей» on the welcome screen: every sign-in gets a fresh private
  copy of veteran Andrey (consent, prescription, ЕМИАС record) and lands in the chat — no codes, nobody's
  crisis or half-done questionnaire leaks into another viewer.
- Initial screening switch: SCREENING_ENABLED env default + /demo/settings + a toggle in the demo console;
  off → after consent straight to the chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 21:01:05 +03:00
co-authored by Claude Opus 5.5
parent 99e7b8e55d
commit cdd5ad9cde
13 changed files with 228 additions and 7 deletions
+2
View File
@@ -44,6 +44,8 @@ export const api = {
signInEmias: (code: string) => post<AuthResult>("/auth/emias", { code }),
signInEmiasDemo: (personaToken: string) => post<AuthResult>("/auth/emias/demo", { persona_token: personaToken }),
signInAnonymous: (focus?: Focus) => post<AuthResult>("/auth/anonymous", { focus }),
// Presentation entry: a fresh private copy of the demo patient, straight to the chat.
signInDemoFresh: () => post<AuthResult>("/auth/demo/fresh", {}),
patientCapabilities: () => request<{ primary_screening: boolean }>("/patient/capabilities"),
connectDoctor: (token: string) => post<{ has_doctor: boolean }>(`/me/${encodeURIComponent(token)}/doctor/connect`),
demoPersonas: () => request<DemoPersona[]>("/patient/demo-personas"),
+6
View File
@@ -41,6 +41,12 @@ export const ru = {
},
welcome: {
title: "Вход",
demo: {
title: "Демо: войти как Андрей",
text: "Ветеран СВО, 34 года, сертралин. Назначения и карта ЕМИАС уже подключены — сразу в чат.",
busy: "Готовим демо…",
error: "Не удалось открыть демо. Попробуйте ещё раз.",
},
emias: {
title: "Войти через ЕМИАС",
perks: [
+42
View File
@@ -1,5 +1,9 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { api } from "../api/client";
import { useSession } from "../session/SessionContext";
import { telHref, useHelp } from "../components/HelpSheet";
import { Icon, LogoMark } from "../components/Icon";
import { ru } from "../content/ru";
@@ -7,10 +11,26 @@ import { ru } from "../content/ru";
// A1 · Вход: без сайдбара, одна карточка по центру. ЕМИАС — основной путь, анонимный — по желанию
export function Welcome() {
const navigate = useNavigate();
const { signIn } = useSession();
const [demoBusy, setDemoBusy] = useState(false);
const [demoError, setDemoError] = useState(false);
const { resources, open } = useHelp();
const t = ru.welcome;
const emergency = resources[0];
// Presentation: one click → a fresh private demo patient → the chat.
const demo = async () => {
setDemoBusy(true);
setDemoError(false);
try {
signIn(await api.signInDemoFresh());
navigate("/messages", { replace: true });
} catch {
setDemoError(true);
setDemoBusy(false);
}
};
return (
<div className="signin-page">
<main className="signin" id="main">
@@ -25,6 +45,28 @@ export function Welcome() {
<section className="signin__card">
<h1 className="h1">{t.title}</h1>
<button type="button" className="entry entry--emias" onClick={demo} disabled={demoBusy}>
<span className="entry__top">
<span className="tile-icon tile-icon--teal">
<Icon name="check" size={26} />
</span>
<span className="entry__title">
{demoBusy ? t.demo.busy : t.demo.title}
<Icon name="chevronRight" />
</span>
</span>
<span className="muted small">{t.demo.text}</span>
</button>
{demoError && (
<p className="muted small center" role="alert">
{t.demo.error}
</p>
)}
<div className="divider" role="separator">
<span>{t.or}</span>
</div>
<button type="button" className="entry entry--emias" onClick={() => navigate("/emias")}>
<span className="entry__top">
<span className="tile-icon tile-icon--teal">