Initial release: rarelens platform skeleton (AGPL-3.0)
ci / api (push) Failing after 10s
ci / terraform (push) Failing after 11s
ci / web (push) Failing after 35s
ci / pipeline (push) Failing after 2m29s
ci / images (api) (push) Skipped
ci / images (ml) (push) Skipped
ci / images (pipeline) (push) Skipped
ci / images (web) (push) Skipped

End-to-end variant interpretation platform for rare genetic disease research:
SvelteKit UI, FastAPI + PostgreSQL API, Nextflow/Ensembl VEP pipeline,
LightGBM pathogenicity scoring with MLflow, K8s/ArgoCD/GCP infrastructure.
Public test data only; no clinical claims.
This commit is contained in:
2026-09-11 16:55:35 +01:00
commit 5463f489a3
74 changed files with 4597 additions and 0 deletions
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api, type Job, type VariantPage } from '$lib/api';
let { data } = $props();
let job = $state<Job | null>(null);
let page = $state<VariantPage | null>(null);
let gene = $state('');
let impact = $state('');
let maxAf = $state('0.01');
let busy = $state(false);
onMount(async () => {
const jobs = await api.jobsForSample(data.sampleId);
const done = jobs.find(j => j.status === 'succeeded');
if (done) { job = done; await loadVariants(); }
else if (jobs.length) job = jobs[0];
});
async function runAnnotation() {
busy = true;
job = await api.annotate(data.sampleId);
const poll = setInterval(async () => {
if (!job) return;
job = await api.job(job.id);
if (job.status === 'succeeded' || job.status === 'failed') { clearInterval(poll); busy = false; if (job.status === 'succeeded') await loadVariants(); }
}, 3000);
}
async function loadVariants() {
if (!job) return;
const q: Record<string, string> = { job_id: job.id, max_af: maxAf, limit: '100' };
if (gene) q.gene = gene;
if (impact) q.impact = impact;
page = await api.variants(q);
}
const scoreClass = (s: number) => (s >= 0.8 ? 'score high' : s >= 0.5 ? 'score mid' : 'score');
</script>
<a href="/">All samples</a>
<h1>Sample {data.sampleId.slice(0, 8)}</h1>
{#if !job}
<button onclick={runAnnotation} disabled={busy}>Run VEP annotation</button>
{:else}
<p>Job <span class="hgvs">{job.id.slice(0, 8)}</span>: <span class="status {job.status}">{job.status}</span></p>
{/if}
{#if job?.status === 'succeeded'}
<h2>Variants</h2>
<div style="display:flex; gap:0.5rem; flex-wrap:wrap; margin-bottom:0.75rem">
<input placeholder="Gene symbol" bind:value={gene} aria-label="Gene" />
<select bind:value={impact} aria-label="Impact">
<option value="">Any impact</option><option>HIGH</option><option>MODERATE</option><option>LOW</option><option>MODIFIER</option>
</select>
<label>Max gnomAD AF <input type="number" step="0.001" min="0" max="1" bind:value={maxAf} style="width:6rem" /></label>
<button class="quiet" onclick={loadVariants}>Apply filters</button>
</div>
{#if page && page.items.length === 0}
<div class="empty">No variants match these filters. Raise the allele frequency cap or clear the gene filter.</div>
{:else if page}
<p style="color:var(--ink-soft)">{page.total} variants, showing {page.items.length}</p>
<table>
<thead><tr><th>Position</th><th>Gene</th><th>Consequence</th><th>HGVS</th><th>gnomAD AF</th><th>ClinVar</th><th>Score</th></tr></thead>
<tbody>
{#each page.items as v (v.id)}
<tr>
<td class="coord">{v.chrom}:{v.pos} {v.ref}>{v.alt}</td>
<td>{v.gene ?? ''}</td>
<td>{v.consequence ?? ''}<br /><small style="color:var(--ink-soft)">{v.impact ?? ''}</small></td>
<td class="hgvs">{v.hgvsp ?? v.hgvsc ?? ''}</td>
<td>{v.gnomad_af?.toExponential(2) ?? 'absent'}</td>
<td>{v.clinvar_sig ?? ''}</td>
<td>{#if v.prediction}<span class={scoreClass(v.prediction.score)}>{v.prediction.score.toFixed(2)}</span>{:else}<span style="color:var(--ink-soft)">unscored</span>{/if}</td>
</tr>
{/each}
</tbody>
</table>
{/if}
{/if}
+2
View File
@@ -0,0 +1,2 @@
import type { PageLoad } from './$types';
export const load: PageLoad = ({ params }) => ({ sampleId: params.id });