Skip to content

1.0.1: correctness fixes, genotyping views, and a documentation site - #3

Merged
Paururo merged 46 commits into
mainfrom
1.0.1
Jul 25, 2026
Merged

1.0.1: correctness fixes, genotyping views, and a documentation site#3
Paururo merged 46 commits into
mainfrom
1.0.1

Conversation

@Paururo

@Paururo Paururo commented Jul 25, 2026

Copy link
Copy Markdown
Member

Release branch for 1.0.1: bug fixes across the CLI and the desktop app, new
genotyping visualisations, and a documentation site.

Correctness

Fixes found by auditing the codebase, each with a regression test where the
behaviour was testable:

  • classify skipped genomes with no markers. They vanished from the summary
    entirely instead of being reported as unclassified.
  • classify scanned only the forward strand, so a marker on a
    reverse-oriented contig was missed. It now scans both strands and counts each
    marker once per genome.
  • --input-files could not be used on its own and rejected more than one
    file, despite being documented as accepting several.
  • Multi-contig assemblies were unsupported via --input-list.
  • Sample positions in the GUI were off by one base.
  • Paired-end grouping merged same-named FASTQs from different folders into a
    single chimeric sample.
  • Masked FASTA was written for records not in reference coordinates, which
    silently produced wrong output.
  • Excel summaries disagreed with their TSV, and a failed save left a corrupt
    file behind.
  • Partial outputs were left behind on error, and one path deleted the wrong
    file.
  • SSRF filter bypasses: IPv4-mapped IPv6 addresses were not blocked, and
    download_file was open to DNS rebinding. It now resolves through a
    validating resolver.

cargo test --workspace: 74 passing.

Desktop app

  • Resistance results are now described by drug and WHO grade rather than by a
    raw marker path.
  • New views: resistance matrix, lineage composition, and a depth vs allele
    fraction scatter for quality control.
  • Mixed infections are detected from fixed markers on incompatible lineage
    branches
    , which is the signal that survives the --min-alt-percent filter.
  • cargo tauri dev starts again. The config declared a devUrl with no
    command to serve it, so the documented development command hung forever.

Documentation

A full MkDocs Material site at
https://pathogenomics-lab.github.io/pathotypr/, with 22 pages covering every
command, the input formats, the algorithms, benchmarks, an FAQ and a citation
page.

Every page was audited against the code; 68 factual errors were corrected,
including flags that do not exist, an arithmetic example that did not add up,
and the drug-resistance panel being credited to the 2021 WHO catalogue when it
is built from the 2023 one.

Size

The site's landing page carried 4.2 MB of gzipped artwork, and frontend/ (the
directory Tauri embeds in every installer) held four copies of the same diagram.

before after
landing page images 4.19 MB 657 KB
frontend/ 9.82 MB 1.27 MB

After merging

edit_uri in mkdocs.yml already points at main, so the documentation's edit
buttons start resolving once this lands. The docs workflow still triggers on
pushes to 1.0.1; that entry can be dropped when the branch is deleted.

Paururo added 30 commits July 19, 2026 15:34
Update the application version to 1.0.1 across the workspace crates
(pathotypr-core, pathotypr-gui), the Tauri config, Cargo.lock and
CITATION.cff. Zenodo marker/model data assets remain at v1.0.0 as they
are versioned independently of the application.
analyze_genome loaded each --input-list sample with get_ref, which errors
on any FASTA containing more than one record. Ordinary multi-contig draft
assemblies (the norm for bacterial WGS) therefore aborted the entire
classify run with a misleading 'Reference FASTA ... contains multiple
records' message, even though the same files succeed via --input and
--input-files.

Read every record with get_genomes_from_fasta and run find_markers on
each contig, aggregating matches under the sample name. get_ref stays
reserved for the single-record reference genome.
detect_paired_end_files stored a single Option per read direction, so
multiple R1 (or R2) files sharing a base name (chunked/lane-split Illumina
output such as _R1_001, _R1_002) silently overwrote each other and roughly
half the reads were dropped, skewing depth-based marker and resistance
calls.

