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
+39 -5
View File
@@ -35,12 +35,12 @@ from app.core.intake_runtime import intake_enabled, load_runtime
from app.core.interpretation import texts as results_texts
from app.core.meds_reference import find_drug, indication_for, load_reference
from app.core.safety import CrisisResource, crisis_reply
from app.db import repo
from app.db import identity_repo, repo
from app.db.audit import audit
from app.db.clinical import DailyCheckin, Dose, Prescription
from app.db.identity import TOKEN_LEN
from app.db.intake import QuestionnaireRecord
from app.db.session import get_clinical_session
from app.db.session import get_clinical_session, get_identity_session
from app.integrations.emias_mock import CODE_MAX_LEN, EmiasMock
from app.services import anonymous_care
@@ -81,14 +81,14 @@ async def _attach_doctor(token: str, doctor_id: str) -> None:
await repo.upsert_profile(s, token=token, doctor_id=doctor_id)
async def _seed_demo_persona(token: str) -> None:
async def _seed_demo_persona(token: str, source: str | None = None) -> None:
"""Synthetic clinical start for a demo persona (no `make seed` yet): profile, prescription, today's doses.
Idempotent: only missing pieces are created. Data mirror the dashboard fixtures, so the patient app and the
doctor's card tell the same story.
"""
card = fixtures.card(token)
item = next((p for p in fixtures.PATIENTS if p.token == token), None)
card = fixtures.card(source or token)
item = next((p for p in fixtures.PATIENTS if p.token == (source or token)), None)
if card is None or item is None:
return
now = clock.now()
@@ -153,6 +153,40 @@ async def auth_emias_demo(body: EmiasDemoIn) -> AuthOut:
return AuthOut(token=persona.token, mode="emias", display_name=persona.display_name)
DEMO_SOURCE = "tok-andrey"
@router.post("/auth/demo/fresh", response_model=AuthOut)
async def auth_demo_fresh() -> AuthOut:
"""Presentation entry: a fresh private copy of veteran Andrey for every sign-in — consent given, prescription
and ЕМИАС record in place, dialog resting in the chat. Nobody's crisis or half-done questionnaire leaks into
another viewer's demo. Synthetic data only."""
token = f"{DEMO_SOURCE}-{secrets.token_hex(4)}"
now = clock.now()
persona = next(p for p in fixtures.DEMO_PERSONAS if p.token == DEMO_SOURCE)
snils = f"000-{secrets.randbelow(10**3):03d}-{secrets.randbelow(10**3):03d} {secrets.randbelow(100):02d}"
async with get_identity_session() as s:
await identity_repo.create_patient(
s,
token=token,
display_name=f"{persona.display_name} (демо)",
snils=snils,
clinic_id="demo",
doctor_id=fixtures.DOCTOR_ID,
now=now,
birth_year=now.year - 34,
sex="m",
)
await identity_repo.record_consent(s, token=token, version="demo", now=now)
await _seed_demo_persona(token, source=DEMO_SOURCE)
async with get_clinical_session() as s:
await repo.upsert_profile(s, token=token, sex="m", birth_year=now.year - 34)
consent = {"consent": True, "consent_ver": "demo", "consent_at": now.isoformat(), "revoked_at": None}
await repo.set_dialog_state(s, token=token, state="idle", data={**consent, "adult": True}, now=now)
log.info("web auth: demo fresh tok=%s", token[:14])
return AuthOut(token=token, mode="emias", display_name=persona.display_name)
@router.post("/auth/anonymous", response_model=AuthOut)
async def auth_anonymous(body: AnonymousIn | None = None) -> AuthOut:
"""Anonymous sign-in: a fresh random token; care without an identity record.