1use std::collections::BTreeMap;
5
6use super::record::{snapshot, Day, Top};
7
8fn quote(s: &str) -> String {
9 let mut out = String::with_capacity(s.len() + 2);
10 out.push('"');
11 for c in s.chars() {
12 match c {
13 '"' => out.push_str("\\\""),
14 '\\' => out.push_str("\\\\"),
15 c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
16 c => out.push(c),
17 }
18 }
19 out.push('"');
20 out
21}
22
23fn pairs(top: &Top, n: usize, host: Option<&str>) -> String {
24 let mut filtered = Top::default();
25 for (key, count) in &top.0 {
26 if host.is_none_or(|h| key.starts_with(h) || key == "(other)") {
27 filtered.0.insert(key.clone(), *count);
28 }
29 }
30 let rows: Vec<String> = filtered.sorted(n).iter().map(|(k, v)| format!("[{},{v}]", quote(k))).collect();
31 format!("[{}]", rows.join(","))
32}
33
34pub fn json(hours: u64, host: Option<&str>, now: u64) -> String {
36 let hours = hours.clamp(1, 24 * 90);
37 let (buckets, days) = snapshot(now);
38 let names = super::record::host_names();
39 let host_index = host.and_then(|h| names.iter().position(|n| n.eq_ignore_ascii_case(h))).map(|i| i as u8);
40 let wanted = |h: u8| host.is_none() || Some(h) == host_index;
41 let this_hour = now / 3600;
42 let start = this_hour + 1 - hours;
43 let previous_start = start.saturating_sub(hours);
44
45 let mut totals = [[0u64; 2]; 3];
46 let mut previous = [[0u64; 2]; 3];
47 let daily = hours > 48;
48 let mut series: BTreeMap<u64, [u64; 3]> = BTreeMap::new();
49 let mut first_hour = None;
50 for ((hour, h), counts) in &buckets {
51 if !wanted(*h) {
52 continue;
53 }
54 first_hour = Some(first_hour.map_or(*hour, |f: u64| f.min(*hour)));
55 let target = if *hour >= start && *hour <= this_hour {
56 Some(&mut totals)
57 } else if *hour >= previous_start && *hour < start {
58 Some(&mut previous)
59 } else {
60 None
61 };
62 if let Some(t) = target {
63 for g in 0..3 {
64 t[g][0] += counts[g].requests;
65 t[g][1] += counts[g].pages;
66 }
67 }
68 if *hour >= start && *hour <= this_hour {
69 let key = if daily { hour / 24 * 24 } else { *hour };
70 let point = series.entry(key).or_default();
71 for g in 0..3 {
72 point[g] += counts[g].requests;
73 }
74 }
75 }
76 let step = if daily { 24 } else { 1 };
78 let mut t = if daily { start / 24 * 24 } else { start };
79 while t <= this_hour {
80 series.entry(t).or_default();
81 t += step;
82 }
83
84 let start_day = start / 24;
85 let previous_day = previous_start / 24;
86 let mut lists = Day::default();
87 let mut visitors = [0u64; 3];
88 let mut previous_visitors = [0u64; 3];
89 for (day, d) in &days {
90 if *day >= start_day {
91 lists.merge(d);
92 for g in 0..3 {
93 visitors[g] += d.visitors[g];
94 }
95 } else if *day >= previous_day {
96 for g in 0..3 {
97 previous_visitors[g] += d.visitors[g];
98 }
99 }
100 }
101
102 let group = |t: &[[u64; 2]; 3], v: &[u64; 3], g: usize| format!(r#"{{"requests":{},"pages":{},"visitors":{}}}"#, t[g][0], t[g][1], v[g]);
103 let groups = |t: &[[u64; 2]; 3], v: &[u64; 3]| format!(r#"{{"people":{},"ai":{},"bots":{}}}"#, group(t, v, 0), group(t, v, 1), group(t, v, 2));
104 let points: Vec<String> = series.iter().map(|(t, p)| format!(r#"{{"t":{},"people":{},"ai":{},"bots":{}}}"#, t * 3600, p[0], p[1], p[2])).collect();
105 let agents: Vec<String> = lists
106 .ai_agents
107 .sorted(25)
108 .iter()
109 .map(|(key, n)| {
110 let mut parts = key.split('\t');
111 let (name, operator, kind, verified) = (parts.next().unwrap_or(""), parts.next().unwrap_or(""), parts.next().unwrap_or(""), parts.next() == Some("true"));
112 format!(r#"{{"name":{},"operator":{},"kind":{},"verified":{verified},"requests":{n}}}"#, quote(name), quote(operator), quote(kind))
113 })
114 .collect();
115 let bots: Vec<String> = lists
116 .bots
117 .sorted(25)
118 .iter()
119 .map(|(key, n)| {
120 let (kind, name) = key.split_once('\t').unwrap_or((key, ""));
121 format!(r#"{{"kind":{},"name":{},"requests":{n}}}"#, quote(kind), quote(name))
122 })
123 .collect();
124 format!(
125 r#"{{"hours":{hours},"bucket":"{}","since":{},"hosts":[{}],"host":{},"totals":{},"previous":{},"series":[{}],"people_pages":{},"ai_pages":{},"ai_agents":[{}],"bots":[{}],"referrers":{},"not_found":{}}}"#,
126 if daily { "day" } else { "hour" },
127 first_hour.map_or("null".to_string(), |h| (h * 3600).to_string()),
128 names.iter().map(|n| quote(n)).collect::<Vec<_>>().join(","),
129 host.map_or("null".to_string(), quote),
130 groups(&totals, &visitors),
131 groups(&previous, &previous_visitors),
132 points.join(","),
133 pairs(&lists.people_pages, 25, host),
134 pairs(&lists.ai_pages, 25, host),
135 agents.join(","),
136 bots.join(","),
137 pairs(&lists.referrers, 25, None),
138 pairs(&lists.not_found, 25, host),
139 )
140}