mockforge_ui/handlers/
coverage_metrics.rs

1//! Coverage Metrics API handlers (MockOps)
2//!
3//! Provides endpoints for scenario usage, persona CI hits, endpoint coverage,
4//! reality level staleness, and drift percentage metrics.
5
6use axum::{extract::Query, http::StatusCode, Json};
7use mockforge_analytics::{
8    AnalyticsDatabase, DriftPercentageMetrics, EndpointCoverage, PersonaCIHit,
9    RealityLevelStaleness, ScenarioUsageMetrics,
10};
11use serde::Deserialize;
12use std::sync::Arc;
13use tracing::{debug, error};
14
15use crate::models::ApiResponse;
16
17/// Coverage metrics state
18#[derive(Clone)]
19pub struct CoverageMetricsState {
20    pub db: Arc<tokio::sync::OnceCell<AnalyticsDatabase>>,
21}
22
23impl CoverageMetricsState {
24    pub fn new(db: AnalyticsDatabase) -> Self {
25        let cell = tokio::sync::OnceCell::new();
26        let _ = cell.set(db);
27        Self { db: Arc::new(cell) }
28    }
29
30    async fn get_db(&self) -> Result<&AnalyticsDatabase, StatusCode> {
31        self.db.get().ok_or_else(|| {
32            error!("Analytics database not initialized");
33            StatusCode::SERVICE_UNAVAILABLE
34        })
35    }
36}
37
38/// Query parameters for coverage metrics endpoints
39#[derive(Debug, Deserialize)]
40pub struct CoverageQuery {
41    /// Workspace ID filter
42    pub workspace_id: Option<String>,
43    /// Organization ID filter
44    pub org_id: Option<String>,
45    /// Limit results
46    pub limit: Option<i64>,
47    /// Minimum coverage percentage (for endpoint coverage)
48    pub min_coverage: Option<f64>,
49    /// Maximum staleness days (for reality level staleness)
50    pub max_staleness_days: Option<i32>,
51}
52
53/// Get scenario usage metrics
54///
55/// GET /api/v2/analytics/scenarios/usage
56pub async fn get_scenario_usage(
57    axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
58    Query(params): Query<CoverageQuery>,
59) -> Result<Json<ApiResponse<Vec<ScenarioUsageMetrics>>>, StatusCode> {
60    debug!("Getting scenario usage metrics");
61
62    let db = state.get_db().await?;
63    match db
64        .get_scenario_usage(params.workspace_id.as_deref(), params.org_id.as_deref(), params.limit)
65        .await
66    {
67        Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
68        Err(e) => {
69            error!("Failed to get scenario usage metrics: {}", e);
70            Err(StatusCode::INTERNAL_SERVER_ERROR)
71        }
72    }
73}
74
75/// Get persona CI hits
76///
77/// GET /api/v2/analytics/personas/ci-hits
78pub async fn get_persona_ci_hits(
79    axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
80    Query(params): Query<CoverageQuery>,
81) -> Result<Json<ApiResponse<Vec<PersonaCIHit>>>, StatusCode> {
82    debug!("Getting persona CI hits");
83
84    let db = state.get_db().await?;
85    match db
86        .get_persona_ci_hits(params.workspace_id.as_deref(), params.org_id.as_deref(), params.limit)
87        .await
88    {
89        Ok(hits) => Ok(Json(ApiResponse::success(hits))),
90        Err(e) => {
91            error!("Failed to get persona CI hits: {}", e);
92            Err(StatusCode::INTERNAL_SERVER_ERROR)
93        }
94    }
95}
96
97/// Get endpoint coverage
98///
99/// GET /api/v2/analytics/endpoints/coverage
100pub async fn get_endpoint_coverage(
101    axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
102    Query(params): Query<CoverageQuery>,
103) -> Result<Json<ApiResponse<Vec<EndpointCoverage>>>, StatusCode> {
104    debug!("Getting endpoint coverage");
105
106    let db = state.get_db().await?;
107    match db
108        .get_endpoint_coverage(
109            params.workspace_id.as_deref(),
110            params.org_id.as_deref(),
111            params.min_coverage,
112        )
113        .await
114    {
115        Ok(coverage) => Ok(Json(ApiResponse::success(coverage))),
116        Err(e) => {
117            error!("Failed to get endpoint coverage: {}", e);
118            Err(StatusCode::INTERNAL_SERVER_ERROR)
119        }
120    }
121}
122
123/// Get reality level staleness
124///
125/// GET /api/v2/analytics/reality-levels/staleness
126pub async fn get_reality_level_staleness(
127    axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
128    Query(params): Query<CoverageQuery>,
129) -> Result<Json<ApiResponse<Vec<RealityLevelStaleness>>>, StatusCode> {
130    debug!("Getting reality level staleness");
131
132    let db = state.get_db().await?;
133    match db
134        .get_reality_level_staleness(
135            params.workspace_id.as_deref(),
136            params.org_id.as_deref(),
137            params.max_staleness_days,
138        )
139        .await
140    {
141        Ok(staleness) => Ok(Json(ApiResponse::success(staleness))),
142        Err(e) => {
143            error!("Failed to get reality level staleness: {}", e);
144            Err(StatusCode::INTERNAL_SERVER_ERROR)
145        }
146    }
147}
148
149/// Get drift percentage metrics
150///
151/// GET /api/v2/analytics/drift/percentage
152pub async fn get_drift_percentage(
153    axum::extract::Extension(state): axum::extract::Extension<CoverageMetricsState>,
154    Query(params): Query<CoverageQuery>,
155) -> Result<Json<ApiResponse<Vec<DriftPercentageMetrics>>>, StatusCode> {
156    debug!("Getting drift percentage metrics");
157
158    let db = state.get_db().await?;
159    match db
160        .get_drift_percentage(
161            params.workspace_id.as_deref(),
162            params.org_id.as_deref(),
163            params.limit,
164        )
165        .await
166    {
167        Ok(metrics) => Ok(Json(ApiResponse::success(metrics))),
168        Err(e) => {
169            error!("Failed to get drift percentage metrics: {}", e);
170            Err(StatusCode::INTERNAL_SERVER_ERROR)
171        }
172    }
173}
174
175/// Create coverage metrics router
176/// Note: Returns a router without state - handlers will get state from extensions
177pub fn coverage_metrics_router() -> axum::Router {
178    use axum::routing::get;
179
180    axum::Router::new()
181        .route("/scenarios/usage", get(get_scenario_usage))
182        .route("/personas/ci-hits", get(get_persona_ci_hits))
183        .route("/endpoints/coverage", get(get_endpoint_coverage))
184        .route("/reality-levels/staleness", get(get_reality_level_staleness))
185        .route("/drift/percentage", get(get_drift_percentage))
186}