mockforge_http/handlers/
xray.rs

1//! X-Ray API handlers for frontend debugging
2//!
3//! This module provides lightweight API endpoints for the browser extension
4//! to display current state (scenario, persona, reality level, chaos rules).
5
6use axum::{
7    extract::{Path, Query, State},
8    http::StatusCode,
9    response::Json,
10};
11use mockforge_core::consistency::ConsistencyEngine;
12use serde::{Deserialize, Serialize};
13use serde_json::Value;
14use std::sync::Arc;
15use tracing::debug;
16use uuid::Uuid;
17
18/// State for X-Ray handlers
19#[derive(Clone)]
20pub struct XRayState {
21    /// Consistency engine
22    pub engine: Arc<ConsistencyEngine>,
23}
24
25/// Query parameters for X-Ray operations
26#[derive(Debug, Deserialize)]
27pub struct XRayQuery {
28    /// Workspace ID (defaults to "default" if not provided)
29    #[serde(default = "default_workspace")]
30    pub workspace: String,
31}
32
33fn default_workspace() -> String {
34    "default".to_string()
35}
36
37/// Get current state summary (optimized for extension overlay)
38///
39/// GET /api/v1/xray/state/summary?workspace={workspace_id}
40///
41/// Returns a lightweight summary suitable for the browser extension overlay.
42pub async fn get_state_summary(
43    State(state): State<XRayState>,
44    Query(params): Query<XRayQuery>,
45) -> Result<Json<Value>, StatusCode> {
46    let unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
47        debug!("No state found for workspace: {}", params.workspace);
48        StatusCode::NOT_FOUND
49    })?;
50
51    // Build lightweight summary
52    let summary = serde_json::json!({
53        "workspace_id": unified_state.workspace_id,
54        "scenario": unified_state.active_scenario,
55        "persona": unified_state.active_persona.as_ref().map(|p| serde_json::json!({
56            "id": p.id,
57            "traits": p.traits,
58        })),
59        "reality_level": unified_state.reality_level.value(),
60        "reality_level_name": unified_state.reality_level.name(),
61        "reality_ratio": unified_state.reality_continuum_ratio,
62        // Note: ChaosScenario is now serde_json::Value, so we extract the name field
63        "chaos_rules": unified_state
64            .active_chaos_rules
65            .iter()
66            .filter_map(|r| r.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()))
67            .collect::<Vec<_>>(),
68        "timestamp": unified_state.last_updated,
69    });
70
71    Ok(Json(summary))
72}
73
74/// Get full state (for DevTools panel)
75///
76/// GET /api/v1/xray/state?workspace={workspace_id}
77pub async fn get_state(
78    State(state): State<XRayState>,
79    Query(params): Query<XRayQuery>,
80) -> Result<Json<Value>, StatusCode> {
81    let unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
82        debug!("No state found for workspace: {}", params.workspace);
83        StatusCode::NOT_FOUND
84    })?;
85
86    Ok(Json(serde_json::to_value(&unified_state).unwrap()))
87}
88
89/// Get request context for a specific request ID
90///
91/// GET /api/v1/xray/request-context/{request_id}?workspace={workspace_id}
92///
93/// Returns the state that was active when a specific request was made.
94/// Request IDs are provided in X-MockForge-Request-ID headers.
95pub async fn get_request_context(
96    State(_state): State<XRayState>,
97    Path(request_id): Path<String>,
98    Query(params): Query<XRayQuery>,
99) -> Result<Json<Value>, StatusCode> {
100    // TODO: Implement request context storage/retrieval
101    // For now, return current state
102    debug!("Request context lookup for request_id: {} (not yet implemented)", request_id);
103    Ok(Json(serde_json::json!({
104        "request_id": request_id,
105        "workspace": params.workspace,
106        "message": "Request context tracking not yet implemented",
107    })))
108}
109
110/// Get workspace summary
111///
112/// GET /api/v1/xray/workspace/{workspace_id}/summary
113pub async fn get_workspace_summary(
114    State(state): State<XRayState>,
115    Path(workspace_id): Path<String>,
116) -> Result<Json<Value>, StatusCode> {
117    let unified_state = state.engine.get_state(&workspace_id).await.ok_or_else(|| {
118        debug!("No state found for workspace: {}", workspace_id);
119        StatusCode::NOT_FOUND
120    })?;
121
122    let summary = serde_json::json!({
123        "workspace_id": unified_state.workspace_id,
124        "scenario": unified_state.active_scenario,
125        "persona_id": unified_state.active_persona.as_ref().map(|p| p.id.clone()),
126        "reality_level": unified_state.reality_level.value(),
127        "reality_ratio": unified_state.reality_continuum_ratio,
128        "active_chaos_rules_count": unified_state.active_chaos_rules.len(),
129        "entity_count": unified_state.entity_state.len(),
130        "protocol_count": unified_state.protocol_states.len(),
131        "last_updated": unified_state.last_updated,
132    });
133
134    Ok(Json(summary))
135}
136
137/// List all entities for a workspace
138///
139/// GET /api/v1/xray/entities?workspace={workspace_id}
140pub async fn list_entities(
141    State(state): State<XRayState>,
142    Query(params): Query<XRayQuery>,
143) -> Result<Json<Value>, StatusCode> {
144    let unified_state = state.engine.get_state(&params.workspace).await.ok_or_else(|| {
145        debug!("No state found for workspace: {}", params.workspace);
146        StatusCode::NOT_FOUND
147    })?;
148
149    let entities: Vec<&mockforge_core::consistency::EntityState> =
150        unified_state.entity_state.values().collect();
151
152    Ok(Json(serde_json::json!({
153        "workspace": params.workspace,
154        "entities": entities,
155        "count": entities.len(),
156    })))
157}
158
159/// Get specific entity
160///
161/// GET /api/v1/xray/entities/{entity_type}/{entity_id}?workspace={workspace_id}
162pub async fn get_entity(
163    State(state): State<XRayState>,
164    Path((entity_type, entity_id)): Path<(String, String)>,
165    Query(params): Query<XRayQuery>,
166) -> Result<Json<Value>, StatusCode> {
167    let entity = state
168        .engine
169        .get_entity(&params.workspace, &entity_type, &entity_id)
170        .await
171        .ok_or_else(|| {
172            debug!(
173                "Entity not found: {}:{} in workspace: {}",
174                entity_type, entity_id, params.workspace
175            );
176            StatusCode::NOT_FOUND
177        })?;
178
179    Ok(Json(serde_json::to_value(&entity).unwrap()))
180}
181
182/// Create X-Ray router
183pub fn xray_router(state: XRayState) -> axum::Router {
184    use axum::routing::get;
185
186    axum::Router::new()
187        .route("/api/v1/xray/state/summary", get(get_state_summary))
188        .route("/api/v1/xray/state", get(get_state))
189        .route("/api/v1/xray/request-context/{request_id}", get(get_request_context))
190        .route("/api/v1/xray/workspace/{workspace_id}/summary", get(get_workspace_summary))
191        .route("/api/v1/xray/entities", get(list_entities))
192        .route("/api/v1/xray/entities/{entity_type}/{entity_id}", get(get_entity))
193        .with_state(state)
194}