Skip to content

Commit 9fb8dd8

Browse files
committed
Fix 4 bugs found in deep audit
BamViewer: - B1: Count cards showed truncated read counts (max 80) instead of real totals. Now uses view.counts from backend (all reads). - B2: BamCell variant site coloring was case-sensitive — lowercase BAM bases didn't match uppercase VCF alleles. Now uses toUpperCase(). Backend: - B3: ensure_fasta_index silently wrote corrupt .fai for gzipped FASTA (byte offsets meaningless in compressed files). Now returns error asking user to decompress first. App.tsx: - B4: Ctrl/Cmd+Enter shortcut bypassed overwrite conflict check, going directly to handleRunAll. Extracted shared triggerRun() function used by both the button onClick and keyboard shortcut. Both now check for existing files before running.
1 parent 6a092ee commit 9fb8dd8

3 files changed

Lines changed: 49 additions & 41 deletions

File tree

frontend/src/App.tsx

Lines changed: 33 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -609,20 +609,46 @@ function App() {
609609
// Count pending + error samples (runnable)
610610
const runnableCount = samples.filter((s) => s.status === "pending" || s.status === "error").length;
611611

612+
// Shared run trigger — checks for overwrite conflicts before launching
613+
const triggerRun = useCallback(async () => {
614+
if (!filesReady || samples.length === 0) {
615+
setShowValidation(true);
616+
return;
617+
}
618+
setShowValidation(false);
619+
620+
if (runnableCount > 0) {
621+
try {
622+
const conflicts = await invoke<string[]>("check_output_conflicts", { config: configRef.current });
623+
if (conflicts.length > 0) {
624+
const names = conflicts.map((p: string) => p.split(/[\\/]/).pop()).join(", ");
625+
const ok = window.confirm(
626+
`The following output files already exist and will be overwritten:\n\n${names}\n\nContinue?`
627+
);
628+
if (!ok) return;
629+
}
630+
} catch {
631+
// If check fails, proceed anyway
632+
}
633+
handleRunAll();
634+
} else {
635+
handleRerunAll();
636+
}
637+
}, [filesReady, samples.length, runnableCount, handleRunAll, handleRerunAll]);
638+
612639
// Ctrl+Enter / Cmd+Enter shortcut to run analysis
613-
// Use a ref to always capture the latest handleRunAll without re-registering the listener
614-
const handleRunAllRef = useRef(handleRunAll);
615-
handleRunAllRef.current = handleRunAll;
640+
const triggerRunRef = useRef(triggerRun);
641+
triggerRunRef.current = triggerRun;
616642
const runStateRef = useRef({ running, filesReady, runnableCount });
617643
runStateRef.current = { running, filesReady, runnableCount };
618644

619645
useEffect(() => {
620646
const handler = (e: KeyboardEvent) => {
621647
if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
622648
e.preventDefault();
623-
const { running: r, filesReady: f, runnableCount: rc } = runStateRef.current;
624-
if (!r && f && rc > 0) {
625-
handleRunAllRef.current();
649+
const { running: r } = runStateRef.current;
650+
if (!r) {
651+
triggerRunRef.current();
626652
}
627653
}
628654
};
@@ -834,32 +860,7 @@ function App() {
834860
<button
835861
className={`run-button${justFinished ? " run-button--success" : ""}`}
836862
disabled={running}
837-
onClick={async () => {
838-
if (!filesReady || samples.length === 0) {
839-
setShowValidation(true);
840-
return;
841-
}
842-
setShowValidation(false);
843-
844-
// Check for existing output files before running
845-
if (runnableCount > 0) {
846-
try {
847-
const conflicts = await invoke<string[]>("check_output_conflicts", { config });
848-
if (conflicts.length > 0) {
849-
const names = conflicts.map((p) => p.split(/[\\/]/).pop()).join(", ");
850-
const ok = window.confirm(
851-
`The following output files already exist and will be overwritten:\n\n${names}\n\nContinue?`
852-
);
853-
if (!ok) return;
854-
}
855-
} catch {
856-
// If check fails, proceed anyway
857-
}
858-
handleRunAll();
859-
} else {
860-
handleRerunAll();
861-
}
862-
}}
863+
onClick={() => triggerRun()}
863864
>
864865
{justFinished ? (
865866
<>

frontend/src/components/BamViewer.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -233,17 +233,18 @@ function BamCell({ value, position, referenceBase, site }: {
233233
const cls = ["bam-cell"];
234234
let text = value || "";
235235

236+
const uc = value?.toUpperCase() ?? "";
236237
if (!value) {
237238
cls.push("bam-cell--empty");
238239
} else if (site) {
239240
cls.push("bam-cell--focus");
240-
if (value === site.altBase) cls.push("bam-cell--focus-alt");
241-
else if (value === site.referenceBase) cls.push("bam-cell--focus-ref");
241+
if (uc === site.altBase.toUpperCase()) cls.push("bam-cell--focus-alt");
242+
else if (uc === site.referenceBase.toUpperCase()) cls.push("bam-cell--focus-ref");
242243
else if (value === "-") cls.push("bam-cell--focus-gap");
243244
else cls.push("bam-cell--focus-other");
244245
} else if (value === "-") {
245246
cls.push("bam-cell--gap");
246-
} else if (value === referenceBase) {
247+
} else if (uc === referenceBase.toUpperCase()) {
247248
cls.push("bam-cell--match");
248249
text = ""; // IGV-style: match = colored bar, no text
249250
} else {
@@ -507,14 +508,15 @@ export default function BamViewer({ bamPath, fastaPath, data, minMapq, minBaseQu
507508
}, [view]);
508509
const maxCoverage = useMemo(() => Math.max(1, ...coverageData), [coverageData]);
509510

510-
// Counts from the returned (truncated) reads — matches what's actually visible
511+
// Use real counts from ALL reads (backend computes before truncation)
511512
const displayCounts = useMemo(() => {
512513
if (!view) return { mnv: 0, partial: 0, reference: 0, other: 0 };
513-
const c = { mnv: 0, partial: 0, reference: 0, other: 0 };
514-
for (const read of view.reads) {
515-
if (read.support in c) c[read.support as keyof typeof c]++;
516-
}
517-
return c;
514+
return {
515+
mnv: view.counts.mnv,
516+
partial: view.counts.partial,
517+
reference: view.counts.reference,
518+
other: view.counts.other,
519+
};
518520
}, [view]);
519521

520522
// Filtered reads by support type

src-tauri/src/commands.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ pub fn ensure_fasta_index(fasta_path: String) -> Result<String, String> {
198198
return Ok(fai_path);
199199
}
200200

201+
// Cannot index gzipped FASTA — byte offsets would be meaningless
202+
if fasta_path.ends_with(".gz") || fasta_path.ends_with(".bgz") {
203+
return Err("Cannot create .fai index for gzipped FASTA. Please decompress first.".to_string());
204+
}
205+
201206
let bytes = std::fs::read(&fasta_path)
202207
.map_err(|e| format!("Cannot read FASTA: {}", e))?;
203208

0 commit comments

Comments
 (0)