lobe-cli 0.1.1

Lobe — local-first HTTP performance profiling CLI for developers. Spins up a capture proxy, records DNS/TCP/TLS/TTFB/download phases per request, and flags anomalies against grounded network baselines.
use lobe_core::engine::history::RegressionSummary;
use lobe_core::models::{ProbeResult, ProbeStatus, TlsStatus};

pub fn print_probe_result(result: &ProbeResult) {
    println!("Run #{}", result.id);
    println!("target: {}", result.target);
    println!("status: {}", format_status(result.status));
    println!(
        "timing: dns={}ms tcp={}ms tls={}ms ttfb={}ms download={}ms total={}ms",
        result.report.dns_ms,
        result.report.tcp_ms,
        result.report.tls_ms,
        result.report.ttfb_ms,
        result.report.download_ms,
        result.report.total_ms
    );

    if let Some(tls) = &result.tls {
        println!(
            "tls: {}{}{}",
            format_tls_status(tls.status),
            tls.version
                .as_deref()
                .map(|value| format!(" version={value}"))
                .unwrap_or_default(),
            tls.cipher_suite
                .as_deref()
                .map(|value| format!(" cipher={value}"))
                .unwrap_or_default()
        );
    }

    if let Some(error_message) = &result.error_message {
        println!("error: {error_message}");
    }
}

pub fn print_history(results: &[ProbeResult]) {
    if results.is_empty() {
        println!("No saved runs found.");
        return;
    }

    for result in results {
        println!(
            "#{} [{}] {} total={}ms ttfb={}ms created_at_ms={}",
            result.id,
            format_status(result.status),
            result.target,
            result.report.total_ms,
            result.report.ttfb_ms,
            result.created_at_ms
        );
    }
}

pub fn print_comparison(current: &ProbeResult, previous: &ProbeResult, summary: &RegressionSummary) {
    println!("target: {}", current.target);
    println!(
        "current: run #{} total={}ms ttfb={}ms",
        current.id, current.report.total_ms, current.report.ttfb_ms
    );
    println!(
        "previous: run #{} total={}ms ttfb={}ms",
        previous.id, previous.report.total_ms, previous.report.ttfb_ms
    );
    println!(
        "delta: {}ms ({:.1}%) threshold={:.1}% status={}",
        summary.delta_ms,
        summary.delta_percent,
        summary.threshold_percent,
        if summary.exceeded_threshold {
            "regression"
        } else {
            "within-threshold"
        }
    );
}

pub fn print_comparison_unavailable(target: &str) {
    println!("Not enough saved runs to compare for target: {target}");
}

pub fn print_zero_baseline_notice(target: &str) {
    println!("Cannot compare target {target} because the previous run had 0ms TTFB.");
}

pub fn print_watch_result(
    result: &ProbeResult,
    comparison: Option<&RegressionSummary>,
    comparison_note: Option<&str>,
) {
    print!(
        "#{} [{}] total={}ms ttfb={}ms download={}ms dns={}ms tcp={}ms tls={}ms",
        result.id,
        format_status(result.status),
        result.report.total_ms,
        result.report.ttfb_ms,
        result.report.download_ms,
        result.report.dns_ms,
        result.report.tcp_ms,
        result.report.tls_ms
    );

    if let Some(tls) = &result.tls {
        print!(" tls_status={}", format_tls_status(tls.status));
    }

    if let Some(summary) = comparison {
        print!(
            " delta_ttfb={}ms ({:.1}%) {}",
            summary.delta_ms,
            summary.delta_percent,
            if summary.exceeded_threshold {
                "REGRESSION"
            } else {
                "ok"
            }
        );
    }

    if let Some(note) = comparison_note {
        print!(" note={note}");
    }

    if let Some(error_message) = &result.error_message {
        print!(" error={error_message}");
    }

    println!();
}

fn format_status(status: ProbeStatus) -> &'static str {
    match status {
        ProbeStatus::Succeeded => "succeeded",
        ProbeStatus::Failed => "failed",
    }
}

fn format_tls_status(status: TlsStatus) -> &'static str {
    match status {
        TlsStatus::NotUsed => "not-used",
        TlsStatus::HandshakeSucceeded => "handshake-succeeded",
        TlsStatus::HandshakeFailed => "handshake-failed",
    }
}