OXYGENHUB

Coverage / crates/hub/src/web/coverage.rs 83.7%

313 of 374 lines run, 52 of 144 functions · run #507 on main · run missed

1
//! Coverage pages (oxygenhub#113): a repository's coverage, its trend,
2
//! every file, and each file's lines marked run or missed; and a badge.
3
4
use super::{filters, lookup, Error, Page, RepoShell, Shell};
5
use crate::auth::Viewer;
6
use crate::coverage::{self, FileRow, Report};
7
use crate::repos::view::{self, FileBody, RefKind, RefTarget};
8
use crate::Shared;
9
use askama::Template;
10
use axum::extract::{Path, Query, State};
11
use axum::response::{IntoResponse, Response};
12
use serde::Deserialize;
13
use std::collections::BTreeMap;
14
use std::sync::{Arc, Mutex};
15
16
/// Reports the charts and the recent list draw on.
17
const HISTORY: i64 = 200;
18
/// Runs listed under Recent runs.
19
const RECENT: usize = 10;
20
/// Files listed as changed against the run before.
21
const CHANGED: usize = 12;
22
23
#[derive(Deserialize, Default)]
24
pub struct CoverageQuery {
25
    /// A bookmark other than the default.
26
    bookmark: Option<String>,
27
    /// One report, by id.
28
    report: Option<i64>,
29
    /// `missed` (default), `percent` or `path`.
30
    sort: Option<String>,
31
}
32
33
/// A run in the Recent runs list.
34
pub struct RecentRow {
35
    pub report: Report,
36
    /// Against the report before it, in percentage points.
37
    pub delta: Option<f64>,
38
}
39
40
/// A file whose coverage moved against the run before.
41
pub struct Moved {
42
    pub path: String,
43
    pub before: Option<f64>,
44
    pub after: Option<f64>,
45
    pub delta: f64,
46
}
47
48
/// One band of the file distribution.
49
pub struct Band {
50
    pub label: &'static str,
51
    pub class: &'static str,
52
    pub files: usize,
53
    /// Of the widest band, for the bar's width.
54
    pub width: f64,
55
}
56
57
#[derive(Template)]
58
#[template(path = "coverage.html")]
59
pub struct CoveragePage {
60
    shell: Shell,
61
    repo: RepoShell,
62
    bookmark: String,
63
    bookmarks: Vec<String>,
64
    report: Option<Report>,
65
    /// The report before this one on the bookmark.
66
    previous: Option<Report>,
67
    delta: Option<f64>,
68
    files: Vec<FileRow>,
69
    sort: String,
70
    coverage_chart: String,
71
    lines_chart: String,
72
    runs: usize,
73
    first_ms: Option<i64>,
74
    recent: Vec<RecentRow>,
75
    moved: Vec<Moved>,
76
    /// Files that are new against the run before.
77
    added: usize,
78
    bands: Vec<Band>,
79
}
80
81
impl CoveragePage {
8211×
    pub fn pct(&self, p: Option<f64>) -> String {
8311×
        p.map(|p| format!("{p:.1}%")).unwrap_or_else(|| "–".into())
8411×
    }
85
860×
    pub fn pct_of(&self, p: &Option<f64>) -> String {
870×
        self.pct(*p)
880×
    }
89
900×
    pub fn signed(&self, d: &f64) -> String {
910×
        if d.abs() < 0.05 {
920×
            "±0.0".to_string()
93
        } else {
940×
            format!("{d:+.1}")
95
        }
960×
    }
97
980×
    pub fn delta_class(&self, d: &f64) -> &'static str {
990×
        if *d >= 0.05 {
1000×
            "up"
1010×
        } else if *d <= -0.05 {
1020×
            "down"
103
        } else {
1040×
            "same"
105
        }
1060×
    }
107
1086×
    pub fn class_of(&self, p: Option<f64>) -> &'static str {
1096×
        level(p)
1106×
    }
111
1124×
    pub fn hits(&self, r: &Report) -> String {
1134×
        r.hits_per_line().map(|h| if h >= 100.0 { compact(h) } else { format!("{h:.1}") }).unwrap_or_else(|| "–".into())
1144×
    }
115
1162×
    pub fn lines_delta(&self) -> Option<(i64, i64)> {
1172×
        let (r, p) = (self.report.as_ref()?, self.previous.as_ref()?);
1180×
        Some((r.lines_found - p.lines_found, r.lines_hit - p.lines_hit))
1192×
    }
