conproxy 0.2.1

Cache proxy server for heterogeneous RAG/vector search backends with cascade, scope, and MCP
Documentation
/// Escape HTML special characters.
pub fn html_escape(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for ch in s.chars() {
        match ch {
            '&' => out.push_str("&"),
            '<' => out.push_str("&lt;"),
            '>' => out.push_str("&gt;"),
            '"' => out.push_str("&quot;"),
            '\'' => out.push_str("&#39;"),
            _ => out.push(ch),
        }
    }
    out
}

/// Shared dark-theme CSS for all test reports.
pub fn report_css() -> &'static str {
    r#"  :root { --bg: #1a1a2e; --card: #161b22; --accent: #0f3460; --text: #e0e0e0;
           --link: #58a6ff; --border: #30363d; --green: #3fb950; --red: #f85149;
           --yellow: #d29922; --surface: #161b22; }
  * { box-sizing: border-box; margin: 0; padding: 0; }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
         background: var(--bg); color: var(--text); padding: 2rem; line-height: 1.5; }
  h1 { font-size: 1.5rem; margin-bottom: .25rem; }
  h2 { font-size: 1.15rem; margin: 1.5rem 0 .8rem; color: var(--link); }
  .timestamp { color: #888; margin-bottom: 1.5rem; font-size: .9rem; }
  table { width: 100%; border-collapse: collapse; margin-bottom: 1.2rem; }
  th, td { padding: .55rem .75rem; text-align: left; border: 1px solid var(--border); font-size: .85rem; }
  th { background: var(--surface); font-weight: 600; color: #8b949e; }
  tr:hover td { background: #1c2333; }
  p { margin-bottom: .75rem; }
  .pass { color: var(--green); font-weight: 600; }
  .fail { color: var(--red); font-weight: 600; }
  .warn { color: var(--yellow); font-weight: 600; }
  .stat { background: var(--card); border: 1px solid var(--border); border-radius: 8px;
          padding: 1rem; text-align: center; }
  .stat .value { font-size: 1.5rem; font-weight: bold; }
  .stat .label { font-size: .75rem; color: #8b949e; margin-top: .25rem; }
  .grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 1.25rem; }
  .card { background: var(--card); border: 1px solid var(--border); border-radius: 8px; padding: 1.25rem; }
  .card h2 { font-size: 1.1rem; margin-bottom: .75rem; text-transform: capitalize;
             border-bottom: 1px solid var(--border); padding-bottom: .5rem; }
  .card ul { list-style: none; }
  .card li { padding: .25rem 0; }
  .card a { color: var(--link); text-decoration: none; }
  .card a:hover { text-decoration: underline; }
  .badge { display: inline-block; font-size: .7rem; padding: .1rem .4rem; border-radius: 4px;
           margin-left: .4rem; vertical-align: middle; }
  .html { background: #1f6feb33; color: #58a6ff; }
  .json { background: #23863633; color: #3fb950; }
  .txt  { background: #d2992233; color: #d29922; }
  details { margin: .4rem 0; }
  summary { cursor: pointer; font-size: .85rem; color: #8b949e; }
  summary:hover { color: var(--text); }
  pre { background: var(--surface); border: 1px solid var(--border); border-radius: 4px;
        padding: .6rem .8rem; font-size: .8rem; overflow-x: auto; white-space: pre-wrap; }
  footer { margin-top: 2rem; color: #555; font-size: .8rem; text-align: center; }"#
}

/// Generate a full `<head>` block with inline CSS.
pub fn html_head(title: &str) -> String {
    format!(
        r#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{}</title>
<style>
{}
</style>
</head>
<body>
"#,
        html_escape(title),
        report_css()
    )
}

/// Generate an HTML footer.
pub fn html_footer() -> String {
    "<footer>Generated by conproxy test suite</footer>\n</body>\n</html>\n".to_string()
}

/// Simple ISO-style timestamp without external dep.
pub fn timestamp_now() -> String {
    let d = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default();
    format!("{}s-since-epoch", d.as_secs())
}

/// Generate an inline SVG sparkline chart from (x, y) data points.
///
/// Shared by e2e_proxy and e2e_eval reports for resource profiling sections.
pub fn render_sparkline_svg(
    points: &[(f64, f64)],
    label: &str,
    color: &str,
    w: u32,
    h: u32,
) -> String {
    if points.is_empty() {
        return String::new();
    }

    let x_min = points.iter().map(|p| p.0).fold(f64::INFINITY, f64::min);
    let x_max = points.iter().map(|p| p.0).fold(f64::NEG_INFINITY, f64::max);
    let y_min = points.iter().map(|p| p.1).fold(f64::INFINITY, f64::min);
    let y_max = points.iter().map(|p| p.1).fold(f64::NEG_INFINITY, f64::max);

    let x_range = if (x_max - x_min).abs() < f64::EPSILON {
        1.0
    } else {
        x_max - x_min
    };
    let y_range = if (y_max - y_min).abs() < f64::EPSILON {
        1.0
    } else {
        y_max - y_min
    };

    let margin = 4.0;
    let plot_w = w as f64 - 2.0 * margin;
    let plot_h = h as f64 - 2.0 * margin - 14.0;

    let mut path = String::new();
    for (i, (x, y)) in points.iter().enumerate() {
        let px = margin + (x - x_min) / x_range * plot_w;
        let py = margin + plot_h - (y - y_min) / y_range * plot_h;
        if i == 0 {
            path.push_str(&format!("M{px:.1},{py:.1}"));
        } else {
            path.push_str(&format!(" L{px:.1},{py:.1}"));
        }
    }

    format!(
        r##"<svg width="{w}" height="{h}" viewBox="0 0 {w} {h}" xmlns="http://www.w3.org/2000/svg">
  <path d="{path}" fill="none" stroke="{color}" stroke-width="1.5" stroke-linejoin="round"/>
  <text x="{}" y="{}" font-size="10" fill="#8b949e" font-family="monospace">{}</text>
</svg>"##,
        margin,
        h as f64 - 2.0,
        html_escape(label),
    )
}