1use axum::{
7 extract::{Path, Query, State},
8 http::StatusCode,
9 Json,
10};
11use mockforge_analytics::{AnalyticsDatabase, PillarUsageMetrics};
12use serde::{Deserialize, Serialize};
13use std::sync::Arc;
14use tracing::{debug, error};
15
16use crate::models::ApiResponse;
17
18#[derive(Clone)]
20pub struct PillarAnalyticsState {
21 pub db: Arc<AnalyticsDatabase>,
22}
23
24impl PillarAnalyticsState {
25 pub fn new(db: AnalyticsDatabase) -> Self {
26 Self { db: Arc::new(db) }
27 }
28}
29
30#[derive(Debug, Deserialize)]
32pub struct PillarAnalyticsQuery {
33 #[serde(default = "default_duration")]
35 pub duration: i64,
36 pub start_time: Option<i64>,
38 pub end_time: Option<i64>,
40}
41
42fn default_duration() -> i64 {
43 3600 }
45
46pub async fn get_workspace_pillar_metrics(
55 State(state): State<PillarAnalyticsState>,
56 Path(workspace_id): Path<String>,
57 Query(query): Query<PillarAnalyticsQuery>,
58) -> Result<Json<ApiResponse<PillarUsageMetrics>>, StatusCode> {
59 debug!("Fetching pillar metrics for workspace: {}", workspace_id);
60
61 let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
62 end - start
63 } else {
64 query.duration
65 };
66
67 match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
68 Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
69 Err(e) => {
70 error!("Failed to get workspace pillar metrics: {}", e);
71 Err(StatusCode::INTERNAL_SERVER_ERROR)
72 }
73 }
74}
75
76pub async fn get_org_pillar_metrics(
80 State(state): State<PillarAnalyticsState>,
81 Path(org_id): Path<String>,
82 Query(query): Query<PillarAnalyticsQuery>,
83) -> Result<Json<ApiResponse<PillarUsageMetrics>>, StatusCode> {
84 debug!("Fetching pillar metrics for org: {}", org_id);
85
86 let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
87 end - start
88 } else {
89 query.duration
90 };
91
92 match state.db.get_org_pillar_metrics(&org_id, duration).await {
93 Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
94 Err(e) => {
95 error!("Failed to get org pillar metrics: {}", e);
96 Err(StatusCode::INTERNAL_SERVER_ERROR)
97 }
98 }
99}
100
101#[derive(Debug, Serialize)]
103pub struct RealityPillarDetails {
104 pub metrics: Option<mockforge_analytics::RealityPillarMetrics>,
105 pub time_range: String,
106}
107
108pub async fn get_reality_pillar_details(
109 State(state): State<PillarAnalyticsState>,
110 Path(workspace_id): Path<String>,
111 Query(query): Query<PillarAnalyticsQuery>,
112) -> Result<Json<ApiResponse<RealityPillarDetails>>, StatusCode> {
113 debug!("Fetching Reality pillar details for workspace: {}", workspace_id);
114
115 let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
116 end - start
117 } else {
118 query.duration
119 };
120
121 match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
122 Ok(metrics) => {
123 let details = RealityPillarDetails {
124 metrics: metrics.reality,
125 time_range: metrics.time_range,
126 };
127 Ok(Json(ApiResponse::success(details)))
128 }
129 Err(e) => {
130 error!("Failed to get Reality pillar details: {}", e);
131 Err(StatusCode::INTERNAL_SERVER_ERROR)
132 }
133 }
134}
135
136#[derive(Debug, Serialize)]
138pub struct ContractsPillarDetails {
139 pub metrics: Option<mockforge_analytics::ContractsPillarMetrics>,
140 pub time_range: String,
141}
142
143pub async fn get_contracts_pillar_details(
144 State(state): State<PillarAnalyticsState>,
145 Path(workspace_id): Path<String>,
146 Query(query): Query<PillarAnalyticsQuery>,
147) -> Result<Json<ApiResponse<ContractsPillarDetails>>, StatusCode> {
148 debug!("Fetching Contracts pillar details for workspace: {}", workspace_id);
149
150 let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
151 end - start
152 } else {
153 query.duration
154 };
155
156 match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
157 Ok(metrics) => {
158 let details = ContractsPillarDetails {
159 metrics: metrics.contracts,
160 time_range: metrics.time_range,
161 };
162 Ok(Json(ApiResponse::success(details)))
163 }
164 Err(e) => {
165 error!("Failed to get Contracts pillar details: {}", e);
166 Err(StatusCode::INTERNAL_SERVER_ERROR)
167 }
168 }
169}
170
171#[derive(Debug, Serialize)]
173pub struct AiPillarDetails {
174 pub metrics: Option<mockforge_analytics::AiPillarMetrics>,
175 pub time_range: String,
176}
177
178pub async fn get_ai_pillar_details(
179 State(state): State<PillarAnalyticsState>,
180 Path(workspace_id): Path<String>,
181 Query(query): Query<PillarAnalyticsQuery>,
182) -> Result<Json<ApiResponse<AiPillarDetails>>, StatusCode> {
183 debug!("Fetching AI pillar details for workspace: {}", workspace_id);
184
185 let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
186 end - start
187 } else {
188 query.duration
189 };
190
191 match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
192 Ok(metrics) => {
193 let details = AiPillarDetails {
194 metrics: metrics.ai,
195 time_range: metrics.time_range,
196 };
197 Ok(Json(ApiResponse::success(details)))
198 }
199 Err(e) => {
200 error!("Failed to get AI pillar details: {}", e);
201 Err(StatusCode::INTERNAL_SERVER_ERROR)
202 }
203 }
204}
205
206#[derive(Debug, Serialize)]
208pub struct PillarUsageSummary {
209 pub time_range: String,
211 pub rankings: Vec<PillarRanking>,
213 pub total_usage: u64,
215}
216
217#[derive(Debug, Serialize)]
219pub struct PillarRanking {
220 pub pillar: String,
222 pub usage: u64,
224 pub percentage: f64,
226 pub is_most_used: bool,
228 pub is_least_used: bool,
230}
231
232pub async fn get_pillar_usage_summary(
237 State(state): State<PillarAnalyticsState>,
238 Path(workspace_id): Path<String>,
239 Query(query): Query<PillarAnalyticsQuery>,
240) -> Result<Json<ApiResponse<PillarUsageSummary>>, StatusCode> {
241 debug!("Fetching pillar usage summary for workspace: {}", workspace_id);
242
243 let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
244 end - start
245 } else {
246 query.duration
247 };
248
249 match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
250 Ok(metrics) => {
251 let mut rankings = Vec::new();
252 let mut total_usage = 0u64;
253
254 if let Some(ref reality) = metrics.reality {
257 let usage = (reality.blended_reality_percent + reality.smart_personas_percent)
258 as u64
259 + reality.chaos_enabled_count;
260 rankings.push(PillarRanking {
261 pillar: "Reality".to_string(),
262 usage,
263 percentage: 0.0, is_most_used: false,
265 is_least_used: false,
266 });
267 total_usage += usage;
268 }
269
270 if let Some(ref contracts) = metrics.contracts {
272 let usage = contracts.validation_enforce_percent as u64
273 + contracts.drift_budget_configured_count
274 + contracts.drift_incidents_count;
275 rankings.push(PillarRanking {
276 pillar: "Contracts".to_string(),
277 usage,
278 percentage: 0.0,
279 is_most_used: false,
280 is_least_used: false,
281 });
282 total_usage += usage;
283 }
284
285 if let Some(ref devx) = metrics.devx {
287 let usage =
288 devx.sdk_installations + devx.client_generations + devx.playground_sessions;
289 rankings.push(PillarRanking {
290 pillar: "DevX".to_string(),
291 usage,
292 percentage: 0.0,
293 is_most_used: false,
294 is_least_used: false,
295 });
296 total_usage += usage;
297 }
298
299 if let Some(ref cloud) = metrics.cloud {
301 let usage = cloud.shared_scenarios_count
302 + cloud.marketplace_downloads
303 + cloud.collaborative_workspaces;
304 rankings.push(PillarRanking {
305 pillar: "Cloud".to_string(),
306 usage,
307 percentage: 0.0,
308 is_most_used: false,
309 is_least_used: false,
310 });
311 total_usage += usage;
312 }
313
314 if let Some(ref ai) = metrics.ai {
316 let usage =
317 ai.ai_generated_mocks + ai.ai_contract_diffs + ai.llm_assisted_operations;
318 rankings.push(PillarRanking {
319 pillar: "AI".to_string(),
320 usage,
321 percentage: 0.0,
322 is_most_used: false,
323 is_least_used: false,
324 });
325 total_usage += usage;
326 }
327
328 for ranking in &mut rankings {
330 if total_usage > 0 {
331 ranking.percentage = (ranking.usage as f64 / total_usage as f64) * 100.0;
332 }
333 }
334
335 rankings.sort_by(|a, b| b.usage.cmp(&a.usage));
336
337 let rankings_len = rankings.len();
339 if let Some(first) = rankings.first_mut() {
340 first.is_most_used = true;
341 }
342 if rankings_len > 1 {
343 if let Some(last) = rankings.last_mut() {
344 last.is_least_used = true;
345 }
346 }
347
348 let summary = PillarUsageSummary {
349 time_range: metrics.time_range,
350 rankings,
351 total_usage,
352 };
353
354 Ok(Json(ApiResponse::success(summary)))
355 }
356 Err(e) => {
357 error!("Failed to get pillar usage summary: {}", e);
358 Err(StatusCode::INTERNAL_SERVER_ERROR)
359 }
360 }
361}