120
}
121
122
/// A count, short: 9.4k, 147.7k, 2.3M.
12325×
pub(super) fn compact(v: f64) -> String {
12425×
    match v {
12525×
        v if v >= 1e9 => format!("{:.1}G", v / 1e9),
12625×
        v if v >= 1e6 => format!("{:.1}M", v / 1e6),
12725×
        v if v >= 1e3 => format!("{:.1}k", v / 1e3),
12825×
        v => format!("{v:.0}"),
129
    }
13025×
}
131
132
/// A colour class for a percentage.
1338×
pub fn level(p: Option<f64>) -> &'static str {
1345×
    match p {
1358×
        Some(p) if p >= 90.0 => "cov-good",
1368×
        Some(p) if p >= 75.0 => "cov-fair",
1375×
        Some(p) if p >= 50.0 => "cov-low",
1380×
        Some(_) => "cov-poor",
1390×
        None => "cov-none",
140
    }
1418×
}
142
143
/// A line of values across runs, for [`chart`].
144
pub(super) struct Series<'a> {
145
    pub values: Vec<f64>,
146
    pub class: &'a str,
147
    /// Shade down to these values instead of to the axis: a band.
148
    pub floor: Option<Vec<f64>>,
149
}
150
151
/// A run on a chart: its number, where a click goes, what a hover says.
152
pub(super) struct Point {
153
    pub run: i64,
154
    pub href: String,
155
    pub tip: String,
156
}
157
158
/// An SVG chart of one or more series over runs, oldest on the left, with
159
/// a light grid and labels, and a column per run that names it on hover
160
/// and links to its report. `fmt` labels the y axis.
16111×
pub(super) fn chart(label: &str, runs: &[Point], series: &[Series], lo: f64, hi: f64, fmt: &dyn Fn(f64) -> String) -> String {
16211×
    let (w, h) = (800.0, 180.0);
16311×
    let (left, right, top, bottom) = (52.0, 10.0, 12.0, 24.0);
16411×
    let (pw, ph) = (w - left - right, h - top - bottom);
16511×
    let n = runs.len();
16611×
    if n == 0 {
1670×
        return String::new();
16811×
    }
169100×
    let x = |i: usize| if n == 1 { left + pw / 2.0 } else { left + pw * i as f64 / (n - 1) as f64 };
17011×
    let span = if hi > lo { hi - lo } else { 1.0 };
171171×
    let y = |v: f64| top + ph * (hi - v) / span;
17211×
    let mut svg = format!(r#"<svg class="cov-chart" viewBox="0 0 {w} {h}" role="img" aria-label="{label}">"#);
17355×
    for i in 0..=4 {
17455×
        let v = lo + span * i as f64 / 4.0;
17555×
        svg.push_str(&format!(
17655×
            r#"<line class="grid" x1="{left}" x2="{x2}" y1="{yy:.1}" y2="{yy:.1}"/><text class="axis" x="{tx}" y="{ty:.1}" text-anchor="end">{t}</text>"#,
17755×
            x2 = w - right,
17855×
            yy = y(v),
17955×
            tx = left - 6.0,
18055×
            ty = y(v) + 3.5,
18155×
            t = fmt(v),
18255×
        ));
18355×
    }
18416×
    for s in series {
18536×
        let pts: Vec<String> = s.values.iter().enumerate().map(|(i, v)| format!("{:.1},{:.1}", x(i), y(*v))).collect();
18616×
        if n > 1 {
18710×
            let area = match &s.floor {
1883×
                Some(floor) => {
1899×
                    let back: Vec<String> = floor.iter().enumerate().rev().map(|(i, v)| format!("{:.1},{:.1}", x(i), y(*v))).collect();
1903×
                    format!("M{} L{} Z", pts.join(" L"), back.join(" L"))
191
                }
1927×
                None => format!("M{:.1},{:.1} L{} L{:.1},{:.1} Z", x(0), top + ph, pts.join(" L"), x(n - 1), top + ph),
193
            };
19410×
            svg.push_str(&format!(
19510×
                r#"<path class="area {c}" d="{area}"/><polyline class="line {c}" points="{line}"/>"#,
19610×
                c = s.class,
19710×
                line = pts.join(" ")
19810×
            ));
1996×
        }
20016×
        if let Some(last) = s.values.last() {
20116×
            svg.push_str(&format!(r#"<circle class="dot {}" cx="{:.1}" cy="{:.1}" r="4"/>"#, s.class, x(n - 1), y(*last)));
20216×
        }
203
    }
204
    // A column per run: its tip on hover, its report on a click.
20511×
    let col = if n == 1 { pw } else { pw / (n - 1) as f64 };
20625×
    for (i, r) in runs.iter().enumerate() {
20725×
        svg.push_str(&format!(
20825×
            r#"<a href="{href}"><rect class="hit" x="{rx:.1}" y="{top}" width="{col:.1}" height="{ph}"><title>{t}</title></rect></a>"#,
20925×
            href = r.href,
21025×
            rx = x(i) - col / 2.0,
21125×
            t = r.tip,
21225×
        ));
21325×
    }
21411×
    if n > 1 {
2157×
        svg.push_str(&format!(
2167×
            r#"<text class="axis" x="{left}" y="{yb}">run #{first}</text><text class="axis" x="{xr}" y="{yb}" text-anchor="end">run #{last}</text>"#,
2177×
            yb = h - 6.0,
2187×
            xr = w - right,
2197×
            first = runs[0].run,
2207×
            last = runs[n - 1].run,
2217×
        ));
2227×
    }
22311×
    svg.push_str("</svg>");
22411×
    svg
22511×
}
226
227
/// `GET /{owner}/{name}/coverage`.
2282×
pub async fn overview(
2292×
    State(app): State<Shared>,
2302×
    viewer: Viewer,
2312×
    Path((owner, name)): Path<(String, String)>,
2322×
    Query(q): Query<CoverageQuery>,
2332×
) -> Result<Page<CoveragePage>, Error> {
2342×
    let shell = Shell::new(&app, &format!("/{owner}/{name}/coverage"), &viewer);
2352×
    let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
2362×
    let e = |e| Error::from_db(shell.clone(), e);
2372×
    let picked = match q.report {
2380×
        Some(id) => Some(coverage::report(&app.db, row.id, id).await.map_err(e)?.ok_or_else(|| Error::not_found(shell.clone()))?),
2392×
        None => None,
240
    };
2412×
    let bookmark = picked.as_ref().map(|r| r.bookmark.clone()).or(q.bookmark.clone()).unwrap_or_else(|| row.default_bookmark.clone());
2422×
    let bookmarks = coverage::bookmarks(&app.db, row.id).await.map_err(e)?;
2432×
    let history = coverage::on_bookmark(&app.db, row.id, &bookmark, HISTORY).await.map_err(e)?;
2442×
    let report = picked.or_else(|| history.first().cloned());
245
    // The report before the one shown, on the same bookmark; for a
246
    // bookmark's first report, the default bookmark's latest before it.
2472×
    let mut previous = report
2482×
        .as_ref()
2492×
        .and_then(|r| history.iter().find(|h| h.created_ms < r.created_ms || (h.created_ms == r.created_ms && h.id < r.id)).cloned());
2502×
    if let (None, Some(r)) = (&previous, &report) {
2512×
        if r.bookmark != row.default_bookmark {
2520×
            previous = coverage::baseline(&app.db, r, &row.default_bookmark).await.map_err(e)?;
2532×
        }
2540×
    }
2552×
    let delta = report.as_ref().zip(previous.as_ref()).and_then(|(r, p)| Some(r.percent()? - p.percent()?));
2562×
    let mut files = match &report {
2572×
        Some(r) => coverage::files(&app.db, r.id).await.map_err(e)?,
2580×
        None => vec![],
259
    };
260
    // Files that moved against the run before, the largest moves first.
2612×
    let (mut moved, mut added) = (Vec::new(), 0);
2622×
    if let Some(p) = &previous {
2630×
        let before: std::collections::HashMap<String, Option<f64>> =
2640×
            coverage::files(&app.db, p.id).await.map_err(e)?.into_iter().map(|f| (f.path.clone(), f.percent())).collect();
2650×
        for f in &files {
2660×
            match before.get(&f.path) {
2670×
                None => added += 1,
2680×
                Some(b) => {
2690×
                    let d = f.percent().unwrap_or(0.0) - b.unwrap_or(0.0);
2700×
                    if d.abs() >= 0.05 {
2710×
                        moved.push(Moved { path: f.path.clone(), before: *b, after: f.percent(), delta: d });
2720×
                    }
273
                }
274
            }
275
        }
2760×
        moved.sort_by(|a, b| b.delta.abs().total_cmp(&a.delta.abs()).then(a.path.cmp(&b.path)));
2770×
        moved.truncate(CHANGED);
2782×
    }
279
    // How the files spread over coverage bands.
2802×
    let bounds: [(&str, &str, f64, f64); 5] = [
2812×
        ("100%", "cov-good", 100.0, 100.1),
2822×
        ("90 – 99%", "cov-good", 90.0, 100.0),
2832×
        ("75 – 89%", "cov-fair", 75.0, 90.0),
2842×
        ("50 – 74%", "cov-low", 50.0, 75.0),
2852×
        ("under 50%", "cov-poor", -1.0, 50.0),
2862×
    ];
2872×
    let counts: Vec<usize> =
28810×
        bounds.iter().map(|(_, _, lo, hi)| files.iter().filter(|f| f.percent().is_some_and(|p| p >= *lo && p < *hi)).count()).collect();
2892×
    let widest = counts.iter().copied().max().unwrap_or(0).max(1) as f64;
2902×
    let bands = bounds
2912×
        .iter()
2922×
        .zip(&counts)
29310×
        .map(|((label, class, _, _), n)| Band { label, class, files: *n, width: *n as f64 * 100.0 / widest })
2942×
        .collect();
2952×
    let sort = match q.sort.as_deref() {
2960×
        Some("percent") => "percent",
2970×
        Some("path") => "path",
2982×
        _ => "missed",
299
    };
3002×
    match sort {
3012×
        "percent" => files.sort_by(|a, b| a.percent().unwrap_or(101.0).total_cmp(&b.percent().unwrap_or(101.0)).then(a.path.cmp(&b.path))),
3022×
        "path" => {}
3032×
        _ => files.sort_by(|a, b| b.missed().cmp(&a.missed()).then(a.path.cmp(&b.path))),
304
    }
3052×
    let repo = RepoShell::new(&app, &row, "coverage").with_role(role);
306
    // Charts: oldest on the left.
3072×
    let oldest_first: Vec<&Report> = history.iter().rev().filter(|r| r.lines_found > 0).collect();
3082×
    let pcts: Vec<f64> = oldest_first.iter().map(|r| r.percent().unwrap_or(0.0)).collect();
3092×
    let (lo, hi) = (
3102×
        (pcts.iter().copied().fold(100.0, f64::min) - 2.0).floor().max(0.0),
3112×
        (pcts.iter().copied().fold(0.0, f64::max) + 2.0).ceil().min(100.0),
3122×
    );
3132×
    let tip = |r: &Report| {
3142×
        format!(
315
            "{} · {} · {} of {} lines",
3162×
            r.source(),
3172×
            r.percent().map(|p| format!("{p:.1}%")).unwrap_or_default(),
318
            r.lines_hit,
319
            r.lines_found
320
        )
3212×
        .replacen("run #", "Run #", 1)
3222×
    };
3232×
    let points: Vec<Point> = oldest_first
3242×
        .iter()
3252×
        .map(|r| Point { run: r.run_number, href: format!("{}/coverage?report={}", repo.url, r.id), tip: tip(r) })
3262×
        .collect();
3272×
    let coverage_chart = chart(
3282×
        &format!("Coverage of {bookmark} over the last {} runs", oldest_first.len()),
3292×
        &points,
3302×
        &[Series { values: pcts, class: "cov", floor: None }],
3312×
        lo,
3322×
        hi,
33310×
        &|v| format!("{v:.0}%"),
334
    );
3352×
    let top = oldest_first.iter().map(|r| r.lines_found).max().unwrap_or(0) as f64;
3362×
    let covered: Vec<f64> = oldest_first.iter().map(|r| r.lines_hit as f64).collect();
3372×
    let lines_chart = chart(
3382×
        &format!("Relevant and covered lines of {bookmark}"),
3392×
        &points,
340
        &[
3412×
            Series { values: covered.clone(), class: "covered", floor: None },
3422×
            Series { values: oldest_first.iter().map(|r| r.lines_found as f64).collect(), class: "relevant", floor: Some(covered) },
343
        ],
344
        0.0,
3452×
        (top * 1.08).max(1.0),
3462×
        &compact,
347
    );
3482×
    let recent = history
3492×
        .iter()
3502×
        .enumerate()
3512×
        .take(RECENT)
3522×
        .map(|(i, r)| RecentRow { report: r.clone(), delta: history.get(i + 1).and_then(|p| Some(r.percent()? - p.percent()?)) })
3532×
        .collect();
3542×
    let first_ms = coverage::first_ms(&app.db, row.id).await.map_err(e)?;
3552×
    Ok(Page(CoveragePage {
3562×
        shell,
3572×
        repo,
3582×
        bookmark,
3592×
        bookmarks,
3602×
        report,
3612×
        previous,
3622×
        delta,
3632×
        files,
3642×
        sort: sort.into(),
3652×
        coverage_chart,
3662×
        lines_chart,
3672×
        runs: oldest_first.len(),
3682×
        first_ms,
3692×
        recent,
3702×
        moved,
3712×
        added,
3722×
        bands,
3732×
    }))
3742×
}
375
376
/// One line of a file, as the file view shows it.
377
pub struct CovLine {
378
    pub number: usize,
379
    pub html: String,
380
    /// None: not an instrumented line.
381
    pub hits: Option<u64>,
382
}
383
384
#[derive(Template)]
385
#[template(path = "coverage_file.html")]
386
pub struct CoverageFilePage {
387
    shell: Shell,
388
    repo: RepoShell,
389
    report: Report,
390
    file: FileRow,
391
    lines: Vec<CovLine>,
392
    note: Option<String>,
393
}
394
395
impl CoverageFilePage {
3961×
    pub fn pct(&self, p: Option<f64>) -> String {
3971×
        p.map(|p| format!("{p:.1}%")).unwrap_or_else(|| "–".into())
3981×
    }
399
4001×
    pub fn class_of(&self, p: Option<f64>) -> &'static str {
4011×
        level(p)
4021×
    }
403
}
404
405
/// The line counts of the report last read, by file: the file view reads
406
/// the report's artifact again, so that browsing one report's files
407
/// parses it once.
408
type Parsed = Arc<BTreeMap<String, BTreeMap<u32, u64>>>;
409
static LAST: Mutex<Option<(String, Parsed)>> = Mutex::new(None);
410
4111×
async fn line_counts(app: &Shared, repo: &crate::repos::RepoName, blob: &str) -> Option<Parsed> {
4121×
    if let Some((b, p)) = LAST.lock().ok()?.as_ref() {
4130×
        if b == blob {
4140×
            return Some(p.clone());
4150×
        }
4161×
    }
4171×
    let r = oxy_core::hash::ObjectRef::from_hex(blob)?;
4181×
    let bytes = app.repos.read(repo, move |repo, _| Ok(repo.read_blob(&r)?)).await.ok()?;
4191×
    let parsed = tokio::task::spawn_blocking(move || {
4201×
        let text = String::from_utf8_lossy(&bytes);
4212×
        coverage::parse(&text).ok().map(|(files, _, _)| files.into_iter().map(|f| (f.path, f.lines)).collect::<BTreeMap<_, _>>())
4221×
    })
4231×
    .await
4241×
    .ok()??;
4251×
    let parsed = Arc::new(parsed);
4261×
    if let Ok(mut last) = LAST.lock() {
4271×
        *last = Some((blob.to_string(), parsed.clone()));
4281×
    }
4291×
    Some(parsed)
4301×
}
431
432
/// `GET /{owner}/{name}/coverage/{report}/file/{*path}`.
4332×
pub async fn file(
4342×
    State(app): State<Shared>,
4352×
    viewer: Viewer,
4362×
    Path((owner, name, id, path)): Path<(String, String, i64, String)>,
4372×
) -> Result<Page<CoverageFilePage>, Error> {
4382×
    let shell = Shell::new(&app, &format!("/{owner}/{name}/coverage/{id}/file/{path}"), &viewer);
4392×
    let (repo_name, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
4402×
    let e = |e| Error::from_db(shell.clone(), e);
4412×
    let report = coverage::report(&app.db, row.id, id).await.map_err(e)?.ok_or_else(|| Error::not_found(shell.clone()))?;
4422×
    let file = coverage::files(&app.db, report.id)
4432×
        .await
4442×
        .map_err(e)?
4452×
        .into_iter()
4462×
        .find(|f| f.path == path)
4472×
        .ok_or_else(|| Error::not_found(shell.clone()))?;
4481×
    let counts = line_counts(&app, &repo_name, &report.blob).await.and_then(|p| p.get(&path).cloned()).unwrap_or_default();
4491×
    let heads: Vec<_> = report.head_list().into_iter().filter_map(oxy_core::hash::ChangeHash::from_hex).collect();
4501×
    let limit = app.config.limits.render_limit.0;
4511×
    let p = path.clone();
4521×
    let target = RefTarget { label: report.state.clone(), kind: RefKind::Change, heads };
4531×
    let fv = app
4541×
        .repos
4551×
        .read(&repo_name, move |r, cache| view::file(r, cache, target, &p, limit))
4561×
        .await
4571×
        .map_err(|e| Error::from_repo(shell.clone(), e))?;
4581×
    let (lines, note) = match &fv.body {
4591×
        FileBody::Text(text) => {
4601×
            let html = crate::render::highlight::lines(&path, text);
4611×
            let lines = html
4621×
                .into_iter()
4631×
                .enumerate()
4647×
                .map(|(i, h)| CovLine { number: i + 1, html: h, hits: counts.get(&(i as u32 + 1)).copied() })
4651×
                .collect();
4661×
            (lines, None)
467
        }
4680×
        FileBody::Binary => (vec![], Some("This file is binary.".to_string())),
4690×
        FileBody::TooLarge => (vec![], Some("This file is too large to show.".to_string())),
470
    };
4711×
    let repo = RepoShell::new(&app, &row, "coverage").with_role(role);
4721×
    Ok(Page(CoverageFilePage { shell, repo, report, file, lines, note }))
4732×
}
474
475
/// `GET /{owner}/{name}/coverage/badge.svg`: the default bookmark's latest coverage.
4761×
pub async fn badge(State(app): State<Shared>, viewer: Viewer, Path((owner, name)): Path<(String, String)>) -> Result<Response, Error> {
4771×
    let shell = Shell::new(&app, &format!("/{owner}/{name}/coverage"), &viewer);
4781×
    let (_, row, _) = lookup(&app, &shell, &viewer, &owner, &name).await?;
4791×
    let latest = coverage::on_bookmark(&app.db, row.id, &row.default_bookmark, 1).await.map_err(|e| Error::from_db(shell.clone(), e))?;
4801×
    let pct = latest.first().and_then(|r| r.percent());
4811×
    let text = pct.map(|p| format!("{p:.1}%")).unwrap_or_else(|| "unknown".into());
4821×
    let colour = match level(pct) {
4831×
        "cov-good" => "#2da44e",
4841×
        "cov-fair" => "#97ca00",
4851×
        "cov-low" => "#dfb317",
4860×
        "cov-poor" => "#e05d44",
4870×
        _ => "#6e7781",
488
    };
4891×
    let label = "coverage";
4901×
    let (lw, tw) = (label.len() as u32 * 7 + 10, text.len() as u32 * 7 + 10);
4911×
    let svg = format!(
492
        r##"<svg xmlns="http://www.w3.org/2000/svg" width="{w}" height="20" role="img" aria-label="{label}: {text}"><title>{label}: {text}</title><rect width="{lw}" height="20" rx="3" fill="#555"/><rect x="{lw}" width="{tw}" height="20" rx="3" fill="{colour}"/><rect x="{lw}" width="4" height="20" fill="{colour}"/><g fill="#fff" font-family="Verdana,DejaVu Sans,sans-serif" font-size="11" text-anchor="middle"><text x="{lx}" y="14">{label}</text><text x="{tx}" y="14">{text}</text></g></svg>"##,
4931×
        w = lw + tw,
4941×
        lx = lw / 2,
4951×
        tx = lw + tw / 2,
496
    );
4971×
    Ok((
4981×
        [(axum::http::header::CONTENT_TYPE, "image/svg+xml; charset=utf-8"), (axum::http::header::CACHE_CONTROL, "no-cache, max-age=60")],
4991×
        svg,
5001×
    )
5011×
        .into_response())
5021×
}