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