mockforge_ui/handlers/
pillar_analytics.rs

1//! Pillar Usage Analytics API handlers
2//!
3//! Provides endpoints for querying pillar usage metrics (Reality, Contracts, DevX, Cloud, AI)
4//! at both workspace and organization levels.
5
6use 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/// Pillar analytics state
19#[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/// Query parameters for pillar analytics
31#[derive(Debug, Deserialize)]
32pub struct PillarAnalyticsQuery {
33    /// Duration in seconds (default: 3600 = 1 hour)
34    #[serde(default = "default_duration")]
35    pub duration: i64,
36    /// Start time (Unix timestamp, optional)
37    pub start_time: Option<i64>,
38    /// End time (Unix timestamp, optional)
39    pub end_time: Option<i64>,
40}
41
42fn default_duration() -> i64 {
43    3600 // 1 hour
44}
45
46/// Get pillar usage metrics for a workspace
47///
48/// Returns comprehensive pillar usage metrics including:
49/// - Reality pillar: blended reality usage, personas, chaos
50/// - Contracts pillar: validation modes, drift budgets
51/// - DevX pillar: SDK usage, client generations
52/// - Cloud pillar: shared scenarios, templates
53/// - AI pillar: AI-generated mocks, contract diffs
54pub 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
76/// Get pillar usage metrics for an organization
77///
78/// Returns aggregated pillar metrics across all workspaces in the organization.
79pub 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/// Get Reality pillar detailed metrics
102#[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/// Get Contracts pillar detailed metrics
137#[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/// Get AI pillar detailed metrics
172#[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/// Pillar usage summary showing most/least used pillars
207#[derive(Debug, Serialize)]
208pub struct PillarUsageSummary {
209    /// Time range for the summary
210    pub time_range: String,
211    /// Pillar usage rankings (sorted by usage, highest first)
212    pub rankings: Vec<PillarRanking>,
213    /// Total usage across all pillars
214    pub total_usage: u64,
215}
216
217/// Individual pillar ranking
218#[derive(Debug, Serialize)]
219pub struct PillarRanking {
220    /// Pillar name
221    pub pillar: String,
222    /// Usage count/score for this pillar
223    pub usage: u64,
224    /// Percentage of total usage
225    pub percentage: f64,
226    /// Whether this is the most used pillar
227    pub is_most_used: bool,
228    /// Whether this is the least used pillar
229    pub is_least_used: bool,
230}
231
232/// Get pillar usage summary showing most/least used pillars
233///
234/// This endpoint provides a high-level view of pillar usage, ranking pillars
235/// by their usage metrics to help identify where investment and usage are concentrated.
236pub 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            // Calculate usage scores for each pillar
255            // Reality: blended_reality_percent + smart_personas_percent + chaos_enabled_count
256            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, // Will calculate after total
264                    is_most_used: false,
265                    is_least_used: false,
266                });
267                total_usage += usage;
268            }
269
270            // Contracts: validation_enforce_percent + drift_budget_configured_count + drift_incidents_count
271            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            // DevX: sdk_installations + client_generations + playground_sessions
286            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            // Cloud: shared_scenarios_count + marketplace_downloads + collaborative_workspaces
300            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            // AI: ai_generated_mocks + ai_contract_diffs + llm_assisted_operations
315            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            // Calculate percentages and sort by usage (highest first)
329            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            // Mark most/least used
338            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}