Accumulate every R1 and R2 path per base name and concatenate them for the
sample. Also replace the ineffective file_stem collision fallback (which
usually equalled the derived name) with insert_unique_sample, so an
unrelated single-end file can no longer merge its reads into an existing
sample's counts.
The match and SplitFastq subcommands passed -k straight to needletail
without calling validate_kmer_size (unlike classify/train/predict). k=0
underflows in needletail's update_position (debug panic; release yields
zero k-mers and a silent 'no match'/'Unclassified' result), and k>32
overflows the u64 2-bit packing so distinct k-mers collide, producing a
wrong best match. Validate k to 1..=31 up front in both subcommands.
- excel: derive the .xlsx path by stripping the trailing .tsv extension
  instead of str::replace, which mangled any earlier '.tsv' in the path.
- gff: use saturating_sub(1) for the 1-based->0-based start conversion so a
  malformed CDS start of 0 no longer underflows (matching markers/masking).
- split_kmer: truncate the logged marker header by characters, not raw
  byte index 80, which panicked when byte 80 fell inside a UTF-8 char.
- train: flush the buffered writer before reporting success so a failed
  final write surfaces instead of silently corrupting the saved model.
- predict: skip sequences shorter than k (not just empty ones), which
  otherwise received a bogus high-confidence lineage call.
run_classify and run_split_fastq derived the per-marker output prefix from
the marker file's basename only, so two marker files with the same basename
(selected from different folders) produced the same prefix and the second
run silently overwrote the first's output while still reporting success.
Include the marker index in the prefix to guarantee uniqueness.
is_disallowed_ipv6 did not normalize IPv4-mapped addresses, so an internal
IPv4 target could be reached via the ::ffff:a.b.c.d form (e.g.
::ffff:169.254.169.254 for cloud metadata, or ::ffff:127.0.0.1), bypassing
the IPv4 rules on dual-stack hosts. Fold mapped (and deprecated
IPv4-compatible) addresses back through is_disallowed_ipv4; ::1 and :: are
unaffected as to_ipv4_mapped returns None for them.
escapeHtml relied on textContent/innerHTML, which escapes & < > but not
quotes, so data-derived values interpolated into quoted HTML attributes
(e.g. option value="...", title="...", data-filter-id="...") could break
out of the attribute and inject markup/handlers into the Tauri webview.
Escape " and ' as well so the shared helper is safe in attribute contexts.
…ogress and tracks

- forms/loadDemoData: guard writes to the optional match-coarse-top-n /
  match-coarse-stride controls, which are absent from the DOM and threw a
  TypeError that aborted the whole one-click demo/onboarding loader.
- forms/detectInputListHasHeader: treat a second column that looks like a
  path (separator or file extension) as data, so a real first sample is no
  longer misread as a header and dropped from the classify track context.
- progress: stop the progress-complete listener from force-marking every
  step 'completed'; the backend emits it on error paths too, which made a
  failed run show all steps green. stopProgress finalizes by real outcome.
- visualization/match track: map overview-strip clicks through the full
  domain instead of the zoom window, so clicking the overview navigates
  correctly once zoomed in.
- visualization: test gene start/end finiteness on the raw value; Number(null)
  is 0, which read missing gene coordinates as position 0 and hid the
  inferred-gene fallback when a sample has no GFF.
…resolver

validate_download_url resolved the host and checked its IPs, but reqwest
re-resolved the host when connecting, so an attacker controlling the URL and
a low-TTL DNS record could return a public IP at validation time and a
private/loopback IP at connect time (TOCTOU), reaching internal services.

Install a custom reqwest DNS resolver (SsrfGuardResolver) that rejects any
resolution containing a local/reserved address. reqwest uses it for the
actual connection and for every redirect, so validation and connection share
the same resolution. Public addresses are returned unchanged, preserving CDN
failover.
Set up a Material for MkDocs site that renders the existing guides as a
navigable, searchable documentation site with light and dark themes.

- mkdocs.yml: Material theme, deep-purple palette with light/dark toggle,
  tabbed navigation, search and pymdownx extensions.
- docs/index.md: landing page (overview, five commands, highlights,
  quick start, citation).
- docs/installation.md: desktop, Bioconda and from-source instructions
  plus the MTBC marker/model downloads.
- docs/benchmarks.md: convert the <picture> blocks to Material light/dark
  images and vendor the figures under docs/assets so the site is
  self-contained.
- .github/workflows/docs.yml: build (strict) and deploy to GitHub Pages.
- Remove docs/README.md (superseded by index.md to avoid a URL clash).
The home hero showed the light scheme SVG in both themes. Add the dark
variant and switch between them with Material's #only-light/#only-dark,
matching the benchmark figures.
Rewrite the five command pages (train, predict, classify, split-fastq,
match) to a consistent template: synopsis, an options table verified
against the clap definitions, tabbed examples, and an exact output-columns
reference. Improve the input-format, marker-format and GUI guides.

