use std::io::Write;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use crate::engine::models::TestReport;
const MBPS: f64 = 1_000_000.0;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct HistoryRecord {
pub timestamp: u64,
pub server: String,
pub ping_ms: f64,
pub jitter_ms: f64,
pub packet_loss_pct: f64,
pub download_mbps: f64,
pub download_peak_mbps: f64,
pub download_bytes: u64,
pub upload_mbps: f64,
pub upload_peak_mbps: f64,
pub upload_bytes: u64,
}
impl HistoryRecord {
#[must_use]
pub fn from_report(report: &TestReport) -> Self {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |elapsed| elapsed.as_secs());
Self {
timestamp,
server: report.server_name.clone(),
ping_ms: round1(report.latency.average_ms),
jitter_ms: round1(report.latency.jitter_ms),
packet_loss_pct: round1(report.latency.packet_loss_pct),
download_mbps: round1(report.download.average_bps / MBPS),
download_peak_mbps: round1(report.download.peak_bps / MBPS),
download_bytes: report.download.bytes,
upload_mbps: round1(report.upload.average_bps / MBPS),
upload_peak_mbps: round1(report.upload.peak_bps / MBPS),
upload_bytes: report.upload.bytes,
}
}
pub fn to_json(&self) -> serde_json::Result<String> {
serde_json::to_string(self)
}
}
#[must_use]
pub fn default_path() -> Option<PathBuf> {
dirs::data_dir().map(|dir| dir.join("netspd").join("history.jsonl"))
}
pub fn append(path: &Path, record: &HistoryRecord) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
let line = record
.to_json()
.map_err(|err| std::io::Error::new(std::io::ErrorKind::InvalidData, err))?;
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)?;
writeln!(file, "{line}")
}
pub fn record_report(report: &TestReport) {
if let Some(path) = default_path() {
let _ = append(&path, &HistoryRecord::from_report(report));
}
}
fn round1(value: f64) -> f64 {
(value * 10.0).round() / 10.0
}