1use axum::{
7 extract::{Path, Query, State},
8 http::StatusCode,
9 response::Json,
10};
11use mockforge_core::consistency::ConsistencyEngine;
12use serde::Deserialize;
13use serde_json::Value;
14use std::collections::HashMap;
15use std::sync::Arc;
16use tokio::sync::RwLock;
17use tracing::debug;
18
19#[derive(Debug, Clone)]
21pub(crate) struct RequestContextSnapshot {
22 workspace_id: String,
24 state_snapshot: serde_json::Value,
26 timestamp: i64,
28}
29
30#[derive(Clone)]
32pub struct XRayState {
33 pub engine: Arc<ConsistencyEngine>,
35 pub request_contexts: Arc<RwLock<HashMap<String, RequestContextSnapshot>>>,
37}
38
39#[derive(Debug, Deserialize)]
41pub struct XRayQuery {
42 #[serde(default = "default_workspace")]
44 pub workspace: String,
45}
46
47fn default_workspace() -> String {
48 "default".to_string()
49}
50
51pub async fn get_state_summary(
57 State(state): State<XRayState>,
58 Query(params): Query<XRayQuery>,
59) -> Result<Json<Value>, StatusCode> {
60 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
61 debug!("No state found for workspace: {}", params.workspace);
62 StatusCode::NOT_FOUND
63 })?;
64
65 let summary = serde_json::json!({
67 "workspace_id": unified_state.workspace_id,
68 "scenario": unified_state.active_scenario,
69 "persona": unified_state.active_persona.as_ref().map(|p| serde_json::json!({
70 "id": p.id,
71 "traits": p.traits,
72 })),
73 "reality_level": unified_state.reality_level.value(),
74 "reality_level_name": unified_state.reality_level.name(),
75 "reality_ratio": unified_state.reality_continuum_ratio,
76 "chaos_rules": unified_state
78 .active_chaos_rules
79 .iter()
80 .filter_map(|r| r.get("name").and_then(|v| v.as_str()).map(|s| s.to_string()))
81 .collect::<Vec<_>>(),
82 "timestamp": unified_state.last_updated,
83 });
84
85 Ok(Json(summary))
86}
87
88pub async fn get_state(
92 State(state): State<XRayState>,
93 Query(params): Query<XRayQuery>,
94) -> Result<Json<Value>, StatusCode> {
95 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
96 debug!("No state found for workspace: {}", params.workspace);
97 StatusCode::NOT_FOUND
98 })?;
99
100 Ok(Json(serde_json::to_value(&unified_state).unwrap()))
101}
102
103pub async fn get_request_context(
110 State(state): State<XRayState>,
111 Path(request_id): Path<String>,
112 Query(params): Query<XRayQuery>,
113) -> Result<Json<Value>, StatusCode> {
114 let contexts = state.request_contexts.read().await;
116 if let Some(snapshot) = contexts.get(&request_id) {
117 if snapshot.workspace_id == params.workspace {
119 return Ok(Json(serde_json::json!({
120 "request_id": request_id,
121 "workspace": snapshot.workspace_id,
122 "state_snapshot": snapshot.state_snapshot,
123 "timestamp": snapshot.timestamp,
124 "cached": true,
125 })));
126 }
127 }
128 drop(contexts);
129
130 debug!(
132 "Request context not found for request_id: {}, returning current state",
133 request_id
134 );
135 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
136 debug!("No state found for workspace: {}", params.workspace);
137 StatusCode::NOT_FOUND
138 })?;
139
140 Ok(Json(serde_json::json!({
141 "request_id": request_id,
142 "workspace": params.workspace,
143 "state_snapshot": serde_json::to_value(&unified_state).unwrap(),
144 "timestamp": unified_state.last_updated,
145 "cached": false,
146 "note": "Snapshot not found, returning current state",
147 })))
148}
149
150pub async fn store_request_context(
155 state: &XRayState,
156 request_id: String,
157 workspace_id: String,
158 unified_state: &mockforge_core::consistency::types::UnifiedState,
159) {
160 let state_snapshot = serde_json::to_value(unified_state).unwrap_or_default();
161 let snapshot = RequestContextSnapshot {
162 workspace_id: workspace_id.clone(),
163 state_snapshot,
164 timestamp: unified_state.last_updated.timestamp(),
165 };
166
167 let mut contexts = state.request_contexts.write().await;
168
169 let workspace_entries: Vec<_> = contexts
172 .iter()
173 .filter(|(_, s)| s.workspace_id == workspace_id)
174 .map(|(k, _)| k.clone())
175 .collect();
176
177 if workspace_entries.len() >= 1000 {
178 let mut timestamps: Vec<_> = workspace_entries
180 .iter()
181 .filter_map(|id| contexts.get(id).map(|s| (id.clone(), s.timestamp)))
182 .collect();
183 timestamps.sort_by_key(|(_, ts)| *ts);
184
185 for (id, _) in timestamps.iter().take(100) {
186 contexts.remove(id);
187 }
188 }
189
190 contexts.insert(request_id, snapshot);
191}
192
193pub async fn get_workspace_summary(
197 State(state): State<XRayState>,
198 Path(workspace_id): Path<String>,
199) -> Result<Json<Value>, StatusCode> {
200 let unified_state = state.engine.get_state(&workspace_id).await.ok_or_else(|| {
201 debug!("No state found for workspace: {}", workspace_id);
202 StatusCode::NOT_FOUND
203 })?;
204
205 let summary = serde_json::json!({
206 "workspace_id": unified_state.workspace_id,
207 "scenario": unified_state.active_scenario,
208 "persona_id": unified_state.active_persona.as_ref().map(|p| p.id.clone()),
209 "reality_level": unified_state.reality_level.value(),
210 "reality_ratio": unified_state.reality_continuum_ratio,
211 "active_chaos_rules_count": unified_state.active_chaos_rules.len(),
212 "entity_count": unified_state.entity_state.len(),
213 "protocol_count": unified_state.protocol_states.len(),
214 "last_updated": unified_state.last_updated,
215 });
216
217 Ok(Json(summary))
218}
219
220pub async fn list_entities(
224 State(state): State<XRayState>,
225 Query(params): Query<XRayQuery>,
226) -> Result<Json<Value>, StatusCode> {
227 let unified_state = state.engine.get_state(¶ms.workspace).await.ok_or_else(|| {
228 debug!("No state found for workspace: {}", params.workspace);
229 StatusCode::NOT_FOUND
230 })?;
231
232 let entities: Vec<&mockforge_core::consistency::EntityState> =
233 unified_state.entity_state.values().collect();
234
235 Ok(Json(serde_json::json!({
236 "workspace": params.workspace,
237 "entities": entities,
238 "count": entities.len(),
239 })))
240}
241
242pub async fn get_entity(
246 State(state): State<XRayState>,
247 Path((entity_type, entity_id)): Path<(String, String)>,
248 Query(params): Query<XRayQuery>,
249) -> Result<Json<Value>, StatusCode> {
250 let entity = state
251 .engine
252 .get_entity(¶ms.workspace, &entity_type, &entity_id)
253 .await
254 .ok_or_else(|| {
255 debug!(
256 "Entity not found: {}:{} in workspace: {}",
257 entity_type, entity_id, params.workspace
258 );
259 StatusCode::NOT_FOUND
260 })?;
261
262 Ok(Json(serde_json::to_value(&entity).unwrap()))
263}
264
265pub fn xray_router(state: XRayState) -> axum::Router {
267 use axum::routing::get;
268
269 axum::Router::new()
270 .route("/api/v1/xray/state/summary", get(get_state_summary))
271 .route("/api/v1/xray/state", get(get_state))
272 .route("/api/v1/xray/request-context/{request_id}", get(get_request_context))
273 .route("/api/v1/xray/workspace/{workspace_id}/summary", get(get_workspace_summary))
274 .route("/api/v1/xray/entities", get(list_entities))
275 .route("/api/v1/xray/entities/{entity_type}/{entity_id}", get(get_entity))
276 .with_state(state)
277}