Add three new pages and wire them into the navigation: a Getting started
tutorial (MTBC), an Output files reference, and an FAQ/troubleshooting page.

Fixes several inaccuracies found while cross-checking against the source:
-i/--input vs --input-list contig handling, GFF-derived vs marker-derived
detail columns, the split-fastq k-mer range, the match containment wording,
and the top-level-only -V/--version flag.
Bring the seven algorithm pages plus the section index to the same Material
standard as the command guides: a bold one-line summary, sentence-case
headings, admonitions for the key rationale/caveats, a language on every
code block, and a 'See also' section cross-linking the relevant commands.

Correct several inaccuracies (mostly pre-existing) found by cross-checking
against the source:
- predict: confidence/margin divide by the model's tree count, not a fixed
  100; sequences shorter than k (not only empty ones) are skipped.
- match: the --early-stop-confidence, --early-stop-min-kmers and
  --strict-percentages options are defined but never read — documented as
  reserved/no-effect in both the command and algorithm pages.
- marker genotyping: the Bloom hash is multiplicative (multiply-mod), not
  multiply-shift.
- assembly classification: the text index is also used for non-ACGT marker
  k-mers (not only k > 32); marker keys use a hand-rolled 2-bit encoder.
Replace the deep-purple palette with a red-and-black scheme drawn from the
logo: a white header in light mode (so the black+red logo reads cleanly) and
a charcoal header in dark mode, with links and accents in the logo's brick
red (#b01000 on light, a brighter #ff5a4d on dark) via a small custom
stylesheet.
Grouping accumulated R1/R2 paths by filename only, so two distinct isolates
whose files share a name (e.g. /runA/S1_R1.fastq.gz and /runB/S1_R1.fastq.gz)
were merged into one sample. Their k-mer counts were summed into a single
marker vector, producing a chimeric lineage/resistance call while the second
isolate vanished from the run. The GUI made this easy to hit because the
dropzone accumulates selections across folders.

Key the grouping by directory as well as base name, so only same-folder
chunks merge. Also sort the groups before assigning names: collision
suffixes were handed out in HashMap iteration order, so identical inputs
could relabel samples between runs. Warn when a name has to be
disambiguated instead of renaming silently.

Add the module's first unit tests, covering chunk accumulation, the
cross-directory case, unmatched-file isolation and naming determinism.
classify generated marker k-mers from the reference forward strand and
scanned query genomes forward only (bit_kmers with canonical=false, and a
plain substring lookup for the text index). Contig orientation in a draft
assembly is arbitrary, so every marker sitting on a reverse-oriented contig
was silently missed, under-calling lineages and resistance mutations. The
FASTQ path already indexes all four orientations, so the two workflows
disagreed on the same sample.

Scan the reverse complement of each contig as well, and deduplicate matches
by marker k-mer across contigs and strands: previously find_markers
deduplicated per contig only, so a marker present on several contigs
inflated the lineage tally.

Both genome entry points now share one helper, and two tests cover the
reverse-oriented contig and the repeated-marker cases.
…tput and flanks

- A genome with no marker hits emits a detail row whose lineage column is
  empty; the summary builder returned before registering the genome, so it
  vanished from *_summary.tsv. The summary silently had fewer rows than
  there were input genomes. Register the genome first — it now appears as
  Unclassified.
- Masked FASTA files were named from the input file stem alone, so two
  samples laid out as <sample>/assembly.fasta overwrote each other's output.
  Disambiguate colliding names, and register each path before writing so a
  partial file is cleaned up on failure or cancellation (the previous code
  returned via ? without cleanup and registered only after success).
- Reject --min-flank-bases values that leave no room for the allele inside
  the k-mer, and fail when no usable marker k-mer could be built at all.
  Both previously produced an empty marker index, reporting every genome as
  Unclassified with no error.
… file

- The three cleanup routines derived the .xlsx path with
  Path::with_extension while the Excel writers derive it by stripping a
  trailing .tsv. For any output not ending in .tsv (e.g. -o results.out)
  cleanup deleted 'results.xlsx' — potentially an unrelated user file — and
  left the real partial 'results.out.xlsx' in place. Share the writer's
  derivation instead.
- predict: several failures between creating the output TSV and the final
  flush returned via ? without cleanup, leaving a header-only or truncated
  predictions file that looks like a finished result.
- train: the feature-importance and coordinates writers were dropped
  without an explicit flush, so a failed final write was swallowed and a
  truncated report was announced as written.
…stale file lists

- visualization: the lineage:count cell was split on [\s,;], but ';' is the
  delimiter *inside* a nested lineage path ("L2;L2.2:5") while both backends
  join entries with a space. Nested entries were shredded, dropping the
  leading component and mislabelling the lineage. Split on whitespace only.
- tauri bridge: read_fasta_range passed record_name, but Tauri v2 maps
  camelCase JS keys onto snake_case parameters. The argument never arrived,
  the Option deserialised to None and the wrong FASTA record was read.
- dropzone: setDropzoneFile updated input.value but left a previous
  multi-file selection in input.dataset.files, which the form builders
  prefer — so a freshly picked file was silently ignored in favour of the
  earlier list. Clear it.
- k-mer controls: split-fastq and match shipped a GUI default of 21 while
  the core (and the docs) default to 31, so the same input gave different
  results in the GUI than on the CLI. The match field also allowed up to 51,
  which the core rejects outright. Align both with the core.
…rupt files

The summary .xlsx built its lineage:count cell by iterating a HashMap and
joining with ", ", while the TSV sorts by count then name and joins with a
space. The two files therefore disagreed for the same genome and the Excel
ordering varied between identical runs. Use the TSV's ordering and separator.

Also delete the partially written workbook when Workbook::save fails: every
caller only warns, so a failed save left a corrupt .xlsx next to a run
reported as successful.
parseMutationTrackData added a +1 offset when the position came from the
SNPgenome or k-merPOS column, but classify already writes both as 1-based
(format_marker_match emits genome_position + 1 and variant_start + 1). Every
plotted sample coordinate was therefore one base too high. Drop the extra
offset and the variables that only fed it.
Mask positions come from the marker TSV, which is defined against the
reference, but they were applied to every record of every input FASTA. For a
de-novo assembly the contigs bear no relation to those coordinates, so the
run silently replaced arbitrary positions with N and produced a corrupt
'_masked' file that is typically fed straight into a phylogeny.

Mask only sequences that live in the reference coordinate system — records
whose length matches the reference, which covers consensus genomes and
reference-length alignments. Anything else is skipped with the reason
logged, rather than emitting a misleading file. Tests cover both cases, and
the documentation states the constraint.
…tabs

- Smart drop routing assigned each file to the first dropzone whose
  extension matched, even one the user had already filled, so a second FASTA
  dropped into the Classify panel landed on the reference instead of the
  empty sample input. Prefer still-empty dropzones, fall back to any match,
  and never put a second file into a single-file dropzone.
- The multi-marker-set tab bars were only rebuilt on multi-marker runs, so
  after such a run a later single-marker run still showed the previous run's
  tabs, pointing at stale output sets. Tear them down in the per-run reset.
The sidebar version is replaced at startup by the value the backend
reports, but the literal baked into index.html still read v0.2.0. If that
call ever fails the window would claim a version four releases old.
…ction view

The genotyping output uses one shape for every panel: the marker TSV columns
after REF/ALT are joined into lineage_path. For the WHO resistance catalogue
that path is drug;resistance;marker;grade;gene;mutation, so a resistance run
was being rendered as if those were lineage levels — drug names, WHO grades
and gene symbols all shown as a lineage, with a meaningless major lineage.

Add a dedicated insights module that recognises the resistance shape and
renders three panels beneath the Split FASTQ evidence:

- Drug resistance profile: one card per drug in clinical reading order
  (first line, Group A, companions, injectables), with the verdict taken
  from the strongest WHO grade found — grades 1/2 resistant, 3 uncertain,
  4/5 not associated. Each card expands to gene, change, grade, depth and
  allele fraction.
- Lineage levels: genuine lineage paths are rebuilt into the hierarchy they
  describe (L2 > L2.2 > L2.2.1) instead of a flat list of prefixes.
- Allele fraction distribution: a histogram highlighting the 20-80% band,
  where a mixed infection shows up. Because the core only writes variants at
  or above --min-alt-percent, the panel says so when the data was truncated
  above that band rather than letting an empty middle read as clonal.

Panel detection is majority-based on the WHO grade column, so a genuine
lineage panel is never mistaken for a resistance one.
…neages

Intermediate allele fractions cannot reveal a mixture at the default
settings, because the core only writes variants at or above
--min-alt-percent (95%). The signal that does survive is which lineages the
*fixed* markers belong to: a single clone can only fix markers along one
root-to-tip path, so fixed markers on divergent branches mean more than one
strain.

Resolve the observed lineage paths into their branch tips and count, for
each, the markers no other branch can explain. That separation matters for
sibling branches: markers on the shared ancestor (L2 for L2.1 vs L2.2) are
reported as compatible but never counted as evidence for either, so shared
ancestry cannot manufacture a mixture. A minimum of two branch-specific
markers keeps a single stray marker from calling one.

The panel leads the genotyping insights with the verdict and each branch's
share of the branch-specific evidence.
classify runs the very same marker panels as split-fastq, including the WHO
resistance catalogue, so an assembly genotyped against it was still being
rendered as if drug names and WHO grades were lineage levels. Reuse the
genotyping insight sections there; the allele-fraction panel simply does not
appear because assemblies carry no read depth.

Verified against real output: classify against the WHO catalogue on a sample
carrying rpoB S450L, katG S315T, embB M306I and gyrA D94G now reports
'Resistance markers found for 4 drugs: RIF, INH, EMB, FQ'.
… path

Every KPI flows from one tally of the label column. On a WHO resistance panel
that column holds the whole marker path, so the header announced
'Primary call: FQ;FQ-R;gyrA_D94G;1) Assoc w R;gyrA;D94G' and the panel was
still titled 'Lineage Distribution'.

Re-tally resistance runs per sample by drug, so the cards, the chart and the
single-sample outcome all describe drugs; summarise the sample as
'Resistant: RIF, INH, EMB, FQ'; and retitle the panel 'Resistance profile'.
Assigned counts samples rather than summing the per-drug tally, which would
otherwise report 600% assigned for one sample carrying six drugs' markers.

showToolVisualization replaced the whole activeVisualizations entry, dropping
the stored default title so the panel stayed renamed after switching back to
a lineage run; preserve it.

Also cap the mixed-infection branch list at six, account for the remainder,
and warn when more than three branches appear: that is usually marker noise
rather than that many co-infecting strains.
… runs

A run with many samples was rendered as a list of per-sample cards, which
stops being readable past a handful and silently merged every sample's
mutations into one profile. Add two batch charts, and keep the cards for the
single-sample case where they are the better form.

- Resistance matrix: samples x drugs, drugs in clinical order. Each cell is a
  state, so it uses the fixed status palette; because a status colour must
  never carry meaning alone, every cell also has a glyph, and the chart ships
  a legend and a table view.
- Lineage composition: one stacked bar per sample of branch-specific marker
  share. Colour follows the lineage, not its rank, so a branch keeps its hue
  across every bar; slots go to the branches that lead a sample and the whole
  tail folds into a single Other segment.

Palettes are the validated data-viz defaults, run through the validator
against this app's own surfaces (#ffffff / #192734): categorical slots 1-3
pass in both modes, and the status roles are used as documented with the
glyph + label mitigation. Marks follow the spec: 2px surface gaps rather than
borders, rounded data-ends, hairline chrome, labels only where they fit, and
hit targets well above pinpoint size.

Also stop pooling the mixed-infection verdict across a batch, where every
sample's lineage would have counted as a co-infecting strain.
Paururo added 16 commits July 24, 2026 21:46
One point per called marker: read depth on a log x-axis against alternate
allele fraction. Well-supported clean calls sit top right; points hugging the
left edge rest on very few reads; weight in the shaded 20-80% band means more
than one strain, with the depth behind each point visible — which the
histogram alone cannot show.

Single series, so it takes categorical slot 1 and needs no legend. Colour is
deliberately not mapped to depth or fraction: both are already positional, and
re-encoding them as hue would spend the only free channel on information the
chart already carries. The 20-80% band is drawn as context, not as a series.

Marks follow the spec: 8px dots with a 2px surface ring so overlapping points
stay countable, solid hairline grid and axes, 1-2-5 ticks per decade so a
tight cluster still has references nearby, and a binned table view as the
scatter's tabular twin. Only read-based runs carry depth, so the chart is
absent for assemblies.

Verified against real output: a simulated 70/30 two-strain FASTQ, genotyped
end to end, resolves into the two clusters the mixture predicts.
The earlier fix registered the genome inside update_summary_from_detail_line,
but the caller trimmed the detail line with trim_end() before handing it over.
For a genome with no marker hits — a name followed by empty columns — that
stripped every tab, leaving a bare name the parser rejects as malformed, so
the sample still vanished from *_summary.tsv.

Trim only the line ending. Verified end to end: a FASTA whose record matches
no marker now reports 'Unclassified' in the summary instead of being absent.
Covered by a test on the exact row analyze_genome emits.
…l files

--input-files is documented as one of the three input sources, but -l's
requirement listed only --input as an alternative, so passing --input-files
alone failed with 'the following required arguments were not provided:
--input-list'. Only the GUI, which builds the args struct directly, ever
reached that path.

Both list-valued flags also took a single value per occurrence, so the
documented '--input-files a.fasta b.fasta' was rejected with 'unexpected
argument'. Accept multiple values per occurrence for --input-files and
--gff-files.
An audit of all 22 pages against the source turned up 68 issues; the
substantive ones are corrected here.

- getting-started told readers to fetch H37Rv from NCBI, but the panels are
  built against MTB_ancestor_reference.fasta (the file the app itself
  downloads, defaults.rs). The two share coordinates but not alleles, so the
  tutorial silently built different diagnostic k-mers from the GUI.
- SNPgenome and SNPreference were described as alleles; both are positions.
- match's Best_Match_Reference is the full FASTA header line, not its first
  token, and a run with no shared k-mers still reports the first reference
  with a fraction of 0.0000 rather than emitting a header-only file.
- classify: scanning is now on both strands with per-genome deduplication;
  multi-record FASTA is one genome per record with -i/--input-files but
  aggregated per sample with -l; the summary example used bare lineage names
  where the key is the full ';'-joined path.
- -V/--version is a top-level flag only, not per-subcommand.
- gui: bundles land in the workspace-root target/, Excel export is always on,
  the published installers are unsigned too, and the file map was out of date.
- benchmarks: real MTB models are tens of KB, not 5-50 MB, and the 500-genome
  extrapolations did not match the measured per-genome rates.
- algorithms: lineage counting tallies every ancestor prefix (so the winner is
  normally the shallowest level), and the Excel export has no conditional
  formatting.

The download links still point at the 1.0.0 assets on purpose: 1.0.1 is not
released, so bumping them would break every link.
The last 25 findings from the documentation audit, all small but each one
wrong against the code:

- predict: Header is the record's full FASTA header line, not just an
  identifier; no 'Unknown' label is ever emitted — an unresolved record gets a
  label with near-zero confidence; and the worked example's margin did not
  match its own numbers (0.96 winner - 0.03 runner-up is 0.93, not 0.92).
- train: per-fold accuracies already print at the default verbosity, so -v is
  not needed to see them.
- classify: --min-flank-bases must stay below half of --kmer-size or the run
  errors; --input-files names each genome '[filename] record_id'.
- input formats: the Query FASTA row linked to the Reference FASTA section and
  credited predict with --input-files, which it does not have; a single
  --gff-files entry is applied to every FASTA; only split-fastq warns about a
  short marker row, classify skips it silently.
- marker format / output files: -V/--version is top-level only; masked FASTA
  name collisions get a numeric suffix rather than overwriting.
- faq: no checksums are published for the Zenodo assets.
- getting-started: a Confidence_Margin of 0.40 is 40 points of the whole
  ensemble, not 40% more votes than the runner-up.
- gui: cargo tauri dev has no front-end hot reload (the frontend is static
  files); commands.rs is what emits progress events.
- benchmarks: the speedup column compares predict against train, not the
  reverse.
- algorithms: the text marker index is reached via non-ACGT bases, since
  validate_kmer_size caps k at 31 and makes the k > 32 branch unreachable;
  lineage:count tallies every hierarchical level; Other_Votes is
  label:fraction to two decimals; the model version example was still 0.2.0.
The config declared devUrl http://localhost:1420 while beforeDevCommand was
empty, so nothing ever served that port and cargo tauri dev looped forever
printing 'Waiting for your frontend dev server to start'. The documented
development command could never launch the app.

The frontend is plain static files, so dropping devUrl lets Tauri serve
frontendDist directly in development, which is what the docs already
describe.
The admonition sat between the intro and the diagram and ran to nine lines,
pushing the figure down and dominating the landing page. It now follows the
figure and states the same limitation in three lines.
…ates

The landing page shipped 4.2 MB of gzipped artwork. scheme-dark.png was
6563x2500 for a 900 px slot, and both the light and dark variants are fetched
on every visit because #only-light/#only-dark hides one in CSS rather than
skipping the request. Re-encoding the dark diagram as an 1800 px WebP takes it
from 3.28 MB to 163 KB, and rounding the light SVG's path data from six decimal
places to one takes it from 711 KB to 436 KB without touching how it renders:
six decimals on a 1575-unit viewBox is far below one screen pixel.

The logo was a 5658 px wordmark used both for the 65 px header slot and for the
favicon, where a 2.7:1 image is unreadable. It is now 600 px, with the DNA mark
from its centre as a square favicon.

edit_uri was configured but content.action.edit was not, so the theme never
rendered the button and the setting did nothing. Both actions are on now.

Adds a Citation page carrying BibTeX, RIS and APA entries alongside the two
DOIs, since that is what readers of the preprint come looking for, and shows a
last-updated date per page so nobody has to guess how current a page is.

Social cards give shared links a preview instead of a bare URL. Generating them
needs cairo and pango, so they are built only in CI, where the workflow now
installs those libraries.

Total: 4.2 MB down to 657 KB.
The accent was only defined for the light scheme's tabs bar, so it vanished in
dark mode. It also disappeared below Material's tabs breakpoint, where the tabs
bar is hidden and took the rule with it, leaving phones with no accent at all.

Dark mode now uses the brighter red that already carries links there, and below
the breakpoint the header itself draws the rule.
frontend/ is the frontendDist Tauri embeds, so everything in it is paid for by
every installer download. It held four copies of the workflow diagram totalling
9.3 MB: two PNGs the app displays, an SVG only the README linked, and a PDF
nothing referenced at all. The SVG and the dark PNG were byte-for-byte
identical to the copies already in docs/assets.

The two the app uses are now 2400 px WebP, which keeps the max-width:92vw
lightbox sharp while going from 5.66 MB to 0.51 MB. The README points at
docs/assets instead of carrying its own copies, and the SVG and PDF are gone
from the bundle; git history keeps them if the print master is needed.

logo.png was the same 5658 px wordmark as the docs copy, displayed at 300 px at
most, so it drops to 900 px.

frontend/ goes from 9.82 MB to 1.4 MB.
The file carried a note calling itself legacy and kept for reference, but it
was still inside frontendDist, so its 2324 lines were embedded in every
installer. index.html loads exactly two scripts, Chart.js and js/main.js as a
module, and defines no inline handlers, so nothing could reach the globals it
declared. Removing it cannot change runtime behaviour.

The GUI architecture listing in the docs no longer mentions it.
The install instructions already point at the bioconda channel, but nothing in
the badge row showed the package is published there or how widely it is used.
Both badges read live from anaconda.org, so they track new releases on their
own.

Uses shields.io with flat-square to match the badges already in the row, rather
than the anaconda.org native badge, whose style would not fit.
…site

Four things were wrong. The Contributors heading linked to the AMAP repository
rather than this one, and doubled its closing parenthesis so GitHub rendered it
as plain text instead of a link. A bare --- sat directly under the licence link,
which makes it a setext heading, so the licence name rendered as a second H2
under the License section. A stray </div> closed a block that was never opened.
And the drug-resistance panel was credited to the 2021 WHO catalogue in five
places: it is built by convert_who_markers.py from WHO-UCN-TB-2023.6 and
2023.7, and the file published on Zenodo is byte-identical to that output.

Every install link combined /releases/latest/download/ with a filename pinned
to 1.0.0. Those resolve today because 1.0.0 is the latest release, but each one
404s the moment 1.0.1 ships, since the asset carries the new version in its
name. The tables now name the files without linking them, above a single button
to the releases page, which cannot go stale.

The README also never once linked to the documentation site, sending readers to
sixteen raw .md files that render without search or navigation. Those now point
at the published pages, each checked against the live site.

The header image uses logo/pathotypr.svg, a 34 KB vector, rather than the
identical 5658 px PNG beside it.
edit_uri tracked the release branch, so every edit and view-source button would
have 404'd once this branch merged and the branch was deleted. Pointing it at
main makes them correct for the site's published lifetime.

Until this merges the buttons lead to files that do not exist on main yet.
@Paururo
Paururo merged commit b331039 into main Jul 25, 2026
2 checks passed
@Paururo
Paururo deleted the 1.0.1 branch July 25, 2026 12:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant