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/logs", axum::routing::post(system::post_logs))
459        .route(
460            "/system/beacon/subscribers",
461            get(system::get_beacon_subscribers),
462        )
463        .route(
464            "/system/beacon/subscribe",
465            axum::routing::post(system::post_beacon_subscribe),
466        )
467        .route(
468            "/system/beacon/unsubscribe",
469            axum::routing::delete(system::delete_beacon_unsubscribe),
470        )
471        .route(
472            "/system/global_activity_visualization",
473            get(system::get_global_activity_visualization)
474                .put(system::put_global_activity_visualization),
475        )
476        .route(
477            "/system/circuit_library_path",
478            axum::routing::post(system::post_circuit_library_path),
479        )
480        .route("/system/db/influxdb/test", get(system::get_influxdb_test))
481        .route(
482            "/system/register",
483            axum::routing::post(system::post_register_system),
484        )
485        // ===== CORTICAL_AREA MODULE (25 endpoints) =====
486        .route("/cortical_area/ipu", get(cortical_area::get_ipu))
487        .route(
488            "/cortical_area/ipu/types",
489            get(cortical_area::get_ipu_types),
490        )
491        .route("/cortical_area/opu", get(cortical_area::get_opu))
492        .route(
493            "/cortical_area/opu/types",
494            get(cortical_area::get_opu_types),
495        )
496        .route(
497            "/cortical_area/cortical_area_id_list",
498            get(cortical_area::get_cortical_area_id_list),
499        )
500        .route(
501            "/cortical_area/cortical_area_name_list",
502            get(cortical_area::get_cortical_area_name_list),
503        )
504        .route(
505            "/cortical_area/cortical_id_name_mapping",
506            get(cortical_area::get_cortical_id_name_mapping),
507        )
508        .route(
509            "/cortical_area/cortical_types",
510            get(cortical_area::get_cortical_types),
511        )
512        .route(
513            "/cortical_area/cortical_map_detailed",
514            get(cortical_area::get_cortical_map_detailed),
515        )
516        .route(
517            "/cortical_area/cortical_locations_2d",
518            get(cortical_area::get_cortical_locations_2d),
519        )
520        .route(
521            "/cortical_area/cortical_area/geometry",
522            get(cortical_area::get_cortical_area_geometry),
523        )
524        .route(
525            "/cortical_area/cortical_visibility",
526            get(cortical_area::get_cortical_visibility),
527        )
528        .route(
529            "/cortical_area/cortical_name_location",
530            axum::routing::post(cortical_area::post_cortical_name_location),
531        )
532        .route(
533            "/cortical_area/cortical_area_properties",
534            axum::routing::post(cortical_area::post_cortical_area_properties),
535        )
536        .route(
537            "/cortical_area/multi/cortical_area_properties",
538            axum::routing::post(cortical_area::post_multi_cortical_area_properties),
539        )
540        .route(
541            "/cortical_area/cortical_area",
542            axum::routing::post(cortical_area::post_cortical_area)
543                .put(cortical_area::put_cortical_area)
544                .delete(cortical_area::delete_cortical_area),
545        )
546        .route(
547            "/cortical_area/custom_cortical_area",
548            axum::routing::post(cortical_area::post_custom_cortical_area),
549        )
550        .route(
551            "/cortical_area/clone",
552            axum::routing::post(cortical_area::post_clone),
553        )
554        .route(
555            "/cortical_area/multi/cortical_area",
556            put(cortical_area::put_multi_cortical_area)
557                .delete(cortical_area::delete_multi_cortical_area),
558        )
559        .route("/cortical_area/coord_2d", put(cortical_area::put_coord_2d))
560        .route(
561            "/cortical_area/suppress_cortical_visibility",
562            put(cortical_area::put_suppress_cortical_visibility),
563        )
564        .route("/cortical_area/reset", put(cortical_area::put_reset))
565        .route(
566            "/cortical_area/visualization",
567            get(cortical_area::get_visualization),
568        )
569        .route(
570            "/cortical_area/batch_operations",
571            axum::routing::post(cortical_area::post_batch_operations),
572        )
573        .route("/cortical_area/ipu/list", get(cortical_area::get_ipu_list))
574        .route("/cortical_area/opu/list", get(cortical_area::get_opu_list))
575        .route(
576            "/cortical_area/coordinates_3d",
577            put(cortical_area::put_coordinates_3d),
578        )
579        .route(
580            "/cortical_area/bulk_delete",
581            axum::routing::delete(cortical_area::delete_bulk),
582        )
583        .route(
584            "/cortical_area/resize",
585            axum::routing::post(cortical_area::post_resize),
586        )
587        .route(
588            "/cortical_area/reposition",
589            axum::routing::post(cortical_area::post_reposition),
590        )
591        .route(
592            "/cortical_area/voxel_neurons",
593            get(cortical_area::get_voxel_neurons).post(cortical_area::post_voxel_neurons),
594        )
595        .route(
596            "/cortical_area/memory",
597            get(cortical_area::get_memory_cortical_area),
598        )
599        .route(
600            "/cortical_area/cortical_area_index_list",
601            get(cortical_area::get_cortical_area_index_list),
602        )
603        .route(
604            "/cortical_area/cortical_idx_mapping",
605            get(cortical_area::get_cortical_idx_mapping),
606        )
607        .route(
608            "/cortical_area/mapping_restrictions",
609            get(cortical_area::get_mapping_restrictions_query)
610                .post(cortical_area::post_mapping_restrictions),
611        )
612        .route(
613            "/cortical_area/:cortical_id/memory_usage",
614            get(cortical_area::get_memory_usage),
615        )
616        .route(
617            "/cortical_area/:cortical_id/neuron_count",
618            get(cortical_area::get_area_neuron_count),
619        )
620        .route(
621            "/cortical_area/cortical_type_options",
622            axum::routing::post(cortical_area::post_cortical_type_options),
623        )
624        .route(
625            "/cortical_area/mapping_restrictions_between_areas",
626            axum::routing::post(cortical_area::post_mapping_restrictions_between_areas),
627        )
628        .route("/cortical_area/coord_3d", put(cortical_area::put_coord_3d))
629        // ===== MORPHOLOGY MODULE (14 endpoints) =====
630        .route(
631            "/morphology/morphology_list",
632            get(morphology::get_morphology_list),
633        )
634        .route(
635            "/morphology/morphology_types",
636            get(morphology::get_morphology_types),
637        )
638        .route("/morphology/list/types", get(morphology::get_list_types))
639        .route(
640            "/morphology/morphologies",
641            get(morphology::get_morphologies),
642        )
643        .route(
644            "/morphology/morphology",
645            axum::routing::post(morphology::post_morphology)
646                .put(morphology::put_morphology)
647                .delete(morphology::delete_morphology_by_name),
648        )
649        .route(
650            "/morphology/rename",
651            axum::routing::put(morphology::put_rename_morphology),
652        )
653        .route(
654            "/morphology/morphology_properties",
655            axum::routing::post(morphology::post_morphology_properties),
656        )
657        .route(
658            "/morphology/morphology_usage",
659            axum::routing::post(morphology::post_morphology_usage),
660        )
661        .route("/morphology/list", get(morphology::get_list))
662        .route("/morphology/info/:morphology_id", get(morphology::get_info))
663        .route(
664            "/morphology/create",
665            axum::routing::post(morphology::post_create),
666        )
667        .route(
668            "/morphology/update",
669            axum::routing::put(morphology::put_update),
670        )
671        .route(
672            "/morphology/delete/:morphology_id",
673            axum::routing::delete(morphology::delete_morphology),
674        )
675        // ===== REGION MODULE (12 endpoints) =====
676        .route("/region/regions_members", get(region::get_regions_members))
677        .route(
678            "/region/region",
679            axum::routing::post(region::post_region)
680                .put(region::put_region)
681                .delete(region::delete_region),
682        )
683        .route("/region/clone", axum::routing::post(region::post_clone))
684        .route(
685            "/region/relocate_members",
686            put(region::put_relocate_members),
687        )
688        .route(
689            "/region/region_and_members",
690            axum::routing::delete(region::delete_region_and_members),
691        )
692        .route("/region/regions", get(region::get_regions))
693        .route("/region/region_titles", get(region::get_region_titles))
694        .route("/region/region/:region_id", get(region::get_region_detail))
695        .route(
696            "/region/change_region_parent",
697            put(region::put_change_region_parent),
698        )
699        .route(
700            "/region/change_cortical_area_region",
701            put(region::put_change_cortical_area_region),
702        )
703        // ===== CORTICAL_MAPPING MODULE (8 endpoints) =====
704        .route(
705            "/cortical_mapping/afferents",
706            axum::routing::post(cortical_mapping::post_afferents),
707        )
708        .route(
709            "/cortical_mapping/efferents",
710            axum::routing::post(cortical_mapping::post_efferents),
711        )
712        .route(
713            "/cortical_mapping/mapping_properties",
714            axum::routing::post(cortical_mapping::post_mapping_properties)
715                .put(cortical_mapping::put_mapping_properties),
716        )
717        .route(
718            "/cortical_mapping/mapping",
719            get(cortical_mapping::get_mapping).delete(cortical_mapping::delete_mapping),
720        )
721        .route(
722            "/cortical_mapping/mapping_list",
723            get(cortical_mapping::get_mapping_list),
724        )
725        .route(
726            "/cortical_mapping/batch_update",
727            axum::routing::post(cortical_mapping::post_batch_update),
728        )
729        .route(
730            "/cortical_mapping/mapping",
731            axum::routing::post(cortical_mapping::post_mapping).put(cortical_mapping::put_mapping),
732        )
733        // ===== CONNECTOME MODULE (21 endpoints) =====
734        .route(
735            "/connectome/cortical_areas/list/detailed",
736            get(connectome::get_cortical_areas_list_detailed),
737        )
738        .route(
739            "/connectome/properties/dimensions",
740            get(connectome::get_properties_dimensions),
741        )
742        .route(
743            "/connectome/properties/mappings",
744            get(connectome::get_properties_mappings),
745        )
746        .route("/connectome/snapshot", get(connectome::get_snapshot))
747        .route("/connectome/stats", get(connectome::get_stats))
748        .route(
749            "/connectome/batch_neuron_operations",
750            axum::routing::post(connectome::post_batch_neuron_operations),
751        )
752        .route(
753            "/connectome/batch_synapse_operations",
754            axum::routing::post(connectome::post_batch_synapse_operations),
755        )
756        .route(
757            "/connectome/neuron_count",
758            get(connectome::get_neuron_count),
759        )
760        .route(
761            "/connectome/synapse_count",
762            get(connectome::get_synapse_count),
763        )
764        .route("/connectome/paths", get(connectome::get_paths))
765        .route(
766            "/connectome/cumulative_stats",
767            get(connectome::get_cumulative_stats),
768        )
769        .route(
770            "/connectome/area_details",
771            get(connectome::get_area_details),
772        )
773        .route(
774            "/connectome/rebuild",
775            axum::routing::post(connectome::post_rebuild),
776        )
777        .route("/connectome/structure", get(connectome::get_structure))
778        .route(
779            "/connectome/clear",
780            axum::routing::post(connectome::post_clear),
781        )
782        .route("/connectome/validation", get(connectome::get_validation))
783        .route("/connectome/topology", get(connectome::get_topology))
784        .route(
785            "/connectome/optimize",
786            axum::routing::post(connectome::post_optimize),
787        )
788        .route(
789            "/connectome/connectivity_matrix",
790            get(connectome::get_connectivity_matrix),
791        )
792        .route(
793            "/connectome/neurons/batch",
794            axum::routing::post(connectome::post_neurons_batch),
795        )
796        .route(
797            "/connectome/synapses/batch",
798            axum::routing::post(connectome::post_synapses_batch),
799        )
800        .route(
801            "/connectome/cortical_areas/list/summary",
802            get(connectome::get_cortical_areas_list_summary),
803        )
804        .route(
805            "/connectome/cortical_areas/list/transforming",
806            get(connectome::get_cortical_areas_list_transforming),
807        )
808        .route(
809            "/connectome/cortical_area/list/types",
810            get(connectome::get_cortical_area_list_types),
811        )
812        .route(
813            "/connectome/cortical_area/:cortical_id/neurons",
814            get(connectome::get_cortical_area_neurons),
815        )
816        .route(
817            "/connectome/:cortical_area_id/synapses",
818            get(connectome::get_area_synapses),
819        )
820        .route(
821            "/connectome/cortical_info/:cortical_area",
822            get(connectome::get_cortical_info),
823        )
824        .route(
825            "/connectome/stats/cortical/cumulative/:cortical_area",
826            get(connectome::get_stats_cortical_cumulative),
827        )
828        .route(
829            "/connectome/neuron/:neuron_id/properties",
830            get(connectome::get_neuron_properties_by_id),
831        )
832        .route(
833            "/connectome/neuron_properties",
834            get(connectome::get_neuron_properties_query),
835        )
836        .route(
837            "/connectome/neuron_properties_at",
838            get(connectome::get_neuron_properties_at_query),
839        )
840        .route(
841            "/connectome/memory_neuron",
842            get(connectome::get_memory_neuron),
843        )
844        .route(
845            "/connectome/area_neurons",
846            get(connectome::get_area_neurons_query),
847        )
848        .route(
849            "/connectome/fire_queue/:cortical_area",
850            get(connectome::get_fire_queue_area),
851        )
852        .route(
853            "/connectome/plasticity",
854            get(connectome::get_plasticity_info),
855        )
856        .route("/connectome/path", get(connectome::get_path_query))
857        .route(
858            "/connectome/download",
859            get(connectome::get_download_connectome),
860        )
861        .route(
862            "/connectome/download-cortical-area/:cortical_area",
863            get(connectome::get_download_cortical_area),
864        )
865        .route(
866            "/connectome/upload",
867            axum::routing::post(connectome::post_upload_connectome),
868        )
869        .route(
870            "/connectome/upload-cortical-area",
871            axum::routing::post(connectome::post_upload_cortical_area),
872        )
873        // ===== BURST_ENGINE MODULE (14 endpoints) =====
874        .route(
875            "/burst_engine/simulation_timestep",
876            get(burst_engine::get_simulation_timestep).post(burst_engine::post_simulation_timestep),
877        )
878        .route("/burst_engine/fcl", get(burst_engine::get_fcl))
879        .route(
880            "/burst_engine/fcl/neuron",
881            get(burst_engine::get_fcl_neuron),
882        )
883        .route(
884            "/burst_engine/fire_queue",
885            get(burst_engine::get_fire_queue),
886        )
887        .route(
888            "/burst_engine/fire_queue/neuron",
889            get(burst_engine::get_fire_queue_neuron),
890        )
891        .route(
892            "/burst_engine/fcl_reset",
893            axum::routing::post(burst_engine::post_fcl_reset),
894        )
895        .route(
896            "/burst_engine/fcl_status",
897            get(burst_engine::get_fcl_status),
898        )
899        .route(
900            "/burst_engine/fcl_sampler/config",
901            get(burst_engine::get_fcl_sampler_config).post(burst_engine::post_fcl_sampler_config),
902        )
903        .route(
904            "/burst_engine/fcl_sampler/area/:area_id/sample_rate",
905            get(burst_engine::get_area_fcl_sample_rate)
906                .post(burst_engine::post_area_fcl_sample_rate),
907        )
908        .route(
909            "/burst_engine/fire_ledger/default_window_size",
910            get(burst_engine::get_fire_ledger_default_window_size)
911                .put(burst_engine::put_fire_ledger_default_window_size),
912        )
913        .route(
914            "/burst_engine/fire_ledger/areas_window_config",
915            get(burst_engine::get_fire_ledger_areas_window_config),
916        )
917        .route("/burst_engine/stats", get(burst_engine::get_stats))
918        .route("/burst_engine/status", get(burst_engine::get_status))
919        .route(
920            "/burst_engine/control",
921            axum::routing::post(burst_engine::post_control),
922        )
923        .route(
924            "/burst_engine/burst_counter",
925            get(burst_engine::get_burst_counter),
926        )
927        .route(
928            "/burst_engine/start",
929            axum::routing::post(burst_engine::post_start),
930        )
931        .route(
932            "/burst_engine/stop",
933            axum::routing::post(burst_engine::post_stop),
934        )
935        .route(
936            "/burst_engine/hold",
937            axum::routing::post(burst_engine::post_hold),
938        )
939        .route(
940            "/burst_engine/resume",
941            axum::routing::post(burst_engine::post_resume),
942        )
943        .route(
944            "/burst_engine/config",
945            get(burst_engine::get_config).put(burst_engine::put_config),
946        )
947        .route(
948            "/burst_engine/fire_ledger/area/:area_id/window_size",
949            get(burst_engine::get_fire_ledger_area_window_size)
950                .put(burst_engine::put_fire_ledger_area_window_size),
951        )
952        .route(
953            "/burst_engine/fire_ledger/area/:area_id/history",
954            get(burst_engine::get_fire_ledger_history),
955        )
956        .route(
957            "/burst_engine/membrane_potentials",
958            get(burst_engine::get_membrane_potentials).put(burst_engine::put_membrane_potentials),
959        )
960        .route(
961            "/burst_engine/frequency_status",
962            get(burst_engine::get_frequency_status),
963        )
964        .route(
965            "/burst_engine/measure_frequency",
966            axum::routing::post(burst_engine::post_measure_frequency),
967        )
968        .route(
969            "/burst_engine/frequency_history",
970            get(burst_engine::get_frequency_history),
971        )
972        .route(
973            "/burst_engine/force_connectome_integration",
974            axum::routing::post(burst_engine::post_force_connectome_integration),
975        )
976        // ===== GENOME MODULE (22 endpoints) =====
977        .route("/genome/file_name", get(genome::get_file_name))
978        .route("/genome/circuits", get(genome::get_circuits))
979        .route(
980            "/genome/amalgamation_destination",
981            axum::routing::post(genome::post_amalgamation_destination),
982        )
983        .route(
984            "/genome/amalgamation_cancellation",
985            axum::routing::delete(genome::delete_amalgamation_cancellation),
986        )
987        .route(
988            "/feagi/genome/append",
989            axum::routing::post(genome::post_genome_append),
990        )
991        .route(
992            "/genome/upload/barebones",
993            axum::routing::post(genome::post_upload_barebones_genome),
994        )
995        .route(
996            "/genome/upload/essential",
997            axum::routing::post(genome::post_upload_essential_genome),
998        )
999        .route("/genome/name", get(genome::get_name))
1000        .route("/genome/timestamp", get(genome::get_timestamp))
1001        .route("/genome/save", axum::routing::post(genome::post_save))
1002        .route("/genome/load", axum::routing::post(genome::post_load))
1003        .route("/genome/upload", axum::routing::post(genome::post_upload))
1004        .route("/genome/download", get(genome::get_download))
1005        .route("/genome/properties", get(genome::get_properties))
1006        .route(
1007            "/genome/validate",
1008            axum::routing::post(genome::post_validate),
1009        )
1010        .route(
1011            "/genome/transform",
1012            axum::routing::post(genome::post_transform),
1013        )
1014        .route("/genome/clone", axum::routing::post(genome::post_clone))
1015        .route("/genome/reset", axum::routing::post(genome::post_reset))
1016        .route("/genome/metadata", get(genome::get_metadata))
1017        .route("/genome/merge", axum::routing::post(genome::post_merge))
1018        .route("/genome/diff", get(genome::get_diff))
1019        .route(
1020            "/genome/export_format",
1021            axum::routing::post(genome::post_export_format),
1022        )
1023        .route("/genome/amalgamation", get(genome::get_amalgamation))
1024        .route(
1025            "/genome/amalgamation_history",
1026            get(genome::get_amalgamation_history_exact),
1027        )
1028        .route(
1029            "/genome/cortical_template",
1030            get(genome::get_cortical_template),
1031        )
1032        .route("/genome/defaults/files", get(genome::get_defaults_files))
1033        .route("/genome/download_region", get(genome::get_download_region))
1034        .route("/genome/genome_number", get(genome::get_genome_number))
1035        .route(
1036            "/genome/amalgamation_by_filename",
1037            axum::routing::post(genome::post_amalgamation_by_filename),
1038        )
1039        .route(
1040            "/genome/amalgamation_by_payload",
1041            axum::routing::post(genome::post_amalgamation_by_payload),
1042        )
1043        .route(
1044            "/genome/amalgamation_by_upload",
1045            axum::routing::post(genome::post_amalgamation_by_upload),
1046        )
1047        .route(
1048            "/genome/append-file",
1049            axum::routing::post(genome::post_append_file),
1050        )
1051        .route(
1052            "/genome/upload/file",
1053            axum::routing::post(genome::post_upload_file),
1054        )
1055        .route(
1056            "/genome/upload/file/edit",
1057            axum::routing::post(genome::post_upload_file_edit),
1058        )
1059        .route(
1060            "/genome/upload/string",
1061            axum::routing::post(genome::post_upload_string),
1062        )
1063        // ===== NEUROPLASTICITY MODULE (7 endpoints) =====
1064        .route(
1065            "/neuroplasticity/plasticity_queue_depth",
1066            get(neuroplasticity::get_plasticity_queue_depth)
1067                .put(neuroplasticity::put_plasticity_queue_depth),
1068        )
1069        .route("/neuroplasticity/status", get(neuroplasticity::get_status))
1070        .route(
1071            "/neuroplasticity/transforming",
1072            get(neuroplasticity::get_transforming),
1073        )
1074        .route(
1075            "/neuroplasticity/configure",
1076            axum::routing::post(neuroplasticity::post_configure),
1077        )
1078        .route(
1079            "/neuroplasticity/enable/:area_id",
1080            axum::routing::post(neuroplasticity::post_enable_area),
1081        )
1082        .route(
1083            "/neuroplasticity/disable/:area_id",
1084            axum::routing::post(neuroplasticity::post_disable_area),
1085        )
1086        // ===== INSIGHT MODULE (6 endpoints) =====
1087        .route(
1088            "/insight/neurons/membrane_potential_status",
1089            axum::routing::post(insight::post_neurons_membrane_potential_status),
1090        )
1091        .route(
1092            "/insight/neuron/synaptic_potential_status",
1093            axum::routing::post(insight::post_neuron_synaptic_potential_status),
1094        )
1095        .route(
1096            "/insight/neurons/membrane_potential_set",
1097            axum::routing::post(insight::post_neurons_membrane_potential_set),
1098        )
1099        .route(
1100            "/insight/neuron/synaptic_potential_set",
1101            axum::routing::post(insight::post_neuron_synaptic_potential_set),
1102        )
1103        .route("/insight/analytics", get(insight::get_analytics))
1104        .route("/insight/data", get(insight::get_data))
1105        // ===== INPUT MODULE (4 endpoints) =====
1106        .route(
1107            "/input/vision",
1108            get(input::get_vision).post(input::post_vision),
1109        )
1110        .route("/input/sources", get(input::get_sources))
1111        .route(
1112            "/input/configure",
1113            axum::routing::post(input::post_configure),
1114        )
1115        // ===== OUTPUTS MODULE (2 endpoints) - Python uses /v1/output (singular)
1116        .route("/output/targets", get(outputs::get_targets))
1117        .route(
1118            "/output/configure",
1119            axum::routing::post(outputs::post_configure),
1120        )
1121        // ===== PHYSIOLOGY MODULE (2 endpoints) =====
1122        .route(
1123            "/physiology/",
1124            get(physiology::get_physiology).put(physiology::put_physiology),
1125        )
1126        // ===== SIMULATION MODULE (6 endpoints) =====
1127        .route(
1128            "/simulation/upload/string",
1129            axum::routing::post(simulation::post_stimulation_upload),
1130        )
1131        .route(
1132            "/simulation/reset",
1133            axum::routing::post(simulation::post_reset),
1134        )
1135        .route("/simulation/status", get(simulation::get_status))
1136        .route("/simulation/stats", get(simulation::get_stats))
1137        .route(
1138            "/simulation/config",
1139            axum::routing::post(simulation::post_config),
1140        )
1141        // ===== TRAINING MODULE (25 endpoints) =====
1142        .route("/training/shock", axum::routing::post(training::post_shock))
1143        .route("/training/shock/options", get(training::get_shock_options))
1144        .route("/training/shock/status", get(training::get_shock_status))
1145        .route(
1146            "/training/shock/activate",
1147            axum::routing::post(training::post_shock_activate),
1148        )
1149        .route(
1150            "/training/reward/intensity",
1151            axum::routing::post(training::post_reward_intensity),
1152        )
1153        .route(
1154            "/training/reward",
1155            axum::routing::post(training::post_reward),
1156        )
1157        .route(
1158            "/training/punishment/intensity",
1159            axum::routing::post(training::post_punishment_intensity),
1160        )
1161        .route(
1162            "/training/punishment",
1163            axum::routing::post(training::post_punishment),
1164        )
1165        .route(
1166            "/training/gameover",
1167            axum::routing::post(training::post_gameover),
1168        )
1169        .route("/training/brain_fitness", get(training::get_brain_fitness))
1170        .route(
1171            "/training/fitness_criteria",
1172            get(training::get_fitness_criteria)
1173                .put(training::put_fitness_criteria)
1174                .post(training::post_fitness_criteria),
1175        )
1176        .route(
1177            "/training/fitness_stats",
1178            get(training::get_fitness_stats)
1179                .put(training::put_fitness_stats)
1180                .delete(training::delete_fitness_stats),
1181        )
1182        .route(
1183            "/training/reset_fitness_stats",
1184            axum::routing::delete(training::delete_reset_fitness_stats),
1185        )
1186        .route(
1187            "/training/training_report",
1188            get(training::get_training_report),
1189        )
1190        .route("/training/status", get(training::get_status))
1191        .route("/training/stats", get(training::get_stats))
1192        .route(
1193            "/training/config",
1194            axum::routing::post(training::post_config),
1195        )
1196        // ===== VISUALIZATION MODULE (4 endpoints) =====
1197        .route(
1198            "/visualization/register_client",
1199            axum::routing::post(visualization::post_register_client),
1200        )
1201        .route(
1202            "/visualization/unregister_client",
1203            axum::routing::post(visualization::post_unregister_client),
1204        )
1205        .route(
1206            "/visualization/heartbeat",
1207            axum::routing::post(visualization::post_heartbeat),
1208        )
1209        .route("/visualization/status", get(visualization::get_status))
1210        // ===== MONITORING MODULE (5 endpoints) =====
1211        .route("/monitoring/status", get(monitoring::get_status))
1212        .route("/monitoring/metrics", get(monitoring::get_metrics))
1213        .route("/monitoring/data", get(monitoring::get_data))
1214        .route("/monitoring/performance", get(monitoring::get_performance))
1215        .route(
1216            "/monitoring/cortical_activity",
1217            get(monitoring::get_cortical_activity),
1218        )
1219        // ===== EVOLUTION MODULE (3 endpoints) =====
1220        .route("/evolution/status", get(evolution::get_status))
1221        .route(
1222            "/evolution/config",
1223            axum::routing::post(evolution::post_config),
1224        )
1225        // ===== SNAPSHOT MODULE (12 endpoints) =====
1226        // TODO: Implement snapshot endpoints
1227        // .route("/snapshot/create", axum::routing::post(snapshot::post_create))
1228        // .route("/snapshot/restore", axum::routing::post(snapshot::post_restore))
1229        // .route("/snapshot/", get(snapshot::get_list))
1230        // .route("/snapshot/:snapshot_id", axum::routing::delete(snapshot::delete_snapshot))
1231        // .route("/snapshot/:snapshot_id/artifact/:fmt", get(snapshot::get_artifact))
1232        // .route("/snapshot/compare", axum::routing::post(snapshot::post_compare))
1233        // .route("/snapshot/upload", axum::routing::post(snapshot::post_upload))
1234        // // Python uses /v1/snapshots/* (note the S)
1235        // .route("/snapshots/connectome", axum::routing::post(snapshot::post_snapshots_connectome))
1236        // .route("/snapshots/connectome/:snapshot_id/restore", axum::routing::post(snapshot::post_snapshots_connectome_restore))
1237        // .route("/snapshots/:snapshot_id/restore", axum::routing::post(snapshot::post_snapshots_restore))
1238        // .route("/snapshots/:snapshot_id", axum::routing::delete(snapshot::delete_snapshots_by_id))
1239        // .route("/snapshots/:snapshot_id/artifact/:fmt", get(snapshot::get_snapshots_artifact))
1240        // ===== NETWORK MODULE (3 endpoints) =====
1241        .route("/network/status", get(network::get_status))
1242        .route("/network/config", axum::routing::post(network::post_config))
1243        .route(
1244            "/network/connection_info",
1245            get(network::get_connection_info),
1246        )
1247}
1248
1249/// OpenAPI spec handler
1250#[allow(dead_code)] // In development - will be wired to OpenAPI route
1251async fn openapi_spec() -> Json<utoipa::openapi::OpenApi> {
1252    Json(ApiDoc::openapi())
1253}
1254
1255// ============================================================================
1256// CORS CONFIGURATION
1257// ============================================================================
1258
1259/// Create CORS layer for the API
1260///
1261/// TODO: Configure for production:
1262/// - Restrict allowed origins
1263/// - Allowed methods restricted
1264/// - Credentials support as needed
1265fn create_cors_layer() -> CorsLayer {
1266    CorsLayer::new()
1267        .allow_origin(Any)
1268        .allow_methods(Any)
1269        .allow_headers(Any)
1270}
1271
1272/// Middleware to log request and response bodies for debugging
1273async fn log_request_response_bodies(
1274    request: Request<Body>,
1275    next: Next,
1276) -> Result<Response, StatusCode> {
1277    let (parts, body) = request.into_parts();
1278
1279    // Only log bodies for POST/PUT/PATCH/DELETE requests
1280    let should_log_request = matches!(parts.method.as_str(), "POST" | "PUT" | "PATCH" | "DELETE");
1281
1282    let body_bytes = if should_log_request {
1283        // Collect body bytes
1284        match body.collect().await {
1285            Ok(collected) => {
1286                let bytes = collected.to_bytes();
1287                // Log request body if it's JSON
1288                if let Ok(body_str) = String::from_utf8(bytes.to_vec()) {
1289                    if !body_str.is_empty() {
1290                        tracing::trace!(target: "feagi-api", "Request body: {}", body_str);
1291                    }
1292                }
1293                bytes
1294            }
1295            Err(_) => {
1296                return Err(StatusCode::INTERNAL_SERVER_ERROR);
1297            }
1298        }
1299    } else {
1300        axum::body::Bytes::new()
1301    };
1302
1303    // Reconstruct request with original body
1304    let request = Request::from_parts(parts, Body::from(body_bytes));
1305
1306    // Call the next handler
1307    let response = next.run(request).await;
1308
1309    // Log response body
1310    let (parts, body) = response.into_parts();
1311
1312    match body.collect().await {
1313        Ok(collected) => {
1314            let bytes = collected.to_bytes();
1315            // Log response body if it's JSON and not too large
1316            if bytes.len() < 10000 {
1317                // Only log responses < 10KB
1318                if let Ok(body_str) = String::from_utf8(bytes.to_vec()) {
1319                    if !body_str.is_empty() && body_str.starts_with('{') {
1320                        tracing::trace!(target: "feagi-api", "Response body: {}", body_str);
1321                    }
1322                }
1323            }
1324            // Reconstruct response
1325            Ok(Response::from_parts(parts, Body::from(bytes)))
1326        }
1327        Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
1328    }
1329}
1330
1331// ============================================================================
1332// HELPER HANDLERS
1333// ============================================================================
1334
1335/// Root redirect handler - redirects to Swagger UI
1336async fn root_redirect() -> Redirect {
1337    Redirect::permanent("/swagger-ui/")
1338}
1339
1340// Custom Swagger UI with FEAGI branding and dark/light themes
1341// Embedded from templates/custom-swagger-ui.html at compile time
1342async fn custom_swagger_ui() -> Html<&'static str> {
1343    const CUSTOM_SWAGGER_HTML: &str = include_str!("../../../templates/custom-swagger-ui.html");
1344    Html(CUSTOM_SWAGGER_HTML)
1345}
1346
1347// ============================================================================
1348// PLACEHOLDER HANDLERS (for endpoints not yet implemented)
1349// ============================================================================
1350
1351/// Placeholder handler for unimplemented endpoints
1352/// Returns 501 Not Implemented with a clear message
1353#[allow(dead_code)] // In development - will be used for placeholder routes
1354async fn placeholder_handler(State(_state): State<ApiState>) -> Response {
1355    (
1356        StatusCode::NOT_IMPLEMENTED,
1357        Json(serde_json::json!({
1358            "error": "Not yet implemented",
1359            "message": "This endpoint is registered but not yet implemented in Rust. See Python implementation."
1360        }))
1361    ).into_response()
1362}
1363
1364/// Placeholder health check - returns basic response
1365#[allow(dead_code)] // In development - will be used for basic health route
1366async fn placeholder_health_check(State(_state): State<ApiState>) -> Response {
1367    (
1368        StatusCode::OK,
1369        Json(serde_json::json!({
1370            "status": "ok",
1371            "message": "Health check placeholder - Python-compatible path structure confirmed",
1372            "burst_engine": false,
1373            "brain_readiness": false
1374        })),
1375    )
1376        .into_response()
1377}