Skip to main content

feagi_api/transports/http/
server.rs

1// Copyright 2025 Neuraville Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4// HTTP server implementation (Axum)
5//
6// This module sets up the HTTP API server with Axum, including routing,
7// middleware, and state management.
8
9use axum::{
10    body::Body,
11    extract::State,
12    http::{Method, Request, StatusCode},
13    middleware::{self, Next},
14    response::{Html, IntoResponse, Json, Redirect, Response},
15    routing::{get, put},
16    Router,
17};
18use http_body_util::BodyExt;
19use std::path::PathBuf;
20use std::sync::atomic::{AtomicBool, Ordering};
21use std::sync::Arc;
22use std::time::Duration;
23use tower_http::{
24    cors::{Any, CorsLayer},
25    trace::TraceLayer,
26};
27use utoipa::OpenApi;
28
29use crate::amalgamation;
30#[cfg(feature = "http")]
31use crate::openapi::ApiDoc;
32#[cfg(feature = "feagi-agent")]
33use feagi_config::load_config;
34#[cfg(feature = "feagi-agent")]
35use feagi_io::protocol_implementations::websocket::websocket_std::{
36    FeagiWebSocketServerPublisherProperties, FeagiWebSocketServerPullerProperties,
37};
38#[cfg(feature = "feagi-agent")]
39use feagi_io::protocol_implementations::zmq::{
40    FeagiZmqServerPublisherProperties, FeagiZmqServerPullerProperties,
41};
42#[cfg(feature = "services")]
43use feagi_services::traits::{AgentService, SystemService};
44#[cfg(feature = "services")]
45use feagi_services::{
46    AnalyticsService, ConnectomeService, GenomeService, NeuronService, RuntimeService,
47};
48
49/// Application state shared across all HTTP handlers
50#[derive(Clone)]
51pub struct ApiState {
52    /// Optional provider for GET /v1/network/connection_info (None in tests/WASM)
53    pub network_connection_info_provider:
54        Option<Arc<dyn crate::endpoints::network::NetworkConnectionInfoProvider>>,
55    pub agent_service: Option<Arc<dyn AgentService + Send + Sync>>,
56    pub analytics_service: Arc<dyn AnalyticsService + Send + Sync>,
57    pub connectome_service: Arc<dyn ConnectomeService + Send + Sync>,
58    pub genome_service: Arc<dyn GenomeService + Send + Sync>,
59    pub neuron_service: Arc<dyn NeuronService + Send + Sync>,
60    pub runtime_service: Arc<dyn RuntimeService + Send + Sync>,
61    pub system_service: Arc<dyn SystemService + Send + Sync>,
62    pub snapshot_service: Option<Arc<dyn feagi_services::SnapshotService + Send + Sync>>,
63    /// FEAGI session timestamp in milliseconds (Unix timestamp when FEAGI started)
64    /// This is a unique identifier for each FEAGI instance/session
65    pub feagi_session_timestamp: i64,
66    /// Base directory for default API filesystem writes when the client omits a path
67    /// (e.g. `POST /v1/genome/save` without `file_path`). Default genome files go under
68    /// `{filesystem_data_root}/cache/.genome/` (aligned with feagi-python-sdk `~/.feagi/cache`).
69    /// Set from `[system].data_dir` / `FEAGI_DATA_DIR` at startup; when unset, resolves to
70    /// `{user_home}/.feagi` (native) so writes do not use cwd or OS temp (see
71    /// [`ApiState::filesystem_data_root_from_config`]).
72    pub filesystem_data_root: PathBuf,
73    /// Memory area stats cache (updated by plasticity service, read by health check)
74    pub memory_stats_cache: Option<feagi_npu_plasticity::MemoryStatsCache>,
75    /// In-memory amalgamation state (pending request + history), surfaced via health_check.
76    pub amalgamation_state: amalgamation::SharedAmalgamationState,
77    /// Exclusive lock for genome transition operations (load/upload/reload).
78    pub genome_transition_lock: Arc<tokio::sync::Mutex<()>>,
79    /// Indicates whether a prioritized genome transition is currently in progress.
80    pub genome_transition_in_progress: Arc<AtomicBool>,
81    /// Agent handler for device registrations and transport management
82    #[cfg(feature = "feagi-agent")]
83    pub agent_handler: Option<Arc<std::sync::Mutex<feagi_agent::server::FeagiAgentHandler>>>,
84}
85
86impl ApiState {
87    /// Initialize the agent handler (deprecated - use external initialization).
88    #[cfg(feature = "feagi-agent")]
89    pub fn init_agent_registration_handler(
90    ) -> Arc<std::sync::Mutex<feagi_agent::server::FeagiAgentHandler>> {
91        let config = load_config(None, None).expect("Failed to load FEAGI configuration");
92        let liveness_config = feagi_agent::server::AgentLivenessConfig {
93            heartbeat_timeout: Duration::from_millis(config.zmq.client_heartbeat_timeout),
94            stale_check_interval: Duration::from_millis(config.zmq.polling_timeout),
95        };
96        let mut handler = feagi_agent::server::FeagiAgentHandler::new_with_liveness_config(
97            Box::new(feagi_agent::server::auth::DummyAuth {}),
98            liveness_config,
99        );
100        let available_transports: Vec<String> = config
101            .transports
102            .available
103            .iter()
104            .map(|transport| transport.to_lowercase())
105            .collect();
106
107        if available_transports
108            .iter()
109            .any(|transport| transport == "zmq")
110        {
111            let sensory_address =
112                format_tcp_endpoint(&config.zmq.bind_host, config.ports.zmq_sensory_port);
113            let sensory_adv_address =
114                format_tcp_endpoint(&config.zmq.advertised_host, config.ports.zmq_sensory_port);
115            let motor_address =
116                format_tcp_endpoint(&config.zmq.bind_host, config.ports.zmq_motor_port);
117            let motor_adv_address =
118                format_tcp_endpoint(&config.zmq.advertised_host, config.ports.zmq_motor_port);
119            let visualization_address =
120                format_tcp_endpoint(&config.zmq.bind_host, config.ports.zmq_visualization_port);
121            let visualization_adv_address = format_tcp_endpoint(
122                &config.zmq.advertised_host,
123                config.ports.zmq_visualization_port,
124            );
125
126            let sensory =
127                FeagiZmqServerPullerProperties::new(&sensory_address, &sensory_adv_address)
128                    .expect("Failed to create ZMQ sensory puller properties");
129            handler.add_puller_server(Box::new(sensory));
130
131            let motor = FeagiZmqServerPublisherProperties::new(&motor_address, &motor_adv_address)
132                .expect("Failed to create ZMQ motor publisher properties");
133            let visualization = FeagiZmqServerPublisherProperties::new(
134                &visualization_address,
135                &visualization_adv_address,
136            )
137            .expect("Failed to create ZMQ visualization publisher properties");
138            handler.add_publisher_server(Box::new(motor));
139            handler.add_publisher_server(Box::new(visualization));
140        }
141
142        if available_transports
143            .iter()
144            .any(|transport| transport == "websocket" || transport == "ws")
145        {
146            let sensory_address =
147                format_ws_address(&config.websocket.bind_host, config.websocket.sensory_port);
148            let sensory_adv_address = format_ws_address(
149                &config.websocket.advertised_host,
150                config.websocket.sensory_port,
151            );
152            let motor_address =
153                format_ws_address(&config.websocket.bind_host, config.websocket.motor_port);
154            let motor_adv_address = format_ws_address(
155                &config.websocket.advertised_host,
156                config.websocket.motor_port,
157            );
158            let visualization_address = format_ws_address(
159                &config.websocket.bind_host,
160                config.websocket.visualization_port,
161            );
162            let visualization_adv_address = format_ws_address(
163                &config.websocket.advertised_host,
164                config.websocket.visualization_port,
165            );
166
167            let sensory = FeagiWebSocketServerPullerProperties::new_with_remote(
168                &sensory_address,
169                &sensory_adv_address,
170            )
171            .expect("Failed to create WebSocket sensory puller properties");
172            handler.add_puller_server(Box::new(sensory));
173
174            let motor =
175                FeagiWebSocketServerPublisherProperties::new(&motor_address, &motor_adv_address)
176                    .expect("Failed to create WebSocket motor publisher properties");
177            let visualization = FeagiWebSocketServerPublisherProperties::new(
178                &visualization_address,
179                &visualization_adv_address,
180            )
181            .expect("Failed to create WebSocket visualization publisher properties");
182            handler.add_publisher_server(Box::new(motor));
183            handler.add_publisher_server(Box::new(visualization));
184        }
185
186        Arc::new(std::sync::Mutex::new(handler))
187    }
188
189    /// Initialize amalgamation_state field (empty state).
190    pub fn init_amalgamation_state() -> amalgamation::SharedAmalgamationState {
191        amalgamation::new_shared_state()
192    }
193
194    /// Initialize synchronization primitives for strict genome transitions.
195    pub fn init_genome_transition_controls() -> (Arc<tokio::sync::Mutex<()>>, Arc<AtomicBool>) {
196        (
197            Arc::new(tokio::sync::Mutex::new(())),
198            Arc::new(AtomicBool::new(false)),
199        )
200    }
201
202    /// Resolve the base directory for default on-disk API writes (e.g. genome save without `file_path`).
203    ///
204    /// - If `[system].data_dir` is set (including via **`FEAGI_DATA_DIR`**), returns that path as-is.
205    /// - Otherwise returns **`{user_home}/.feagi`**, matching feagi-python-sdk `FeagiPaths` and
206    ///   feagi-desktop runtime layout. If the home directory cannot be resolved, falls back to
207    ///   `std::env::temp_dir().join("feagi")`.
208    /// - **wasm32:** `/tmp/feagi` (no portable home in browser WASM).
209    ///
210    /// Callers append subpaths such as `cache/.genome` for default genome JSON files.
211    pub fn filesystem_data_root_from_config(data_dir: &std::path::Path) -> PathBuf {
212        if !data_dir.as_os_str().is_empty() {
213            data_dir.to_path_buf()
214        } else {
215            #[cfg(target_arch = "wasm32")]
216            {
217                PathBuf::from("/tmp/feagi")
218            }
219            #[cfg(not(target_arch = "wasm32"))]
220            {
221                dirs::home_dir()
222                    .map(|h| h.join(".feagi"))
223                    .unwrap_or_else(|| std::env::temp_dir().join("feagi"))
224            }
225        }
226    }
227}
228
229#[cfg(feature = "feagi-agent")]
230fn format_tcp_endpoint(host: &str, port: u16) -> String {
231    if host.contains(':') {
232        format!("tcp://[{host}]:{port}")
233    } else {
234        format!("tcp://{host}:{port}")
235    }
236}
237
238#[cfg(feature = "feagi-agent")]
239fn format_ws_address(host: &str, port: u16) -> String {
240    if host.contains(':') {
241        format!("[{host}]:{port}")
242    } else {
243        format!("{host}:{port}")
244    }
245}
246
247/// Create the main HTTP server application
248pub fn create_http_server(state: ApiState) -> Router {
249    let middleware_state = state.clone();
250    Router::new()
251        // Root redirect to custom Swagger UI
252        .route("/", get(root_redirect))
253
254        // Custom Swagger UI with FEAGI branding at /swagger-ui/
255        .route("/swagger-ui/", get(custom_swagger_ui))
256
257        // OpenAPI spec endpoint
258        .route("/api-docs/openapi.json", get(|| async {
259            Json(ApiDoc::openapi())
260        }))
261
262        // Python-compatible paths: /v1/* (ONLY this, matching Python exactly)
263        .nest("/v1", create_v1_router())
264
265        // Catch-all route for debugging unmatched requests
266        .fallback(|| async {
267            tracing::warn!(target: "feagi-api", "Unmatched request - 404 Not Found");
268            (StatusCode::NOT_FOUND, "404 Not Found")
269        })
270
271        // Add state
272        .with_state(state)
273
274        // Add middleware
275        .layer(middleware::from_fn_with_state(
276            middleware_state,
277            reject_during_genome_transition,
278        ))
279        .layer(middleware::from_fn(log_request_response_bodies))
280        .layer(create_cors_layer())
281        .layer(
282            TraceLayer::new_for_http()
283                .make_span_with(|request: &axum::http::Request<_>| {
284                    tracing::span!(
285                        target: "feagi-api",
286                        tracing::Level::TRACE,
287                        "request",
288                        method = %request.method(),
289                        uri = %request.uri(),
290                        version = ?request.version(),
291                    )
292                })
293                .on_request(|request: &axum::http::Request<_>, _span: &tracing::Span| {
294                    tracing::trace!(target: "feagi-api", "Incoming request: {} {}", request.method(), request.uri());
295                })
296                .on_response(|response: &axum::http::Response<_>, latency: std::time::Duration, span: &tracing::Span| {
297                    tracing::trace!(
298                        target: "feagi-api",
299                        "Response: status={}, latency={:?}",
300                        response.status(),
301                        latency
302                    );
303                    span.record("status", response.status().as_u16());
304                    span.record("latency_ms", latency.as_millis());
305                })
306                .on_body_chunk(|chunk: &axum::body::Bytes, latency: std::time::Duration, _span: &tracing::Span| {
307                    tracing::trace!(target: "feagi-api", "Response chunk: {} bytes, latency={:?}", chunk.len(), latency);
308                })
309                .on_eos(|_trailers: Option<&axum::http::HeaderMap>, stream_duration: std::time::Duration, _span: &tracing::Span| {
310                    tracing::trace!(target: "feagi-api", "Stream ended, duration={:?}", stream_duration);
311                })
312                .on_failure(|error: tower_http::classify::ServerErrorsFailureClass, latency: std::time::Duration, _span: &tracing::Span| {
313                    tracing::error!(
314                        target: "feagi-api", 
315                        "Request failed: error_class={:?}, latency={:?}", 
316                        error, latency
317                    );
318                })
319        )
320}
321
322/// Reject non-genome requests while a prioritized genome transition is running.
323async fn reject_during_genome_transition(
324    State(state): State<ApiState>,
325    request: Request<Body>,
326    next: Next,
327) -> Response {
328    let path = request.uri().path();
329    let method = request.method();
330    let transition_in_progress = state.genome_transition_in_progress.load(Ordering::SeqCst);
331
332    if transition_in_progress && !is_transition_allowed_route(path, method) {
333        let payload = crate::common::ApiError::conflict(
334            "Genome transition in progress: request rejected to preserve deterministic load semantics",
335        );
336        return (StatusCode::CONFLICT, Json(payload)).into_response();
337    }
338
339    next.run(request).await
340}
341
342fn is_transition_allowed_route(path: &str, method: &Method) -> bool {
343    if path.starts_with("/v1/genome/upload")
344        || path.starts_with("/v1/genome/load")
345        || path.starts_with("/v1/genome/amalgamation")
346    {
347        return true;
348    }
349
350    matches!(
351        (method, path),
352        (&Method::GET, "/v1/system/health_check") | (&Method::GET, "/v1/system/readiness_check")
353    )
354}
355
356/// Create V1 API router - Match Python structure EXACTLY
357/// Format: /v1/{module}/{snake_case_endpoint}
358fn create_v1_router() -> Router<ApiState> {
359    use crate::endpoints::agent;
360    use crate::endpoints::burst_engine;
361    use crate::endpoints::connectome;
362    use crate::endpoints::cortical_area;
363    use crate::endpoints::cortical_mapping;
364    use crate::endpoints::evolution;
365    use crate::endpoints::genome;
366    use crate::endpoints::input;
367    use crate::endpoints::insight;
368    use crate::endpoints::monitoring;
369    use crate::endpoints::morphology;
370    use crate::endpoints::network;
371    use crate::endpoints::neuroplasticity;
372    use crate::endpoints::outputs;
373    use crate::endpoints::physiology;
374    use crate::endpoints::region;
375    use crate::endpoints::simulation;
376    use crate::endpoints::system;
377    use crate::endpoints::training;
378    use crate::endpoints::visualization; //use crate::endpoints::{agent, system};
379
380    Router::new()
381        // ===== AGENT MODULE =====
382        .route(
383            "/agent/register",
384            axum::routing::post(agent::register_agent),
385        )
386        .route("/agent/heartbeat", axum::routing::post(agent::heartbeat))
387        .route("/agent/list", get(agent::list_agents))
388        .route("/agent/properties", get(agent::get_agent_properties))
389        .route("/agent/shared_mem", get(agent::get_shared_memory))
390        .route(
391            "/agent/deregister",
392            axum::routing::delete(agent::deregister_agent),
393        )
394        .route(
395            "/agent/manual_stimulation",
396            axum::routing::post(agent::manual_stimulation),
397        )
398        .route(
399            "/agent/fq_sampler_status",
400            get(agent::get_fq_sampler_status),
401        )
402        .route("/agent/capabilities", get(agent::get_capabilities))
403        .route(
404            "/agent/capabilities/all",
405            get(agent::get_all_agent_capabilities),
406        )
407        .route("/agent/info/{agent_id}", get(agent::get_agent_info))
408        .route(
409            "/agent/properties/{agent_id}",
410            get(agent::get_agent_properties_path),
411        )
412        .route(
413            "/agent/configure",
414            axum::routing::post(agent::post_configure),
415        )
416        .route(
417            "/agent/{agent_id}/device_registrations",
418            get(agent::export_device_registrations).post(agent::import_device_registrations),
419        )
420        // ===== SYSTEM MODULE (21 endpoints) =====
421        .route("/system/health_check", get(system::get_health_check))
422        .route(
423            "/system/cortical_area_visualization_skip_rate",
424            get(system::get_cortical_area_visualization_skip_rate)
425                .put(system::set_cortical_area_visualization_skip_rate),
426        )
427        .route(
428            "/system/cortical_area_visualization_suppression_threshold",
429            get(system::get_cortical_area_visualization_suppression_threshold)
430                .put(system::set_cortical_area_visualization_suppression_threshold),
431        )
432        .route("/system/version", get(system::get_version))
433        .route("/system/versions", get(system::get_versions))
434        .route("/system/configuration", get(system::get_configuration))
435        .route(
436            "/system/user_preferences",
437            get(system::get_user_preferences).put(system::put_user_preferences),
438        )
439        .route(
440            "/system/cortical_area_types",
441            get(system::get_cortical_area_types_list),
442        )
443        .route(
444            "/system/enable_visualization_fq_sampler",
445            axum::routing::post(system::post_enable_visualization_fq_sampler),
446        )
447        .route(
448            "/system/disable_visualization_fq_sampler",
449            axum::routing::post(system::post_disable_visualization_fq_sampler),
450        )
451        .route("/system/fcl_status", get(system::get_fcl_status_system))
452        .route(
453            "/system/fcl_reset",
454            axum::routing::post(system::post_fcl_reset_system),
455        )
456        .route("/system/processes", get(system::get_processes))
457        .route("/system/unique_logs", get(system::get_unique_logs))
458        .route("/system/log_tail", get(system::get_log_tail))
459        .route("/system/logs", axum::routing::post(system::post_logs))
460        .route(
461            "/system/beacon/subscribers",
462            get(system::get_beacon_subscribers),
463        )
464        .route(
465            "/system/beacon/subscribe",
466            axum::routing::post(system::post_beacon_subscribe),
467        )
468        .route(
469            "/system/beacon/unsubscribe",
470            axum::routing::delete(system::delete_beacon_unsubscribe),
471        )
472        .route(
473            "/system/global_activity_visualization",
474            get(system::get_global_activity_visualization)
475                .put(system::put_global_activity_visualization),
476        )
477        .route(
478            "/system/circuit_library_path",
479            axum::routing::post(system::post_circuit_library_path),
480        )
481        .route("/system/db/influxdb/test", get(system::get_influxdb_test))
482        .route(
483            "/system/register",
484            axum::routing::post(system::post_register_system),
485        )
486        // ===== CORTICAL_AREA MODULE (25 endpoints) =====
487        .route("/cortical_area/ipu", get(cortical_area::get_ipu))
488        .route(
489            "/cortical_area/ipu/types",
490            get(cortical_area::get_ipu_types),
491        )
492        .route("/cortical_area/opu", get(cortical_area::get_opu))
493        .route(
494            "/cortical_area/opu/types",
495            get(cortical_area::get_opu_types),
496        )
497        .route(
498            "/cortical_area/cortical_area_id_list",
499            get(cortical_area::get_cortical_area_id_list),
500        )
501        .route(
502            "/cortical_area/cortical_area_name_list",
503            get(cortical_area::get_cortical_area_name_list),
504        )
505        .route(
506            "/cortical_area/cortical_id_name_mapping",
507            get(cortical_area::get_cortical_id_name_mapping),
508        )
509        .route(
510            "/cortical_area/cortical_types",
511            get(cortical_area::get_cortical_types),
512        )
513        .route(
514            "/cortical_area/cortical_map_detailed",
515            get(cortical_area::get_cortical_map_detailed),
516        )
517        .route(
518            "/cortical_area/cortical_locations_2d",
519            get(cortical_area::get_cortical_locations_2d),
520        )
521        .route(
522            "/cortical_area/cortical_area/geometry",
523            get(cortical_area::get_cortical_area_geometry),
524        )
525        .route(
526            "/cortical_area/cortical_visibility",
527            get(cortical_area::get_cortical_visibility),
528        )
529        .route(
530            "/cortical_area/cortical_name_location",
531            axum::routing::post(cortical_area::post_cortical_name_location),
532        )
533        .route(
534            "/cortical_area/cortical_area_properties",
535            axum::routing::post(cortical_area::post_cortical_area_properties),
536        )
537        .route(
538            "/cortical_area/multi/cortical_area_properties",
539            axum::routing::post(cortical_area::post_multi_cortical_area_properties),
540        )
541        .route(
542            "/cortical_area/cortical_area",
543            axum::routing::post(cortical_area::post_cortical_area)
544                .put(cortical_area::put_cortical_area)
545                .delete(cortical_area::delete_cortical_area),
546        )
547        .route(
548            "/cortical_area/custom_cortical_area",
549            axum::routing::post(cortical_area::post_custom_cortical_area),
550        )
551        .route(
552            "/cortical_area/clone",
553            axum::routing::post(cortical_area::post_clone),
554        )
555        .route(
556            "/cortical_area/multi/cortical_area",
557            put(cortical_area::put_multi_cortical_area)
558                .delete(cortical_area::delete_multi_cortical_area),
559        )
560        .route("/cortical_area/coord_2d", put(cortical_area::put_coord_2d))
561        .route(
562            "/cortical_area/suppress_cortical_visibility",
563            put(cortical_area::put_suppress_cortical_visibility),
564        )
565        .route("/cortical_area/reset", put(cortical_area::put_reset))
566        .route(
567            "/cortical_area/visualization",
568            get(cortical_area::get_visualization),
569        )
570        .route(
571            "/cortical_area/batch_operations",
572            axum::routing::post(cortical_area::post_batch_operations),
573        )
574        .route("/cortical_area/ipu/list", get(cortical_area::get_ipu_list))
575        .route("/cortical_area/opu/list", get(cortical_area::get_opu_list))
576        .route(
577            "/cortical_area/coordinates_3d",
578            put(cortical_area::put_coordinates_3d),
579        )
580        .route(
581            "/cortical_area/bulk_delete",
582            axum::routing::delete(cortical_area::delete_bulk),
583        )
584        .route(
585            "/cortical_area/resize",
586            axum::routing::post(cortical_area::post_resize),
587        )
588        .route(
589            "/cortical_area/reposition",
590            axum::routing::post(cortical_area::post_reposition),
591        )
592        .route(
593            "/cortical_area/voxel_neurons",
594            get(cortical_area::get_voxel_neurons).post(cortical_area::post_voxel_neurons),
595        )
596        .route(
597            "/cortical_area/memory",
598            get(cortical_area::get_memory_cortical_area),
599        )
600        .route(
601            "/cortical_area/cortical_area_index_list",
602            get(cortical_area::get_cortical_area_index_list),
603        )
604        .route(
605            "/cortical_area/cortical_idx_mapping",
606            get(cortical_area::get_cortical_idx_mapping),
607        )
608        .route(
609            "/cortical_area/mapping_restrictions",
610            get(cortical_area::get_mapping_restrictions_query)
611                .post(cortical_area::post_mapping_restrictions),
612        )
613        .route(
614            "/cortical_area/:cortical_id/memory_usage",
615            get(cortical_area::get_memory_usage),
616        )
617        .route(
618            "/cortical_area/:cortical_id/neuron_count",
619            get(cortical_area::get_area_neuron_count),
620        )
621        .route(
622            "/cortical_area/cortical_type_options",
623            axum::routing::post(cortical_area::post_cortical_type_options),
624        )
625        .route(
626            "/cortical_area/mapping_restrictions_between_areas",
627            axum::routing::post(cortical_area::post_mapping_restrictions_between_areas),
628        )
629        .route("/cortical_area/coord_3d", put(cortical_area::put_coord_3d))
630        // ===== MORPHOLOGY MODULE (14 endpoints) =====
631        .route(
632            "/morphology/morphology_list",
633            get(morphology::get_morphology_list),
634        )
635        .route(
636            "/morphology/morphology_types",
637            get(morphology::get_morphology_types),
638        )
639        .route("/morphology/list/types", get(morphology::get_list_types))
640        .route(
641            "/morphology/morphologies",
642            get(morphology::get_morphologies),
643        )
644        .route(
645            "/morphology/morphology",
646            axum::routing::post(morphology::post_morphology)
647                .put(morphology::put_morphology)
648                .delete(morphology::delete_morphology_by_name),
649        )
650        .route(
651            "/morphology/rename",
652            axum::routing::put(morphology::put_rename_morphology),
653        )
654        .route(
655            "/morphology/morphology_properties",
656            axum::routing::post(morphology::post_morphology_properties),
657        )
658        .route(
659            "/morphology/morphology_usage",
660            axum::routing::post(morphology::post_morphology_usage),
661        )
662        .route("/morphology/list", get(morphology::get_list))
663        .route("/morphology/info/:morphology_id", get(morphology::get_info))
664        .route(
665            "/morphology/create",
666            axum::routing::post(morphology::post_create),
667        )
668        .route(
669            "/morphology/update",
670            axum::routing::put(morphology::put_update),
671        )
672        .route(
673            "/morphology/delete/:morphology_id",
674            axum::routing::delete(morphology::delete_morphology),
675        )
676        // ===== REGION MODULE (12 endpoints) =====
677        .route("/region/regions_members", get(region::get_regions_members))
678        .route(
679            "/region/region",
680            axum::routing::post(region::post_region)
681                .put(region::put_region)
682                .delete(region::delete_region),
683        )
684        .route("/region/clone", axum::routing::post(region::post_clone))
685        .route(
686            "/region/relocate_members",
687            put(region::put_relocate_members),
688        )
689        .route(
690            "/region/region_and_members",
691            axum::routing::delete(region::delete_region_and_members),
692        )
693        .route("/region/regions", get(region::get_regions))
694        .route("/region/region_titles", get(region::get_region_titles))
695        .route("/region/region/:region_id", get(region::get_region_detail))
696        .route(
697            "/region/change_region_parent",
698            put(region::put_change_region_parent),
699        )
700        .route(
701            "/region/change_cortical_area_region",
702            put(region::put_change_cortical_area_region),
703        )
704        // ===== CORTICAL_MAPPING MODULE (8 endpoints) =====
705        .route(
706            "/cortical_mapping/afferents",
707            axum::routing::post(cortical_mapping::post_afferents),
708        )
709        .route(
710            "/cortical_mapping/efferents",
711            axum::routing::post(cortical_mapping::post_efferents),
712        )
713        .route(
714            "/cortical_mapping/mapping_properties",
715            axum::routing::post(cortical_mapping::post_mapping_properties)
716                .put(cortical_mapping::put_mapping_properties),
717        )
718        .route(
719            "/cortical_mapping/mapping",
720            get(cortical_mapping::get_mapping).delete(cortical_mapping::delete_mapping),
721        )
722        .route(
723            "/cortical_mapping/mapping_list",
724            get(cortical_mapping::get_mapping_list),
725        )
726        .route(
727            "/cortical_mapping/batch_update",
728            axum::routing::post(cortical_mapping::post_batch_update),
729        )
730        .route(
731            "/cortical_mapping/mapping",
732            axum::routing::post(cortical_mapping::post_mapping).put(cortical_mapping::put_mapping),
733        )
734        // ===== CONNECTOME MODULE (21 endpoints) =====
735        .route(
736            "/connectome/cortical_areas/list/detailed",
737            get(connectome::get_cortical_areas_list_detailed),
738        )
739        .route(
740            "/connectome/properties/dimensions",
741            get(connectome::get_properties_dimensions),
742        )
743        .route(
744            "/connectome/properties/mappings",
745            get(connectome::get_properties_mappings),
746        )
747        .route("/connectome/snapshot", get(connectome::get_snapshot))
748        .route("/connectome/stats", get(connectome::get_stats))
749        .route(
750            "/connectome/batch_neuron_operations",
751            axum::routing::post(connectome::post_batch_neuron_operations),
752        )
753        .route(
754            "/connectome/batch_synapse_operations",
755            axum::routing::post(connectome::post_batch_synapse_operations),
756        )
757        .route(
758            "/connectome/neuron_count",
759            get(connectome::get_neuron_count),
760        )
761        .route(
762            "/connectome/synapse_count",
763            get(connectome::get_synapse_count),
764        )
765        .route("/connectome/paths", get(connectome::get_paths))
766        .route(
767            "/connectome/cumulative_stats",
768            get(connectome::get_cumulative_stats),
769        )
770        .route(
771            "/connectome/area_details",
772            get(connectome::get_area_details),
773        )
774        .route(
775            "/connectome/rebuild",
776            axum::routing::post(connectome::post_rebuild),
777        )
778        .route("/connectome/structure", get(connectome::get_structure))
779        .route(
780            "/connectome/clear",
781            axum::routing::post(connectome::post_clear),
782        )
783        .route("/connectome/validation", get(connectome::get_validation))
784        .route("/connectome/topology", get(connectome::get_topology))
785        .route(
786            "/connectome/optimize",
787            axum::routing::post(connectome::post_optimize),
788        )
789        .route(
790            "/connectome/connectivity_matrix",
791            get(connectome::get_connectivity_matrix),
792        )
793        .route(
794            "/connectome/neurons/batch",
795            axum::routing::post(connectome::post_neurons_batch),
796        )
797        .route(
798            "/connectome/synapses/batch",
799            axum::routing::post(connectome::post_synapses_batch),
800        )
801        .route(
802            "/connectome/cortical_areas/list/summary",
803            get(connectome::get_cortical_areas_list_summary),
804        )
805        .route(
806            "/connectome/cortical_areas/list/transforming",
807            get(connectome::get_cortical_areas_list_transforming),
808        )
809        .route(
810            "/connectome/cortical_area/list/types",
811            get(connectome::get_cortical_area_list_types),
812        )
813        .route(
814            "/connectome/cortical_area/:cortical_id/neurons",
815            get(connectome::get_cortical_area_neurons),
816        )
817        .route(
818            "/connectome/:cortical_area_id/synapses/incoming",
819            get(connectome::get_area_synapses_incoming),
820        )
821        .route(
822            "/connectome/:cortical_area_id/synapses",
823            get(connectome::get_area_synapses),
824        )
825        .route(
826            "/connectome/cortical_info/:cortical_area",
827            get(connectome::get_cortical_info),
828        )
829        .route(
830            "/connectome/stats/cortical/cumulative/:cortical_area",
831            get(connectome::get_stats_cortical_cumulative),
832        )
833        .route(
834            "/connectome/neuron/:neuron_id/properties",
835            get(connectome::get_neuron_properties_by_id),
836        )
837        .route(
838            "/connectome/neuron_properties",
839            get(connectome::get_neuron_properties_query),
840        )
841        .route(
842            "/connectome/neuron_properties_at",
843            get(connectome::get_neuron_properties_at_query),
844        )
845        .route(
846            "/connectome/memory_neuron",
847            get(connectome::get_memory_neuron),
848        )
849        .route(
850            "/connectome/area_neurons",
851            get(connectome::get_area_neurons_query),
852        )
853        .route(
854            "/connectome/fire_queue/:cortical_area",
855            get(connectome::get_fire_queue_area),
856        )
857        .route(
858            "/connectome/plasticity",
859            get(connectome::get_plasticity_info),
860        )
861        .route("/connectome/path", get(connectome::get_path_query))
862        .route(
863            "/connectome/download",
864            get(connectome::get_download_connectome),
865        )
866        .route(
867            "/connectome/download-cortical-area/:cortical_area",
868            get(connectome::get_download_cortical_area),
869        )
870        .route(
871            "/connectome/upload",
872            axum::routing::post(connectome::post_upload_connectome),
873        )
874        .route(
875            "/connectome/upload-cortical-area",
876            axum::routing::post(connectome::post_upload_cortical_area),
877        )
878        // ===== BURST_ENGINE MODULE (14 endpoints) =====
879        .route(
880            "/burst_engine/simulation_timestep",
881            get(burst_engine::get_simulation_timestep).post(burst_engine::post_simulation_timestep),
882        )
883        .route("/burst_engine/fcl", get(burst_engine::get_fcl))
884        .route(
885            "/burst_engine/fcl/neuron",
886            get(burst_engine::get_fcl_neuron),
887        )
888        .route(
889            "/burst_engine/fire_queue",
890            get(burst_engine::get_fire_queue),
891        )
892        .route(
893            "/burst_engine/fire_queue/neuron",
894            get(burst_engine::get_fire_queue_neuron),
895        )
896        .route(
897            "/burst_engine/fcl_reset",
898            axum::routing::post(burst_engine::post_fcl_reset),
899        )
900        .route(
901            "/burst_engine/fcl_status",
902            get(burst_engine::get_fcl_status),
903        )
904        .route(
905            "/burst_engine/fcl_sampler/config",
906            get(burst_engine::get_fcl_sampler_config).post(burst_engine::post_fcl_sampler_config),
907        )
908        .route(
909            "/burst_engine/fcl_sampler/area/:area_id/sample_rate",
910            get(burst_engine::get_area_fcl_sample_rate)
911                .post(burst_engine::post_area_fcl_sample_rate),
912        )
913        .route(
914            "/burst_engine/fire_ledger/default_window_size",
915            get(burst_engine::get_fire_ledger_default_window_size)
916                .put(burst_engine::put_fire_ledger_default_window_size),
917        )
918        .route(
919            "/burst_engine/fire_ledger/areas_window_config",
920            get(burst_engine::get_fire_ledger_areas_window_config),
921        )
922        .route("/burst_engine/stats", get(burst_engine::get_stats))
923        .route("/burst_engine/status", get(burst_engine::get_status))
924        .route(
925            "/burst_engine/control",
926            axum::routing::post(burst_engine::post_control),
927        )
928        .route(
929            "/burst_engine/burst_counter",
930            get(burst_engine::get_burst_counter),
931        )
932        .route(
933            "/burst_engine/start",
934            axum::routing::post(burst_engine::post_start),
935        )
936        .route(
937            "/burst_engine/stop",
938            axum::routing::post(burst_engine::post_stop),
939        )
940        .route(
941            "/burst_engine/hold",
942            axum::routing::post(burst_engine::post_hold),
943        )
944        .route(
945            "/burst_engine/resume",
946            axum::routing::post(burst_engine::post_resume),
947        )
948        .route(
949            "/burst_engine/config",
950            get(burst_engine::get_config).put(burst_engine::put_config),
951        )
952        .route(
953            "/burst_engine/fire_ledger/area/:area_id/window_size",
954            get(burst_engine::get_fire_ledger_area_window_size)
955                .put(burst_engine::put_fire_ledger_area_window_size),
956        )
957        .route(
958            "/burst_engine/fire_ledger/area/:area_id/history",
959            get(burst_engine::get_fire_ledger_history),
960        )
961        .route(
962            "/burst_engine/membrane_potentials",
963            get(burst_engine::get_membrane_potentials).put(burst_engine::put_membrane_potentials),
964        )
965        .route(
966            "/burst_engine/frequency_status",
967            get(burst_engine::get_frequency_status),
968        )
969        .route(
970            "/burst_engine/measure_frequency",
971            axum::routing::post(burst_engine::post_measure_frequency),
972        )
973        .route(
974            "/burst_engine/frequency_history",
975            get(burst_engine::get_frequency_history),
976        )
977        .route(
978            "/burst_engine/force_connectome_integration",
979            axum::routing::post(burst_engine::post_force_connectome_integration),
980        )
981        // ===== GENOME MODULE (22 endpoints) =====
982        .route("/genome/file_name", get(genome::get_file_name))
983        .route("/genome/circuits", get(genome::get_circuits))
984        .route(
985            "/genome/amalgamation_destination",
986            axum::routing::post(genome::post_amalgamation_destination),
987        )
988        .route(
989            "/genome/amalgamation_cancellation",
990            axum::routing::delete(genome::delete_amalgamation_cancellation),
991        )
992        .route(
993            "/feagi/genome/append",
994            axum::routing::post(genome::post_genome_append),
995        )
996        .route(
997            "/genome/upload/barebones",
998            axum::routing::post(genome::post_upload_barebones_genome),
999        )
1000        .route(
1001            "/genome/upload/essential",
1002            axum::routing::post(genome::post_upload_essential_genome),
1003        )
1004        .route("/genome/name", get(genome::get_name))
1005        .route("/genome/timestamp", get(genome::get_timestamp))
1006        .route("/genome/save", axum::routing::post(genome::post_save))
1007        .route("/genome/load", axum::routing::post(genome::post_load))
1008        .route("/genome/upload", axum::routing::post(genome::post_upload))
1009        .route("/genome/download", get(genome::get_download))
1010        .route("/genome/properties", get(genome::get_properties))
1011        .route(
1012            "/genome/validate",
1013            axum::routing::post(genome::post_validate),
1014        )
1015        .route(
1016            "/genome/transform",
1017            axum::routing::post(genome::post_transform),
1018        )
1019        .route("/genome/clone", axum::routing::post(genome::post_clone))
1020        .route("/genome/reset", axum::routing::post(genome::post_reset))
1021        .route("/genome/metadata", get(genome::get_metadata))
1022        .route("/genome/merge", axum::routing::post(genome::post_merge))
1023        .route("/genome/diff", get(genome::get_diff))
1024        .route(
1025            "/genome/export_format",
1026            axum::routing::post(genome::post_export_format),
1027        )
1028        .route("/genome/amalgamation", get(genome::get_amalgamation))
1029        .route(
1030            "/genome/amalgamation_history",
1031            get(genome::get_amalgamation_history_exact),
1032        )
1033        .route(
1034            "/genome/cortical_template",
1035            get(genome::get_cortical_template),
1036        )
1037        .route("/genome/defaults/files", get(genome::get_defaults_files))
1038        .route("/genome/download_region", get(genome::get_download_region))
1039        .route("/genome/genome_number", get(genome::get_genome_number))
1040        .route(
1041            "/genome/amalgamation_by_filename",
1042            axum::routing::post(genome::post_amalgamation_by_filename),
1043        )
1044        .route(
1045            "/genome/amalgamation_by_payload",
1046            axum::routing::post(genome::post_amalgamation_by_payload),
1047        )
1048        .route(
1049            "/genome/amalgamation_by_upload",
1050            axum::routing::post(genome::post_amalgamation_by_upload),
1051        )
1052        .route(
1053            "/genome/append-file",
1054            axum::routing::post(genome::post_append_file),
1055        )
1056        .route(
1057            "/genome/upload/file",
1058            axum::routing::post(genome::post_upload_file),
1059        )
1060        .route(
1061            "/genome/upload/file/edit",
1062            axum::routing::post(genome::post_upload_file_edit),
1063        )
1064        .route(
1065            "/genome/upload/string",
1066            axum::routing::post(genome::post_upload_string),
1067        )
1068        // ===== NEUROPLASTICITY MODULE (7 endpoints) =====
1069        .route(
1070            "/neuroplasticity/plasticity_queue_depth",
1071            get(neuroplasticity::get_plasticity_queue_depth)
1072                .put(neuroplasticity::put_plasticity_queue_depth),
1073        )
1074        .route("/neuroplasticity/status", get(neuroplasticity::get_status))
1075        .route(
1076            "/neuroplasticity/transforming",
1077            get(neuroplasticity::get_transforming),
1078        )
1079        .route(
1080            "/neuroplasticity/configure",
1081            axum::routing::post(neuroplasticity::post_configure),
1082        )
1083        .route(
1084            "/neuroplasticity/enable/:area_id",
1085            axum::routing::post(neuroplasticity::post_enable_area),
1086        )
1087        .route(
1088            "/neuroplasticity/disable/:area_id",
1089            axum::routing::post(neuroplasticity::post_disable_area),
1090        )
1091        // ===== INSIGHT MODULE (6 endpoints) =====
1092        .route(
1093            "/insight/neurons/membrane_potential_status",
1094            axum::routing::post(insight::post_neurons_membrane_potential_status),
1095        )
1096        .route(
1097            "/insight/neuron/synaptic_potential_status",
1098            axum::routing::post(insight::post_neuron_synaptic_potential_status),
1099        )
1100        .route(
1101            "/insight/neurons/membrane_potential_set",
1102            axum::routing::post(insight::post_neurons_membrane_potential_set),
1103        )
1104        .route(
1105            "/insight/neuron/synaptic_potential_set",
1106            axum::routing::post(insight::post_neuron_synaptic_potential_set),
1107        )
1108        .route("/insight/analytics", get(insight::get_analytics))
1109        .route("/insight/data", get(insight::get_data))
1110        // ===== INPUT MODULE (4 endpoints) =====
1111        .route(
1112            "/input/vision",
1113            get(input::get_vision).post(input::post_vision),
1114        )
1115        .route("/input/sources", get(input::get_sources))
1116        .route(
1117            "/input/configure",
1118            axum::routing::post(input::post_configure),
1119        )
1120        .route(
1121            "/input/sensor_snapshot/last",
1122            get(input::get_sensor_snapshot_last),
1123        )
1124        // ===== OUTPUTS MODULE (2 endpoints) - Python uses /v1/output (singular)
1125        .route("/output/targets", get(outputs::get_targets))
1126        .route(
1127            "/output/motor_snapshot/last",
1128            get(outputs::get_motor_snapshot_last),
1129        )
1130        .route(
1131            "/output/configure",
1132            axum::routing::post(outputs::post_configure),
1133        )
1134        // ===== PHYSIOLOGY MODULE (2 endpoints) =====
1135        .route(
1136            "/physiology/",
1137            get(physiology::get_physiology).put(physiology::put_physiology),
1138        )
1139        // ===== SIMULATION MODULE (6 endpoints) =====
1140        .route(
1141            "/simulation/upload/string",
1142            axum::routing::post(simulation::post_stimulation_upload),
1143        )
1144        .route(
1145            "/simulation/reset",
1146            axum::routing::post(simulation::post_reset),
1147        )
1148        .route("/simulation/status", get(simulation::get_status))
1149        .route("/simulation/stats", get(simulation::get_stats))
1150        .route(
1151            "/simulation/config",
1152            axum::routing::post(simulation::post_config),
1153        )
1154        // ===== TRAINING MODULE (25 endpoints) =====
1155        .route("/training/shock", axum::routing::post(training::post_shock))
1156        .route("/training/shock/options", get(training::get_shock_options))
1157        .route("/training/shock/status", get(training::get_shock_status))
1158        .route(
1159            "/training/shock/activate",
1160            axum::routing::post(training::post_shock_activate),
1161        )
1162        .route(
1163            "/training/reward/intensity",
1164            axum::routing::post(training::post_reward_intensity),
1165        )
1166        .route(
1167            "/training/reward",
1168            axum::routing::post(training::post_reward),
1169        )
1170        .route(
1171            "/training/punishment/intensity",
1172            axum::routing::post(training::post_punishment_intensity),
1173        )
1174        .route(
1175            "/training/punishment",
1176            axum::routing::post(training::post_punishment),
1177        )
1178        .route(
1179            "/training/gameover",
1180            axum::routing::post(training::post_gameover),
1181        )
1182        .route("/training/brain_fitness", get(training::get_brain_fitness))
1183        .route(
1184            "/training/fitness_criteria",
1185            get(training::get_fitness_criteria)
1186                .put(training::put_fitness_criteria)
1187                .post(training::post_fitness_criteria),
1188        )
1189        .route(
1190            "/training/fitness_stats",
1191            get(training::get_fitness_stats)
1192                .put(training::put_fitness_stats)
1193                .delete(training::delete_fitness_stats),
1194        )
1195        .route(
1196            "/training/reset_fitness_stats",
1197            axum::routing::delete(training::delete_reset_fitness_stats),
1198        )
1199        .route(
1200            "/training/training_report",
1201            get(training::get_training_report),
1202        )
1203        .route("/training/status", get(training::get_status))
1204        .route("/training/stats", get(training::get_stats))
1205        .route(
1206            "/training/config",
1207            axum::routing::post(training::post_config),
1208        )
1209        // ===== VISUALIZATION MODULE (4 endpoints) =====
1210        .route(
1211            "/visualization/register_client",
1212            axum::routing::post(visualization::post_register_client),
1213        )
1214        .route(
1215            "/visualization/unregister_client",
1216            axum::routing::post(visualization::post_unregister_client),
1217        )
1218        .route(
1219            "/visualization/heartbeat",
1220            axum::routing::post(visualization::post_heartbeat),
1221        )
1222        .route("/visualization/status", get(visualization::get_status))
1223        // ===== MONITORING MODULE (5 endpoints) =====
1224        .route("/monitoring/status", get(monitoring::get_status))
1225        .route("/monitoring/metrics", get(monitoring::get_metrics))
1226        .route("/monitoring/data", get(monitoring::get_data))
1227        .route("/monitoring/performance", get(monitoring::get_performance))
1228        .route(
1229            "/monitoring/cortical_activity",
1230            get(monitoring::get_cortical_activity),
1231        )
1232        // ===== EVOLUTION MODULE (3 endpoints) =====
1233        .route("/evolution/status", get(evolution::get_status))
1234        .route(
1235            "/evolution/config",
1236            axum::routing::post(evolution::post_config),
1237        )
1238        // ===== SNAPSHOT MODULE (12 endpoints) =====
1239        // TODO: Implement snapshot endpoints
1240        // .route("/snapshot/create", axum::routing::post(snapshot::post_create))
1241        // .route("/snapshot/restore", axum::routing::post(snapshot::post_restore))
1242        // .route("/snapshot/", get(snapshot::get_list))
1243        // .route("/snapshot/:snapshot_id", axum::routing::delete(snapshot::delete_snapshot))
1244        // .route("/snapshot/:snapshot_id/artifact/:fmt", get(snapshot::get_artifact))
1245        // .route("/snapshot/compare", axum::routing::post(snapshot::post_compare))
1246        // .route("/snapshot/upload", axum::routing::post(snapshot::post_upload))
1247        // // Python uses /v1/snapshots/* (note the S)
1248        // .route("/snapshots/connectome", axum::routing::post(snapshot::post_snapshots_connectome))
1249        // .route("/snapshots/connectome/:snapshot_id/restore", axum::routing::post(snapshot::post_snapshots_connectome_restore))
1250        // .route("/snapshots/:snapshot_id/restore", axum::routing::post(snapshot::post_snapshots_restore))
1251        // .route("/snapshots/:snapshot_id", axum::routing::delete(snapshot::delete_snapshots_by_id))
1252        // .route("/snapshots/:snapshot_id/artifact/:fmt", get(snapshot::get_snapshots_artifact))
1253        // ===== NETWORK MODULE (3 endpoints) =====
1254        .route("/network/status", get(network::get_status))
1255        .route("/network/config", axum::routing::post(network::post_config))
1256        .route(
1257            "/network/connection_info",
1258            get(network::get_connection_info),
1259        )
1260}
1261
1262/// OpenAPI spec handler
1263#[allow(dead_code)] // In development - will be wired to OpenAPI route
1264async fn openapi_spec() -> Json<utoipa::openapi::OpenApi> {
1265    Json(ApiDoc::openapi())
1266}
1267
1268// ============================================================================
1269// CORS CONFIGURATION
1270// ============================================================================
1271
1272/// Create CORS layer for the API
1273///
1274/// TODO: Configure for production:
1275/// - Restrict allowed origins
1276/// - Allowed methods restricted
1277/// - Credentials support as needed
1278fn create_cors_layer() -> CorsLayer {
1279    CorsLayer::new()
1280        .allow_origin(Any)
1281        .allow_methods(Any)
1282        .allow_headers(Any)
1283}
1284
1285/// Middleware to log request and response bodies for debugging
1286async fn log_request_response_bodies(
1287    request: Request<Body>,
1288    next: Next,
1289) -> Result<Response, StatusCode> {
1290    let (parts, body) = request.into_parts();
1291
1292    // Only log bodies for POST/PUT/PATCH/DELETE requests
1293    let should_log_request = matches!(parts.method.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1294
1295    let body_bytes = if should_log_request {
1296        // Collect body bytes
1297        match body.collect().await {
1298            Ok(collected) => {
1299                let bytes = collected.to_bytes();
1300                // Log request body if it's JSON
1301                if let Ok(body_str) = String::from_utf8(bytes.to_vec()) {
1302                    if !body_str.is_empty() {
1303                        tracing::trace!(target: "feagi-api", "Request body: {}", body_str);
1304                    }
1305                }
1306                bytes
1307            }
1308            Err(_) => {
1309                return Err(StatusCode::INTERNAL_SERVER_ERROR);
1310            }
1311        }
1312    } else {
1313        axum::body::Bytes::new()
1314    };
1315
1316    // Reconstruct request with original body
1317    let request = Request::from_parts(parts, Body::from(body_bytes));
1318
1319    // Call the next handler
1320    let response = next.run(request).await;
1321
1322    // Log response body
1323    let (parts, body) = response.into_parts();
1324
1325    match body.collect().await {
1326        Ok(collected) => {
1327            let bytes = collected.to_bytes();
1328            // Log response body if it's JSON and not too large
1329            if bytes.len() < 10000 {
1330                // Only log responses < 10KB
1331                if let Ok(body_str) = String::from_utf8(bytes.to_vec()) {
1332                    if !body_str.is_empty() && body_str.starts_with('{') {
1333                        tracing::trace!(target: "feagi-api", "Response body: {}", body_str);
1334                    }
1335                }
1336            }
1337            // Reconstruct response
1338            Ok(Response::from_parts(parts, Body::from(bytes)))
1339        }
1340        Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
1341    }
1342}
1343
1344// ============================================================================
1345// HELPER HANDLERS
1346// ============================================================================
1347
1348/// Root redirect handler - redirects to Swagger UI
1349async fn root_redirect() -> Redirect {
1350    Redirect::permanent("/swagger-ui/")
1351}
1352
1353// Custom Swagger UI with FEAGI branding and dark/light themes
1354// Embedded from templates/custom-swagger-ui.html at compile time
1355async fn custom_swagger_ui() -> Html<&'static str> {
1356    const CUSTOM_SWAGGER_HTML: &str = include_str!("../../../templates/custom-swagger-ui.html");
1357    Html(CUSTOM_SWAGGER_HTML)
1358}
1359
1360// ============================================================================
1361// PLACEHOLDER HANDLERS (for endpoints not yet implemented)
1362// ============================================================================
1363
1364/// Placeholder handler for unimplemented endpoints
1365/// Returns 501 Not Implemented with a clear message
1366#[allow(dead_code)] // In development - will be used for placeholder routes
1367async fn placeholder_handler(State(_state): State<ApiState>) -> Response {
1368    (
1369        StatusCode::NOT_IMPLEMENTED,
1370        Json(serde_json::json!({
1371            "error": "Not yet implemented",
1372            "message": "This endpoint is registered but not yet implemented in Rust. See Python implementation."
1373        }))
1374    ).into_response()
1375}
1376
1377/// Placeholder health check - returns basic response
1378#[allow(dead_code)] // In development - will be used for basic health route
1379async fn placeholder_health_check(State(_state): State<ApiState>) -> Response {
1380    (
1381        StatusCode::OK,
1382        Json(serde_json::json!({
1383            "status": "ok",
1384            "message": "Health check placeholder - Python-compatible path structure confirmed",
1385            "burst_engine": false,
1386            "brain_readiness": false
1387        })),
1388    )
1389        .into_response()
1390}