Second of three commits porting the vanilla settings.html. This one
delivers the two Integrations sub-sections, rendered below the Security
block and shown to every authenticated user (not gated by canLocalAuth —
SSO users also integrate Nextcloud and manage documents).
client/src/pages/Settings.tsx — NextcloudCard
Form + status line driven by /api/auth/me. Connect POSTs
/api/nextcloud/connect (nextcloudUrl / username / appPassword), which
does a PROPFIND probe against the remote, creates the target folder
via MKCOL, and encrypts the app password at rest. On success we
invalidate the ['auth-me'] query so the status line flips to
"Connected to …" without a reload. Disconnect goes through a
ConfirmModal (not a native confirm) and POSTs /api/nextcloud/disconnect.
When connected, a second row exposes the "Learning Hub — Default
Browse Path" input backed by POST /api/user/webdav-path. (That handler
lives inline in server.ts, not in userPreferences.ts — a quirk of the
existing codebase that the port preserves.)
client/src/pages/Settings.tsx — DocumentsCard
React Query feed off /api/documents. When S3 is not configured the
server returns { s3_configured: false } and we render a static notice
instead of the upload area (same branch as vanilla documents.js). The
upload form bypasses the JSON api wrapper to send multipart FormData
directly via fetch with credentials: 'include' (cookie auth continues
to work). Downloads hit /api/documents/:id/download to receive a 5-min
presigned URL which we open in a new tab. Delete goes through the
shared ConfirmModal — replaces the vanilla showConfirm({ danger, … }).
Downloading-state spinner is per-row (useMutation.variables === doc.id)
so other rows stay clickable while one is in flight.
shared/types.ts + client/src/shared/types.ts
Additive only:
- AuthUser gains webdav_learning_path — already returned by
/api/auth/me but missing from the type.
- New response shapes: NextcloudConnectOk, UserDocument,
DocumentsListOk, DocumentUploadOk, DocumentDownloadOk.
e2e/tests/settings-react-integrations.spec.js
Four smoke tests: field presence, empty-form validation error, the
S3-configured-or-notice branch renders, and a repeat of the
no-native-dialog guard covering the Integrations interactions.
Client tsc -b, server tsc --noEmit, and vite build all pass locally.
Bundle 404.56 kB / 117.12 kB gzipped (+9 kB over commit 1). The e2e
container still predates /app/*; running these specs needs a rebuild.
307 lines
8.7 KiB
TypeScript
307 lines
8.7 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;
|
|
}
|
|
|
|
// ── 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;
|
|
}
|