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
+61
View File
@@ -0,0 +1,61 @@
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { api } from "../api/client";
import { AppShell, BackLink, Notice } from "../components/AppShell";
import { Icon } from "../components/Icon";
import { ru } from "../content/ru";
import { useSession } from "../session/SessionContext";
export function AnonymousSignIn() {
const t = ru.anonymous;
const navigate = useNavigate();
const { signIn } = useSession();
const [busy, setBusy] = useState(false);
const [error, setError] = useState(false);
const start = async () => {
setBusy(true);
setError(false);
try {
signIn(await api.signInAnonymous());
navigate("/chat", { replace: true });
} catch {
setError(true);
setBusy(false);
}
};
return (
<AppShell>
<main className="page" id="main">
<div className="narrow">
<BackLink to="/">{t.back}</BackLink>
<section className="panel" aria-labelledby="anon-title">
<h1 id="anon-title" className="h2">
{t.title}
</h1>
<p className="lead lead--sm">{t.lead}</p>
<ul className="facts facts--spaced">
{t.points.map((p) => (
<li key={p} className="fact">
<Icon name="check" size={18} />
<span>{p}</span>
</li>
))}
</ul>
<Notice tone="warning">{t.warning}</Notice>
{error && (
<p className="field__error" role="alert">
{ru.emias.networkError}
</p>
)}
<button type="button" className="btn btn--primary btn--block" onClick={start} disabled={busy}>
{busy ? t.submitting : t.submit}
</button>
</section>
</div>
</main>
</AppShell>
);
}