use axum::{
extract::{Path, Query, State},
http::StatusCode,
Json,
};
use mockforge_analytics::{AnalyticsDatabase, PillarUsageMetrics};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tracing::{debug, error};
use crate::models::ApiResponse;
#[derive(Clone)]
pub struct PillarAnalyticsState {
pub db: Arc<AnalyticsDatabase>,
}
impl PillarAnalyticsState {
pub fn new(db: AnalyticsDatabase) -> Self {
Self { db: Arc::new(db) }
}
}
#[derive(Debug, Deserialize)]
pub struct PillarAnalyticsQuery {
#[serde(default = "default_duration")]
pub duration: i64,
pub start_time: Option<i64>,
pub end_time: Option<i64>,
}
fn default_duration() -> i64 {
3600 }
pub async fn get_workspace_pillar_metrics(
State(state): State<PillarAnalyticsState>,
Path(workspace_id): Path<String>,
Query(query): Query<PillarAnalyticsQuery>,
) -> Result<Json<ApiResponse<PillarUsageMetrics>>, StatusCode> {
debug!("Fetching pillar metrics for workspace: {}", workspace_id);
let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
end - start
} else {
query.duration
};
match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
Err(e) => {
error!("Failed to get workspace pillar metrics: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
pub async fn get_org_pillar_metrics(
State(state): State<PillarAnalyticsState>,
Path(org_id): Path<String>,
Query(query): Query<PillarAnalyticsQuery>,
) -> Result<Json<ApiResponse<PillarUsageMetrics>>, StatusCode> {
debug!("Fetching pillar metrics for org: {}", org_id);
let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
end - start
} else {
query.duration
};
match state.db.get_org_pillar_metrics(&org_id, duration).await {
Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
Err(e) => {
error!("Failed to get org pillar metrics: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
#[derive(Debug, Serialize)]
pub struct RealityPillarDetails {
pub metrics: Option<mockforge_analytics::RealityPillarMetrics>,
pub time_range: String,
}
pub async fn get_reality_pillar_details(
State(state): State<PillarAnalyticsState>,
Path(workspace_id): Path<String>,
Query(query): Query<PillarAnalyticsQuery>,
) -> Result<Json<ApiResponse<RealityPillarDetails>>, StatusCode> {
debug!("Fetching Reality pillar details for workspace: {}", workspace_id);
let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
end - start
} else {
query.duration
};
match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
Ok(metrics) => {
let details = RealityPillarDetails {
metrics: metrics.reality,
time_range: metrics.time_range,
};
Ok(Json(ApiResponse::success(details)))
}
Err(e) => {
error!("Failed to get Reality pillar details: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
#[derive(Debug, Serialize)]
pub struct ContractsPillarDetails {
pub metrics: Option<mockforge_analytics::ContractsPillarMetrics>,
pub time_range: String,
}
pub async fn get_contracts_pillar_details(
State(state): State<PillarAnalyticsState>,
Path(workspace_id): Path<String>,
Query(query): Query<PillarAnalyticsQuery>,
) -> Result<Json<ApiResponse<ContractsPillarDetails>>, StatusCode> {
debug!("Fetching Contracts pillar details for workspace: {}", workspace_id);
let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
end - start
} else {
query.duration
};
match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
Ok(metrics) => {
let details = ContractsPillarDetails {
metrics: metrics.contracts,
time_range: metrics.time_range,
};
Ok(Json(ApiResponse::success(details)))
}
Err(e) => {
error!("Failed to get Contracts pillar details: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
#[derive(Debug, Serialize)]
pub struct AiPillarDetails {
pub metrics: Option<mockforge_analytics::AiPillarMetrics>,
pub time_range: String,
}
pub async fn get_ai_pillar_details(
State(state): State<PillarAnalyticsState>,
Path(workspace_id): Path<String>,
Query(query): Query<PillarAnalyticsQuery>,
) -> Result<Json<ApiResponse<AiPillarDetails>>, StatusCode> {
debug!("Fetching AI pillar details for workspace: {}", workspace_id);
let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
end - start
} else {
query.duration
};
match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
Ok(metrics) => {
let details = AiPillarDetails {
metrics: metrics.ai,
time_range: metrics.time_range,
};
Ok(Json(ApiResponse::success(details)))
}
Err(e) => {
error!("Failed to get AI pillar details: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}
#[derive(Debug, Serialize)]
pub struct PillarUsageSummary {
pub time_range: String,
pub rankings: Vec<PillarRanking>,
pub total_usage: u64,
}
#[derive(Debug, Serialize)]
pub struct PillarRanking {
pub pillar: String,
pub usage: u64,
pub percentage: f64,
pub is_most_used: bool,
pub is_least_used: bool,
}
pub async fn get_pillar_usage_summary(
State(state): State<PillarAnalyticsState>,
Path(workspace_id): Path<String>,
Query(query): Query<PillarAnalyticsQuery>,
) -> Result<Json<ApiResponse<PillarUsageSummary>>, StatusCode> {
debug!("Fetching pillar usage summary for workspace: {}", workspace_id);
let duration = if let (Some(start), Some(end)) = (query.start_time, query.end_time) {
end - start
} else {
query.duration
};
match state.db.get_workspace_pillar_metrics(&workspace_id, duration).await {
Ok(metrics) => {
let mut rankings = Vec::new();
let mut total_usage = 0u64;
if let Some(ref reality) = metrics.reality {
let usage = (reality.blended_reality_percent + reality.smart_personas_percent)
as u64
+ reality.chaos_enabled_count;
rankings.push(PillarRanking {
pillar: "Reality".to_string(),
usage,
percentage: 0.0, is_most_used: false,
is_least_used: false,
});
total_usage += usage;
}
if let Some(ref contracts) = metrics.contracts {
let usage = contracts.validation_enforce_percent as u64
+ contracts.drift_budget_configured_count
+ contracts.drift_incidents_count;
rankings.push(PillarRanking {
pillar: "Contracts".to_string(),
usage,
percentage: 0.0,
is_most_used: false,
is_least_used: false,
});
total_usage += usage;
}
if let Some(ref devx) = metrics.devx {
let usage =
devx.sdk_installations + devx.client_generations + devx.playground_sessions;
rankings.push(PillarRanking {
pillar: "DevX".to_string(),
usage,
percentage: 0.0,
is_most_used: false,
is_least_used: false,
});
total_usage += usage;
}
if let Some(ref cloud) = metrics.cloud {
let usage = cloud.shared_scenarios_count
+ cloud.marketplace_downloads
+ cloud.collaborative_workspaces;
rankings.push(PillarRanking {
pillar: "Cloud".to_string(),
usage,
percentage: 0.0,
is_most_used: false,
is_least_used: false,
});
total_usage += usage;
}
if let Some(ref ai) = metrics.ai {
let usage =
ai.ai_generated_mocks + ai.ai_contract_diffs + ai.llm_assisted_operations;
rankings.push(PillarRanking {
pillar: "AI".to_string(),
usage,
percentage: 0.0,
is_most_used: false,
is_least_used: false,
});
total_usage += usage;
}
for ranking in &mut rankings {
if total_usage > 0 {
ranking.percentage = (ranking.usage as f64 / total_usage as f64) * 100.0;
}
}
rankings.sort_by(|a, b| b.usage.cmp(&a.usage));
let rankings_len = rankings.len();
if let Some(first) = rankings.first_mut() {
first.is_most_used = true;
}
if rankings_len > 1 {
if let Some(last) = rankings.last_mut() {
last.is_least_used = true;
}
}
let summary = PillarUsageSummary {
time_range: metrics.time_range,
rankings,
total_usage,
};
Ok(Json(ApiResponse::success(summary)))
}
Err(e) => {
error!("Failed to get pillar usage summary: {}", e);
Err(StatusCode::INTERNAL_SERVER_ERROR)
}
}
}