1use serde_json::{json, Value};
12use std::io::Write;
13use std::path::{Path, PathBuf};
14use std::time::{SystemTime, UNIX_EPOCH};
15
16const MAX_BYTES: u64 = 768 * 1024; const TRIM_KEEP: usize = 2000; const NOISE_CMDS: [&str; 3] = ["get", "sub", "unsub"];
25
26fn log_path() -> PathBuf {
27 crate::store::theme_dir().join("hostlog.jsonl")
28}
29
30fn now_ms() -> u64 {
31 SystemTime::now()
32 .duration_since(UNIX_EPOCH)
33 .map(|d| d.as_millis() as u64)
34 .unwrap_or(0)
35}
36
37fn cmd_of(req: &Value) -> &str {
39 if let Some(c) = req.get("cmd").and_then(|c| c.as_str()) {
40 return c;
41 }
42 if !req["scheme"].is_null() {
43 "scheme"
44 } else if !req["ui"].is_null() {
45 "ui"
46 } else {
47 "?"
48 }
49}
50
51pub fn record(dir: &str, req: &Value, data: &Value) {
55 let cmd = cmd_of(req);
56 if cmd == "hostlog"
59 || NOISE_CMDS.contains(&cmd)
60 || req.get("_nolog").and_then(|b| b.as_bool()).unwrap_or(false)
61 {
62 return;
63 }
64 let mut summary = data.to_string();
65 if summary.len() > 400 {
66 summary.truncate(400);
67 summary.push('…');
68 }
69 let entry = json!({
70 "t": now_ms(),
71 "dir": dir,
72 "cmd": cmd,
73 "pid": std::process::id(),
74 "msg": summary,
75 });
76 let path = log_path();
77 if let Ok(mut f) = std::fs::OpenOptions::new()
78 .create(true)
79 .append(true) .open(&path)
81 {
82 let _ = writeln!(f, "{entry}");
83 }
84 if std::fs::metadata(&path).map(|m| m.len()).unwrap_or(0) > MAX_BYTES {
85 trim(&path);
86 }
87}
88
89fn trim(path: &Path) {
92 let Ok(data) = std::fs::read_to_string(path) else {
93 return;
94 };
95 let lines: Vec<&str> = data.lines().collect();
96 if lines.len() <= TRIM_KEEP {
97 return;
98 }
99 let tail = lines[lines.len() - TRIM_KEEP..].join("\n");
100 let tmp = path.with_extension("jsonl.tmp");
101 if std::fs::write(&tmp, format!("{tail}\n")).is_ok() {
102 let _ = std::fs::rename(&tmp, path);
103 }
104}
105
106pub fn read_tail(n: usize) -> Vec<Value> {
108 let Ok(data) = std::fs::read_to_string(log_path()) else {
109 return Vec::new();
110 };
111 let lines: Vec<&str> = data.lines().collect();
112 let start = lines.len().saturating_sub(n);
113 lines[start..]
114 .iter()
115 .filter_map(|l| serde_json::from_str::<Value>(l).ok())
116 .collect()
117}