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