Skip to main content

feagi_api/transports/http/
server.rs

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