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
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:
@@ -0,0 +1,6 @@
|
||||
<script lang="ts">
|
||||
import '../app.css';
|
||||
let { children } = $props();
|
||||
</script>
|
||||
|
||||
<main>{@render children()}</main>
|
||||
@@ -0,0 +1,48 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { api, type Sample } from '$lib/api';
|
||||
|
||||
let samples = $state<Sample[]>([]);
|
||||
let name = $state('');
|
||||
let vcfUri = $state('');
|
||||
let error = $state<string | null>(null);
|
||||
|
||||
async function refresh() { samples = await api.samples(); }
|
||||
onMount(refresh);
|
||||
|
||||
async function add() {
|
||||
error = null;
|
||||
try { await api.createSample({ name, vcf_uri: vcfUri }); name = ''; vcfUri = ''; await refresh(); }
|
||||
catch (e) { error = (e as Error).message; }
|
||||
}
|
||||
</script>
|
||||
|
||||
<h1>rarelens</h1>
|
||||
<p class="lede">Annotate a VCF with Ensembl VEP, score each variant, and browse what came back. Public test data only.</p>
|
||||
|
||||
<h2>Add a sample</h2>
|
||||
<div style="display:flex; gap:0.5rem; flex-wrap:wrap">
|
||||
<input placeholder="Sample name" bind:value={name} aria-label="Sample name" />
|
||||
<input placeholder="gs://bucket/sample.vcf.gz or /data/example.vcf.gz" bind:value={vcfUri} aria-label="VCF path" style="flex:1; min-width: 20rem" />
|
||||
<button onclick={add} disabled={!name || !vcfUri}>Add sample</button>
|
||||
</div>
|
||||
{#if error}<p role="alert">Could not add the sample: {error}</p>{/if}
|
||||
|
||||
<h2>Samples</h2>
|
||||
{#if samples.length === 0}
|
||||
<div class="empty">No samples yet. Add one above to run the annotation pipeline.</div>
|
||||
{:else}
|
||||
<table>
|
||||
<thead><tr><th>Name</th><th>VCF</th><th>Assembly</th><th>Added</th></tr></thead>
|
||||
<tbody>
|
||||
{#each samples as s (s.id)}
|
||||
<tr>
|
||||
<td><a href="/samples/{s.id}">{s.name}</a></td>
|
||||
<td class="hgvs">{s.vcf_uri}</td>
|
||||
<td>{s.assembly}</td>
|
||||
<td>{new Date(s.created_at).toLocaleDateString('en-GB')}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
@@ -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}
|
||||
@@ -0,0 +1,2 @@
|
||||
import type { PageLoad } from './$types';
|
||||
export const load: PageLoad = ({ params }) => ({ sampleId: params.id });
|
||||
Reference in New Issue
Block a user