// Dynamic, not static: the same image serves /api behind the ingress and a full URL elsewhere. import { env } from '$env/dynamic/public'; import { candidateQuery, type CandidateFilters } from './candidates'; export type Assembly = 'GRCh38' | 'GRCh37'; export type JobStatus = 'queued' | 'running' | 'succeeded' | 'failed'; export type PhenotypeTerm = { hpo_id: string; label: string }; export type Job = { id: string; case_id: string; status: JobStatus; vep_version: string | null; log: string | null; created_at: string; finished_at: string | null; }; export type Case = { id: string; name: string; vcf_uri: string; assembly: Assembly; created_at: string; phenotypes: PhenotypeTerm[]; latest_job: Job | null; shortlisted: number; }; export type Variant = { id: number; chrom: string; pos: number; ref: string; alt: string; gene: string | null; consequence: string | null; impact: string | null; hgvsc: string | null; hgvsp: string | null; gnomad_af: number | null; clinvar_sig: string | null; prediction: { score: number; model_name: string; model_version: string } | null; }; export type DecisionState = 'shortlisted' | 'dismissed'; export type Decision = { state: DecisionState; reason: string | null; note: string | null; decided_at: string; }; export type Candidate = { variant: Variant; score: number; components: Record; matched_terms: PhenotypeTerm[]; scored: boolean; decision: Decision | null; }; export type VariantDetail = Candidate & { annotations: Record }; export type Funnel = { total: number; rare: number; candidates: number; phenotype_matched: number }; export type CandidatePage = { funnel: Funnel; weights: Record; items: Candidate[]; total: number; limit: number; offset: number; }; export type Report = { case: Case; funnel: Funnel; generated_at: string; provenance: { job_id: string | null; vep_version: string | null; finished_at: string | null; model_name: string | null; model_version: string | null; }; shortlisted: Candidate[]; dismissed: Candidate[]; top: Candidate[]; }; async function req(path: string, init?: RequestInit): Promise { const r = await fetch(`${env.PUBLIC_API_URL ?? '/api'}${path}`, { headers: { 'content-type': 'application/json' }, ...init }); if (!r.ok) throw new Error(await errorReason(r)); return r.json() as Promise; } // FastAPI puts the reason in `detail`: a string, or a list of validation errors. async function errorReason(r: Response): Promise { const fallback = `${r.status} ${r.statusText}`; try { const { detail } = await r.json(); if (typeof detail === 'string') return detail; if (Array.isArray(detail)) return detail.map((d: { msg?: string }) => d.msg ?? '').join('; ') || fallback; } catch { // not JSON (e.g. a proxy error page) } return fallback; } const post = (body?: unknown): RequestInit => ({ method: 'POST', body: body === undefined ? undefined : JSON.stringify(body) }); export const api = { cases: () => req('/cases'), createCase: (body: { name: string; vcf_uri: string; assembly: Assembly; phenotypes: PhenotypeTerm[] }) => req('/cases', post(body)), case: (id: string) => req(`/cases/${id}`), annotate: (id: string) => req(`/cases/${id}/annotate`, post()), score: (id: string) => req<{ scored: number; model_version: string }>(`/cases/${id}/score`, post()), job: (id: string) => req(`/jobs/${id}`), candidates: (caseId: string, filters: CandidateFilters = {}) => { const query = candidateQuery(filters); return req(`/cases/${caseId}/candidates${query ? `?${query}` : ''}`); }, variant: (id: number) => req(`/variants/${id}`), decide: (id: number, body: { state: DecisionState; reason?: string; note?: string }) => req(`/variants/${id}/decision`, post(body)), report: (caseId: string) => req(`/cases/${caseId}/report`), phenotypes: (q: string) => req(`/phenotypes?q=${encodeURIComponent(q)}`) };