ccboard_core/analytics/
insights.rs1use super::forecasting::{ForecastData, TrendDirection};
6use super::patterns::UsagePatterns;
7use super::trends::TrendsData;
8
9#[derive(Debug, Clone)]
11pub enum Alert {
12 BudgetWarning { current: f64, budget: f64, pct: f64 },
14 UsageSpike { day: String, tokens: u64, avg: u64 },
16 ProjectedOverage {
18 forecast: f64,
19 budget: f64,
20 overage: f64,
21 },
22}
23
24pub fn generate_insights(
37 _trends: &TrendsData,
38 patterns: &UsagePatterns,
39 forecast: &ForecastData,
40) -> Vec<String> {
41 let mut insights = Vec::new();
42
43 let total_sessions: usize = patterns.hourly_distribution.iter().sum();
45 if !patterns.peak_hours.is_empty() && total_sessions > 0 {
46 let peak_count: usize = patterns
47 .peak_hours
48 .iter()
49 .map(|&h| patterns.hourly_distribution[h as usize])
50 .sum();
51
52 if peak_count > total_sessions * 3 / 10 {
53 insights.push(format!(
54 "Peak hours: {:02}h-{:02}h ({:.0}% of sessions). Consider batching work.",
55 patterns.peak_hours.first().unwrap_or(&0),
56 patterns.peak_hours.last().unwrap_or(&23),
57 peak_count as f64 / total_sessions as f64 * 100.0
58 ));
59 }
60 }
61
62 if let Some(&opus_pct) = patterns.model_distribution.get("opus") {
64 if opus_pct > 0.2 {
65 insights.push(format!(
66 "Opus usage: {:.0}% tokens. Costs 3x more than Sonnet. Review necessity.",
67 opus_pct * 100.0
68 ));
69 }
70 }
71
72 for (model, &token_pct) in &patterns.model_distribution {
74 if let Some(&cost_pct) = patterns.model_cost_distribution.get(model) {
75 let cost_premium = cost_pct / token_pct;
76 if cost_premium > 1.5 && cost_pct > 0.2 {
77 insights.push(format!(
78 "{}: {:.0}% tokens but {:.0}% cost. Cost premium: {:.1}x.",
79 model,
80 token_pct * 100.0,
81 cost_pct * 100.0,
82 cost_premium
83 ));
84 }
85 }
86 }
87
88 if let TrendDirection::Up(pct) = forecast.trend_direction {
90 if pct > 20.0 && forecast.confidence > 0.5 {
91 insights.push(format!(
92 "Cost trend: +{:.0}% over period. Monthly estimate: ${:.2} (confidence: {:.0}%).",
93 pct,
94 forecast.monthly_cost_estimate,
95 forecast.confidence * 100.0
96 ));
97 }
98 }
99
100 let weekday_sum: usize = patterns.weekday_distribution[0..5].iter().sum();
102 let weekend_sum: usize = patterns.weekday_distribution[5..7].iter().sum();
103 let total = weekday_sum + weekend_sum;
104
105 if total > 0 {
106 let weekend_pct = weekend_sum as f64 / total as f64;
107 if weekend_pct < 0.1 {
108 insights.push(format!(
109 "Weekend usage: {:.0}%. Consider weekday-focused workflows.",
110 weekend_pct * 100.0
111 ));
112 }
113 }
114
115 if forecast.confidence < 0.5 {
117 insights.push(format!(
118 "Forecast confidence low ({:.0}%). Predictions may be unreliable.",
119 forecast.confidence * 100.0
120 ));
121 }
122
123 insights
124}
125
126pub fn generate_budget_alerts(
139 trends: &TrendsData,
140 forecast: &ForecastData,
141 monthly_budget: Option<f64>,
142 alert_threshold_pct: f64,
143) -> Vec<Alert> {
144 let mut alerts = Vec::new();
145
146 if let Some(budget) = monthly_budget {
147 let current_cost = forecast.monthly_cost_estimate;
149 let pct = (current_cost / budget * 100.0).min(100.0);
150
151 if pct >= alert_threshold_pct {
152 alerts.push(Alert::BudgetWarning {
153 current: current_cost,
154 budget,
155 pct,
156 });
157 }
158
159 if forecast.unavailable_reason.is_none() {
161 let projected = forecast.next_30_days_cost;
162 if projected > budget {
163 alerts.push(Alert::ProjectedOverage {
164 forecast: projected,
165 budget,
166 overage: projected - budget,
167 });
168 }
169 }
170 }
171
172 if !trends.daily_tokens.is_empty() {
174 let avg_tokens: u64 =
175 trends.daily_tokens.iter().sum::<u64>() / trends.daily_tokens.len() as u64;
176
177 for (i, &tokens) in trends.daily_tokens.iter().enumerate() {
178 if tokens > avg_tokens * 2 {
179 if let Some(day) = trends.dates.get(i) {
180 alerts.push(Alert::UsageSpike {
181 day: day.clone(),
182 tokens,
183 avg: avg_tokens,
184 });
185 }
186 }
187 }
188 }
189
190 alerts
191}