// ============================================================ // SHARED TYPES — consumed by both server (src/routes/*.ts) and // client (client/src/*). A response-shape change here breaks the // build on both sides until they agree. // // This is the single most important file in the TypeScript // migration. Three of the bugs we hit before this point were // response-shape mismatches (refine returned `content` vs // `refined`, sick-visit endpoint path mismatch, hospital-course // key mismatch). Typing the wire protocol makes them compile-time // errors. // // Keep this file strictly about wire protocol — no DB row shapes, // no server-internal helpers. Anything that crosses the network. // ============================================================ // ── Envelope ───────────────────────────────────────────────── // Every route returns either `{success: true, ...extraFields}` or // `{success: false, error: string}`. The generic `T` is the shape // of the extra fields on success. Client code does: // const r: ApiResponse = await fetch(...).then(r => r.json()); // if (r.success) console.log(r.hpi); // else showToast(r.error); export interface ApiErr { success: false; error: string; } export type ApiResponse = ({ success: true } & T) | ApiErr; // Model tag accompanies every AI response — LiteLLM pass-through. export interface WithModel { model: string; } // ── AI generation responses ────────────────────────────────── // Keys match exactly what each route returns today. Do not rename // without updating the server-side res.json() call in the same commit. // /api/generate-hpi-encounter // /api/generate-hpi-dictation export interface HpiOk extends WithModel { hpi: string; } // /api/generate-soap export interface SoapOk extends WithModel { soap: string; } // /api/sick-visit/note (NOT /api/generate-sick-visit — that path does not exist) // /api/well-visit/note export interface VisitNoteOk extends WithModel { note: string; } // /api/generate-hospital-course export interface HospitalCourseOk extends WithModel { hospitalCourse: string; format?: string; } // /api/generate-chart-review export interface ChartReviewOk extends WithModel { review: string; } // /api/generate-pe-narrative export interface PeNarrativeOk extends WithModel { narrative: string; summary: { normal: number; abnormal: number; notAssessed: number; }; } // /api/generate-milestone-narrative export interface MilestoneNarrativeOk extends WithModel { narrative: string; summary: { achieved: number; notAchieved: number; notAssessed: number; }; } // /api/generate-milestone-summary export interface MilestoneSummaryOk extends WithModel { summary: string; } // /api/well-visit/shadess export interface ShadessOk extends WithModel { assessment: string; } // /api/refine export interface RefineOk extends WithModel { refined: string; } // /api/shorten (via refine.js router) export interface ShortenOk extends WithModel { shortened: string; } // /api/clarify and /hospital-course/clarify export interface ClarifyOk extends WithModel { questions: string; } // /api/suggest-billing-codes export interface BillingCodesOk extends WithModel { icd10: Array<{ code: string; description: string; reason?: string }>; cpt: Array<{ code: string; description: string; reason?: string }>; } // /api/transcribe export interface TranscribeOk { text: string; provider: string; duration: number; } // /api/tts export interface TtsOk { audioBase64: string; } // /api/models export interface ModelsOk { models: Array<{ id: string; label?: string }>; } // ── Auth ───────────────────────────────────────────────────── export interface AuthUser { id: number; email: string; name: string; role?: string; isVerified?: boolean; has2FA?: boolean; // canLocalAuth: false for SSO-auto-created accounts whose password is a // random hex blob that can never verify. Settings hides password/2FA UI // for those users. totp_enabled / email_verified / nextcloud_* mirror // DB columns returned by /api/auth/me. canLocalAuth?: boolean; totp_enabled?: boolean; email_verified?: boolean; nextcloud_url?: string | null; nextcloud_user?: string | null; nextcloud_folder?: string | null; webdav_learning_path?: string | null; created_at?: string; } // /api/auth/login (two-phase: may also return requires2FA / needsVerification) export interface LoginOk { token: string; user: AuthUser; sessionId?: string; } export interface LoginRequires2FA { success: true; requires2FA: true; } export interface LoginNeedsVerification { success: true; needsVerification: true; } // /api/auth/me export interface MeOk { user: AuthUser; } // ── Sessions ───────────────────────────────────────────────── // Keys match the wire shape returned by /api/sessions (snake_case from // the DB columns, deliberately unchanged to keep the existing vanilla // client working during migration). export interface SessionRow { id: string; ip_address?: string | null; device_label?: string | null; created_at: string; last_activity: string; } export interface SessionsOk { sessions: SessionRow[]; currentSessionId: string | null; } export interface RevokeAllSessionsOk { revoked?: number; } // ── 2FA + password change ──────────────────────────────────── // /api/auth/setup-2fa export interface Setup2faOk { secret: string; qrCode: string; } // /api/auth/verify-2fa — backupCodes populated only on first enable export interface Verify2faOk { backupCodes: string[] | null; } // /api/auth/2fa/backup-codes/count export interface BackupCodesCountOk { remaining: number; } // /api/auth/2fa/backup-codes (regenerate) export interface RegenBackupCodesOk { codes: string[]; message?: string; } // /api/auth/change-password export interface ChangePasswordOk { message: string; passwordWarning?: string; } // ── Integrations ───────────────────────────────────────────── // /api/nextcloud/connect export interface NextcloudConnectOk { message: string; } // /api/nextcloud/disconnect — {success: true} // /api/documents — shape returned to the client export interface UserDocument { id: number; filename: string; mime_type: string; size_bytes: number; description?: string | null; created_at: string; } export interface DocumentsListOk { documents: UserDocument[]; s3_configured: boolean; } // /api/documents/upload — multipart; response below export interface DocumentUploadOk { id: number; filename: string; } // /api/documents/:id/download — returns a short-lived presigned URL export interface DocumentDownloadOk { url: string; } // ── Voice prefs + transcription settings ───────────────────── // /api/user/preferences export interface UserPreferencesOk { stt_model: string | null; tts_voice: string | null; } // /api/user/preferences/options — the provider-scoped lists of models/voices export interface VoiceOption { value: string; label: string; } export interface PreferencesOptionsOk { sttProvider: string; sttModels: VoiceOption[]; ttsProvider: string; ttsVoices: VoiceOption[]; } // ── Saved encounters list (Settings view) ──────────────────── // Note: /api/encounters/saved returns a richer row than the sidebar // EncounterSummary. The Settings list only needs these fields. export interface SavedEncounterRow { id: number; label: string; enc_type: string; status?: string | null; created_at: string; updated_at: string; expires_at: string; transcript_preview?: string; note_preview?: string; } export interface SavedEncountersListOk { encounters: SavedEncounterRow[]; } // ── Audio backups (server-stored recordings, 24h TTL) ──────── export interface AudioBackupRow { id: number; module: string; mime_type: string; size_bytes: number; compressed_bytes?: number; created_at: string; expires_at: string; } export interface AudioBackupsListOk { backups: AudioBackupRow[]; } // ── Memories (templates + corrections share this shape) ────── // Extends the minimal Memory type with fields needed by the Settings // list view (corrections need created_at to show dates). export interface MemoryRow { id: number; category: string; name: string; content: string; created_at?: string; } export interface MemoriesOk { memories: MemoryRow[]; } // ── Admin ─────────────────────────────────────────────────── // /api/admin/users (admin-gated) export interface AdminUser { id: number; email: string; name: string; role: string | null; email_verified: boolean; totp_enabled: boolean; disabled: boolean; created_at: string; updated_at?: string; nextcloud_url?: string | null; api_calls?: number; last_login?: string | null; } export interface AdminUsersOk { users: AdminUser[] } export interface AdminUserOk { user: AdminUser } export interface AdminSettingsOk { settings: { registrationEnabled: boolean }; stats: { totalUsers: number; totalApiCalls: number; todayApiCalls: number }; } export interface AdminLogEntry { id: number; user_id: number | null; action: string; detail: string; category: string; ip_address?: string | null; timestamp: string; user_email?: string | null; user_name?: string | null; } export interface AdminLogsOk { logs: AdminLogEntry[] } // /api/admin/config / /api/admin/config/:key export interface AdminConfigRow { key: string; value: string | null; description?: string | null; source?: 'env' | 'db' | 'openbao' | string; } export interface AdminConfigOk { config: AdminConfigRow[] } export interface AdminAnnouncementOk { enabled: boolean; message: string; kind?: string; } // /api/admin/config/prompts export interface AdminPromptRow { key: string; value: string; description?: string; default?: string; isDefault?: boolean; } export interface AdminPromptsOk { prompts: AdminPromptRow[] } // /api/admin/config/smtp/status export interface AdminSmtpStatusOk { configured: boolean; host?: string; port?: number; user?: string; from?: string; } // /api/admin/config/models export interface AdminModelRow { id: string; label?: string; provider?: string; enabled: boolean; isDefault?: boolean; isCustom?: boolean; tags?: string[]; } export interface AdminModelsOk { models: AdminModelRow[]; provider?: string; defaultModel?: string | null; } // /api/admin/config/tts and /stt export interface AdminVoiceProviderOk { provider: string; enabled: boolean; voice?: string | null; model?: string | null; endpoint?: string | null; extra?: Record; } // ── Learning Hub (user-facing) ────────────────────────────── // Categories export interface LearningCategory { id: number; name: string; slug: string; description?: string | null; } export interface LearningCategoriesOk { categories: LearningCategory[]; } // Feed / category / search list rows — same shape across all list endpoints export interface LearningFeedRow { id: number; title: string; slug: string; subject?: string | null; content_type: 'article' | 'pearl' | 'presentation' | 'quiz' | string; created_at: string; updated_at?: string; category_name?: string | null; category_slug?: string | null; author_name?: string | null; question_count?: number; score?: number; match_type?: 'keyword' | 'semantic'; } export interface LearningFeedListOk { content: LearningFeedRow[]; total?: number; method?: 'keyword' | 'semantic' | 'hybrid'; } // Single content with questions + progress export interface LearningOption { id: number; option_text: string; sort_order: number; } export interface LearningQuestion { id: number; question_text: string; question_type: 'single' | 'multi' | 'true_false' | string; explanation?: string | null; options: LearningOption[]; } export interface LearningProgressEntry { score: number; total: number; completed_at: string; } export interface LearningContentFull { id: number; title: string; slug: string; subject?: string | null; body?: string; content_type: string; category_name?: string | null; category_slug?: string | null; author_name?: string | null; questions: LearningQuestion[]; progress: LearningProgressEntry[]; } export interface LearningContentOk { content: LearningContentFull; } // Quiz submit export interface QuizAnswer { questionId: number; optionId?: number | null; optionIds?: number[]; } export interface QuizResultEntry { questionId: number; questionType: string; questionText: string; isCorrect: boolean; selectedOptionId?: number | null; selectedOptionIds?: number[]; correctOptionId?: number | null; correctOptionIds?: number[]; correctOptionText?: string; selectedExplanation?: string; generalExplanation?: string; } export interface QuizSubmitOk { score: number; total: number; percentage: number; results: QuizResultEntry[]; } // ── Extensions (pagers/directory) ──────────────────────────── export interface Extension { id: number; location: string; name: string; number: string; type: 'extension' | 'pager'; notes?: string; deletedAt?: string | null; } export interface ExtensionsListOk { items: Extension[]; } // ── Memories (saved templates / style hints) ───────────────── export interface Memory { id: number; category: string; name: string; content: string; } export interface MemoriesListOk { items: Memory[]; } // ── Learning hub ───────────────────────────────────────────── export interface LearningContentItem { id: number | string; slug?: string; title: string; category?: string; excerpt?: string; body?: string; } export interface LearningFeedOk { content: LearningContentItem[]; total?: number; } // ── Encounters (saved drafts) ──────────────────────────────── export interface EncounterSummary { id: number; label: string; type: 'encounter' | 'dictation' | 'hospital' | 'chart' | 'wellvisit' | 'sickvisit' | 'soap'; createdAt: string; updatedAt: string; } export interface EncountersListOk { items: EncounterSummary[]; } // ── Health ─────────────────────────────────────────────────── export interface HealthOk { ok: boolean; }