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
+38
View File
@@ -0,0 +1,38 @@
import { createContext, useCallback, useContext, useMemo, useState, type ReactNode } from "react";
import type { AuthResult } from "../api/types";
import { sessionStore, type Session } from "./store";
interface SessionApi {
session: Session | null;
signIn: (auth: AuthResult) => void;
signOut: () => void;
}
const Ctx = createContext<SessionApi | null>(null);
export function SessionProvider({ children }: { children: ReactNode }) {
const [session, setSession] = useState<Session | null>(() => sessionStore.load());
const signIn = useCallback((auth: AuthResult) => {
const next: Session = { token: auth.token, mode: auth.mode, displayName: auth.display_name };
sessionStore.save(next);
setSession(next);
}, []);
const signOut = useCallback(() => {
setSession((current) => {
sessionStore.clear(current);
return null;
});
}, []);
const value = useMemo(() => ({ session, signIn, signOut }), [session, signIn, signOut]);
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
}
export function useSession(): SessionApi {
const value = useContext(Ctx);
if (!value) throw new Error("useSession outside SessionProvider");
return value;
}