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:
@@ -0,0 +1,74 @@
|
||||
import type { AuthMode, Reply } from "../api/types";
|
||||
|
||||
/*
|
||||
* Session persistence behind one small interface. Web uses localStorage; the mobile shell swaps the backend for
|
||||
* secure device storage (e.g. Capacitor Preferences / Keychain) without touching screens.
|
||||
*/
|
||||
|
||||
export interface Session {
|
||||
token: string;
|
||||
mode: AuthMode;
|
||||
displayName: string | null;
|
||||
}
|
||||
|
||||
export type ChatEntry =
|
||||
| { id: string; from: "assistant"; reply: Reply; answered: boolean }
|
||||
| { id: string; from: "patient"; text: string };
|
||||
|
||||
interface KeyValue {
|
||||
get(key: string): string | null;
|
||||
set(key: string, value: string): void;
|
||||
remove(key: string): void;
|
||||
}
|
||||
|
||||
const browserStorage: KeyValue = {
|
||||
get: (k) => {
|
||||
try {
|
||||
return window.localStorage.getItem(k);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
set: (k, v) => {
|
||||
try {
|
||||
window.localStorage.setItem(k, v);
|
||||
} catch {
|
||||
/* private mode / quota: the session simply is not remembered */
|
||||
}
|
||||
},
|
||||
remove: (k) => {
|
||||
try {
|
||||
window.localStorage.removeItem(k);
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const SESSION_KEY = "ptsr.session.v1";
|
||||
const chatKey = (token: string) => `ptsr.chat.v1.${token}`;
|
||||
|
||||
function read<T>(storage: KeyValue, key: string): T | null {
|
||||
const raw = storage.get(key);
|
||||
if (!raw) return null;
|
||||
try {
|
||||
return JSON.parse(raw) as T;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function createSessionStore(storage: KeyValue = browserStorage) {
|
||||
return {
|
||||
load: () => read<Session>(storage, SESSION_KEY),
|
||||
save: (s: Session) => storage.set(SESSION_KEY, JSON.stringify(s)),
|
||||
clear: (s: Session | null) => {
|
||||
storage.remove(SESSION_KEY);
|
||||
if (s) storage.remove(chatKey(s.token));
|
||||
},
|
||||
loadChat: (token: string) => read<ChatEntry[]>(storage, chatKey(token)) ?? [],
|
||||
saveChat: (token: string, entries: ChatEntry[]) => storage.set(chatKey(token), JSON.stringify(entries)),
|
||||
};
|
||||
}
|
||||
|
||||
export const sessionStore = createSessionStore();
|
||||
Reference in New Issue
Block a user