pediatric-ai-scribe-v3/shared/types.ts
Daniel 9811e39cfe feat(client): port Admin panel — all 10 sub-tabs now run in React
Replaces the legacy-link shell at /app/admin with a proper
multi-sub-tab admin panel. Covers every module the vanilla admin.js
exposed: Users, Site settings, Announcement banner, AI models,
TTS / STT providers, SMTP, Email templates, AI prompts, Audit logs.

Priority 1 per Daniel's note: **Users**. Full CRUD flow ported —
list (with live filter), verify, disable/enable, set role
(user/moderator/admin), delete (with confirm modal), admin-side
password reset (inline modal). Self-protection rules preserved:
can't disable/delete yourself, can't demote your own admin role.

client/src/pages/Admin.tsx (rewritten)
  Sub-tab shell. Role check via useQuery(['auth-me']) reusing the
  Layout cache. 10 pills drive which panel renders. Access-denied
  card for non-admins (data-testid='admin-access-denied' unchanged).

client/src/pages/AdminPanels.tsx (new) — batch 1
  • AdminUsersTab — useQuery ['admin-users'] + 6 mutations
    (verify / disable / enable / set role / delete / reset password).
    Color-coded rows (disabled users opacity-60), inline role select,
    inline search filter over email+name.
  • AdminSettingsTab — GET /api/admin/settings → stats (totalUsers /
    totalApiCalls / todayApiCalls) + registration toggle.
  • AdminAnnouncementTab — reads announcement.{enabled,type,text} via
    /api/admin/config/announcement, saves via 3 parallel PUT
    /api/admin/config/<key> calls. Info/Warning/Critical severity
    select; text rendered in the top-of-page banner.

client/src/pages/AdminPanels2.tsx (new) — batch 2
  • AdminSmtpTab — host/port/user/pass/from/secure form + source
    badge (env / database / none). PUT /api/admin/config/smtp,
    DELETE /api/admin/config/smtp (with ConfirmModal). Inline test
    email sender (recipient + template) calling
    POST /api/admin/config/test-email.
  • AdminEmailTab — template selector (verify / reset /
    password-changed), subject + HTML body textarea. Pulls values
    from /api/admin/config, saves via 2 parallel PUT calls.
  • AdminPromptsTab — GET /api/admin/config/prompts populates the
    selector; textarea edits the active prompt; save via
    PUT /api/admin/config/prompt.<key>; reset-to-default via
    POST /api/admin/config/prompts/<key>/reset with ConfirmModal.
  • AdminModelsTab — GET /api/admin/config/models renders the
    provider-scoped model table with per-row Enabled checkbox +
    Default radio. Mutations hit /api/admin/config/models/toggle
    and /default. LiteLLM + model-discovery flows flagged as
    legacy-viewer follow-up.
  • AdminTtsTab / AdminSttTab — show active provider + voice/model
    selector, save default via PUT /api/admin/config/tts.default_voice
    and /stt.default_model respectively.
  • AdminLogsTab — GET /api/admin/logs/all?category=&limit= with
    sticky-header scrollable table. Category filter
    (auth / admin / clinical / export / integration / documents) and
    limit selector (50-500). Renders time / user / category /
    action / detail / IP per row.

shared/types.ts + client/src/shared/types.ts — additive only:
  AdminUser, AdminUsersOk, AdminUserOk, AdminSettingsOk,
  AdminLogEntry, AdminLogsOk, AdminConfigRow, AdminConfigOk,
  AdminAnnouncementOk, AdminPromptRow, AdminPromptsOk,
  AdminSmtpStatusOk, AdminModelRow, AdminModelsOk,
  AdminVoiceProviderOk.

With this commit the React sidebar covers the full legacy nav.
Access-control is preserved (Admin is still role-gated and the nav
link hides for non-admins). Every existing backend endpoint is
reused as-is — no server changes.

Backend tsc + client tsc + vite build + 136/136 vitest all green.
2026-04-24 02:27:50 +02:00

557 lines
15 KiB
TypeScript

// ============================================================
// 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<HpiOk> = 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<T> = ({ 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<string, unknown>;
}
// ── 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;
}