feagi_api/endpoints/
system.rs

1// Copyright 2025 Neuraville Inc.
2// Licensed under the Apache License, Version 2.0
3
4//! System API Endpoints - Exact port from Python `/v1/system/*`
5//!
6//! Reference: feagi-py/feagi/api/v1/system.py
7
8use serde::{Deserialize, Serialize};
9use std::collections::HashMap;
10
11use crate::amalgamation;
12use crate::common::ApiState;
13use crate::common::{ApiError, ApiResult, Json, State};
14
15// ============================================================================
16// REQUEST/RESPONSE MODELS (matching Python schemas exactly)
17// ============================================================================
18
19#[allow(non_snake_case)] // Field name matches Python API for compatibility
20#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
21pub struct FatigueInfo {
22    /// Fatigue index (0-100) - maximum utilization across all fatigue criteria
23    pub fatigue_index: Option<u8>,
24    /// Whether fatigue is currently active (triggers fatigue neuron injection)
25    pub fatigue_active: Option<bool>,
26    /// Regular neuron utilization percentage (0-100)
27    pub regular_neuron_util: Option<u8>,
28    /// Memory neuron utilization percentage (0-100)
29    pub memory_neuron_util: Option<u8>,
30    /// Synapse utilization percentage (0-100)
31    pub synapse_util: Option<u8>,
32}
33
34#[allow(non_snake_case)] // Field name matches Python API for compatibility
35#[derive(Debug, Serialize, Deserialize, utoipa::ToSchema)]
36pub struct HealthCheckResponse {
37    pub burst_engine: bool,
38    pub connected_agents: Option<i32>,
39    pub influxdb_availability: bool,
40    pub neuron_count_max: i64,
41    pub synapse_count_max: i64,
42    pub latest_changes_saved_externally: bool,
43    pub genome_availability: bool,
44    pub genome_validity: Option<bool>,
45    pub brain_readiness: bool,
46    pub feagi_session: Option<i64>,
47    pub fitness: Option<f64>,
48    pub cortical_area_count: Option<i32>,
49    pub neuron_count: Option<i64>,
50    pub memory_neuron_count: Option<i64>,
51    pub regular_neuron_count: Option<i64>,
52    pub synapse_count: Option<i64>,
53    pub estimated_brain_size_in_MB: Option<f64>,
54    pub genome_num: Option<i32>,
55    pub genome_timestamp: Option<i64>,
56    pub simulation_timestep: Option<f64>,
57    pub memory_area_stats: Option<HashMap<String, HashMap<String, serde_json::Value>>>,
58    pub amalgamation_pending: Option<HashMap<String, serde_json::Value>>,
59    /// Hash of brain regions (hierarchy, membership, and properties)
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub brain_regions_hash: Option<u64>,
62    /// Hash of cortical areas and properties (excluding mappings)
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub cortical_areas_hash: Option<u64>,
65    /// Hash of brain geometry (area positions/dimensions and 2D coordinates)
66    #[serde(skip_serializing_if = "Option::is_none")]
67    pub brain_geometry_hash: Option<u64>,
68    /// Hash of morphology registry
69    #[serde(skip_serializing_if = "Option::is_none")]
70    pub morphologies_hash: Option<u64>,
71    /// Hash of cortical mappings
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub cortical_mappings_hash: Option<u64>,
74    /// Root brain region ID (UUID string) for O(1) root lookup
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub brain_regions_root: Option<String>,
77    /// Fatigue information (index, active state, and breakdown of contributing elements)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub fatigue: Option<FatigueInfo>,
80}
81
82// ============================================================================
83// ENDPOINTS
84// ============================================================================
85
86/// Get comprehensive system health including burst engine status, neuron/synapse counts, and genome availability.
87#[utoipa::path(
88    get,
89    path = "/v1/system/health_check",
90    responses(
91        (status = 200, description = "System health retrieved successfully", body = HealthCheckResponse),
92        (status = 500, description = "Internal server error")
93    ),
94    tag = "system"
95)]
96pub async fn get_health_check(
97    State(state): State<ApiState>,
98) -> ApiResult<Json<HealthCheckResponse>> {
99    let analytics_service = state.analytics_service.as_ref();
100
101    // Get system health from analytics service
102    let health = analytics_service
103        .get_system_health()
104        .await
105        .map_err(|e| ApiError::internal(format!("Failed to get system health: {}", e)))?;
106
107    // Get runtime status if available (source of truth for current burst frequency).
108    let runtime_status = state.runtime_service.get_status().await.ok();
109    let burst_engine_active = runtime_status
110        .as_ref()
111        .map(|status| status.is_running)
112        .unwrap_or(false);
113
114    let _burst_count = state.runtime_service.get_burst_count().await.ok();
115
116    // Get connected agents count from agent service
117    let connected_agents = if let Some(agent_service) = state.agent_service.as_ref() {
118        agent_service
119            .list_agents()
120            .await
121            .ok()
122            .map(|agents| agents.len() as i32)
123    } else {
124        None
125    };
126
127    // Get total synapse count from analytics service
128    let synapse_count = analytics_service
129        .get_total_synapse_count()
130        .await
131        .ok()
132        .map(|count| count as i64);
133
134    // Get regular and memory neuron counts
135    let regular_neuron_count = analytics_service
136        .get_regular_neuron_count()
137        .await
138        .ok()
139        .map(|count| count as i64);
140
141    let memory_neuron_count = analytics_service
142        .get_memory_neuron_count()
143        .await
144        .ok()
145        .map(|count| count as i64);
146
147    // Get genome info for simulation_timestep, genome_num, and genome_timestamp
148    let genome_info = state.genome_service.get_genome_info().await.ok();
149
150    // Prefer runtime frequency-derived timestep to reflect the active simulation rate.
151    let runtime_timestep = runtime_status.as_ref().map(|status| {
152        if status.frequency_hz > 0.0 {
153            1.0 / status.frequency_hz
154        } else {
155            0.0
156        }
157    });
158    let simulation_timestep =
159        runtime_timestep.or_else(|| genome_info.as_ref().map(|info| info.simulation_timestep));
160    let genome_num = genome_info.as_ref().and_then(|info| info.genome_num);
161    let genome_timestamp = genome_info.as_ref().and_then(|info| info.genome_timestamp);
162
163    // Calculate estimated brain size in MB
164    // Rough estimates: ~64 bytes per neuron + ~16 bytes per synapse + metadata
165    #[allow(non_snake_case)] // Matching Python API field name for compatibility
166    let estimated_brain_size_in_MB = {
167        let neuron_bytes = health.neuron_count * 64;
168        let synapse_bytes = synapse_count.unwrap_or(0) as usize * 16;
169        let metadata_bytes = health.cortical_area_count * 512; // ~512 bytes per area
170        let total_bytes = neuron_bytes + synapse_bytes + metadata_bytes;
171        Some((total_bytes as f64) / (1024.0 * 1024.0))
172    };
173
174    // Get actual NPU capacity from SystemHealth (single source of truth from config)
175    let neuron_count_max = health.neuron_capacity as i64;
176    let synapse_count_max = health.synapse_capacity as i64;
177
178    // Configuration values (should eventually come from config service)
179    let influxdb_availability = false; // TODO: Get from monitoring service
180    let latest_changes_saved_externally = false; // TODO: Get from state manager
181    let genome_availability = health.cortical_area_count > 0;
182    let genome_validity = Some(health.brain_readiness);
183
184    // Get FEAGI session timestamp (unique identifier for this FEAGI instance)
185    let feagi_session = Some(state.feagi_session_timestamp);
186
187    // Fields requiring future service implementations
188    let fitness = None; // TODO: Get from evolution service
189
190    // Get memory area stats from plasticity service cache (event-driven updates).
191    //
192    // IMPORTANT: BV expects both:
193    // - per-area stats keyed by cortical_id (base64), and
194    // - a global `memory_neuron_count` that matches the sum of per-area `neuron_count`.
195    let (memory_area_stats, memory_neuron_count_from_cache) = state
196        .memory_stats_cache
197        .as_ref()
198        .map(|cache| {
199            let snapshot = feagi_npu_plasticity::memory_stats_cache::get_stats_snapshot(cache);
200            let total = snapshot
201                .values()
202                .map(|s| s.neuron_count as i64)
203                .sum::<i64>();
204            let per_area = snapshot
205                .into_iter()
206                .map(|(name, stats)| {
207                    let mut inner_map = HashMap::new();
208                    inner_map.insert(
209                        "neuron_count".to_string(),
210                        serde_json::json!(stats.neuron_count),
211                    );
212                    inner_map.insert(
213                        "created_total".to_string(),
214                        serde_json::json!(stats.created_total),
215                    );
216                    inner_map.insert(
217                        "deleted_total".to_string(),
218                        serde_json::json!(stats.deleted_total),
219                    );
220                    inner_map.insert(
221                        "last_updated".to_string(),
222                        serde_json::json!(stats.last_updated),
223                    );
224                    (name, inner_map)
225                })
226                .collect::<HashMap<String, HashMap<String, serde_json::Value>>>();
227            (Some(per_area), Some(total))
228        })
229        .unwrap_or((None, None));
230
231    // Prefer the plasticity cache-derived total to avoid discrepancies.
232    let memory_neuron_count = memory_neuron_count_from_cache.or(memory_neuron_count);
233
234    // BV expects `amalgamation_pending` to be a dict with:
235    // - amalgamation_id
236    // - genome_title
237    // - circuit_size
238    let amalgamation_pending = state.amalgamation_state.read().pending.as_ref().map(|p| {
239        let v = amalgamation::pending_summary_to_health_json(&p.summary);
240        v.as_object()
241            .cloned()
242            .unwrap_or_default()
243            .into_iter()
244            .collect::<HashMap<String, serde_json::Value>>()
245    });
246
247    // Get root region ID from ConnectomeManager (only available when services feature is enabled)
248    #[cfg(feature = "services")]
249    let brain_regions_root = feagi_brain_development::ConnectomeManager::instance()
250        .read()
251        .get_root_region_id();
252    #[cfg(not(feature = "services"))]
253    let brain_regions_root = None; // WASM: Use connectome service instead
254
255    // Get fatigue information from state manager
256    // Note: feagi-state-manager is included in the "services" feature
257    #[cfg(feature = "services")]
258    let fatigue = {
259        use feagi_state_manager::StateManager;
260        // Initialize singleton on first access (Lazy will handle this)
261        match StateManager::instance().try_read() {
262            Some(state_manager) => {
263                let core_state = state_manager.get_core_state();
264                Some(FatigueInfo {
265                    fatigue_index: Some(core_state.get_fatigue_index()),
266                    fatigue_active: Some(core_state.is_fatigue_active()),
267                    regular_neuron_util: Some(core_state.get_regular_neuron_util()),
268                    memory_neuron_util: Some(core_state.get_memory_neuron_util()),
269                    synapse_util: Some(core_state.get_synapse_util()),
270                })
271            }
272            None => {
273                // State manager is locked, return None (shouldn't happen in normal operation)
274                tracing::warn!(target: "feagi-api", "StateManager is locked, cannot read fatigue data");
275                None
276            }
277        }
278    };
279    #[cfg(not(feature = "services"))]
280    let fatigue = {
281        tracing::debug!(target: "feagi-api", "Services feature not enabled, fatigue data unavailable");
282        None
283    };
284
285    let (
286        brain_regions_hash,
287        cortical_areas_hash,
288        brain_geometry_hash,
289        morphologies_hash,
290        cortical_mappings_hash,
291    ) = {
292        let state_manager = feagi_state_manager::StateManager::instance();
293        let state_manager = state_manager.read();
294        (
295            Some(state_manager.get_brain_regions_hash()),
296            Some(state_manager.get_cortical_areas_hash()),
297            Some(state_manager.get_brain_geometry_hash()),
298            Some(state_manager.get_morphologies_hash()),
299            Some(state_manager.get_cortical_mappings_hash()),
300        )
301    };
302
303    Ok(Json(HealthCheckResponse {
304        burst_engine: burst_engine_active,
305        connected_agents,
306        influxdb_availability,
307        neuron_count_max,
308        synapse_count_max,
309        latest_changes_saved_externally,
310        genome_availability,
311        genome_validity,
312        brain_readiness: health.brain_readiness,
313        feagi_session,
314        fitness,
315        cortical_area_count: Some(health.cortical_area_count as i32),
316        neuron_count: Some(health.neuron_count as i64),
317        memory_neuron_count,
318        regular_neuron_count,
319        synapse_count,
320        estimated_brain_size_in_MB,
321        genome_num,
322        genome_timestamp,
323        simulation_timestep,
324        memory_area_stats,
325        amalgamation_pending,
326        brain_regions_hash,
327        cortical_areas_hash,
328        brain_geometry_hash,
329        morphologies_hash,
330        cortical_mappings_hash,
331        brain_regions_root, // NEW: Root region ID for O(1) lookup
332        fatigue,
333    }))
334}
335
336/// Get the visualization skip rate (how many frames to skip during visualization).
337#[utoipa::path(
338    get,
339    path = "/v1/system/cortical_area_visualization_skip_rate",
340    responses(
341        (status = 200, description = "Skip rate retrieved successfully", body = i32),
342        (status = 500, description = "Internal server error")
343    ),
344    tag = "system"
345)]
346pub async fn get_cortical_area_visualization_skip_rate(
347    State(_state): State<ApiState>,
348) -> ApiResult<Json<i32>> {
349    // TODO: Get from visualization config service
350    // For now return default value
351    Ok(Json(1))
352}
353
354/// Set the visualization skip rate to reduce visualization frequency and improve performance.
355#[utoipa::path(
356    put,
357    path = "/v1/system/cortical_area_visualization_skip_rate",
358    request_body = i32,
359    responses(
360        (status = 200, description = "Skip rate updated successfully"),
361        (status = 500, description = "Internal server error")
362    ),
363    tag = "system"
364)]
365pub async fn set_cortical_area_visualization_skip_rate(
366    State(_state): State<ApiState>,
367    Json(skip_rate): Json<i32>,
368) -> ApiResult<Json<serde_json::Value>> {
369    // TODO: Set in visualization config service
370    Ok(Json(serde_json::json!({
371        "message": format!("Skip rate set to {}", skip_rate)
372    })))
373}
374
375/// Get the threshold below which cortical areas are suppressed from visualization.
376#[utoipa::path(
377    get,
378    path = "/v1/system/cortical_area_visualization_suppression_threshold",
379    responses(
380        (status = 200, description = "Threshold retrieved successfully", body = i32),
381        (status = 500, description = "Internal server error")
382    ),
383    tag = "system"
384)]
385pub async fn get_cortical_area_visualization_suppression_threshold(
386    State(_state): State<ApiState>,
387) -> ApiResult<Json<i32>> {
388    // TODO: Get from visualization config service
389    // For now return default value
390    Ok(Json(0))
391}
392
393/// Set the threshold for suppressing low-activity cortical areas from visualization.
394#[utoipa::path(
395    put,
396    path = "/v1/system/cortical_area_visualization_suppression_threshold",
397    request_body = i32,
398    responses(
399        (status = 200, description = "Threshold updated successfully"),
400        (status = 500, description = "Internal server error")
401    ),
402    tag = "system"
403)]
404pub async fn set_cortical_area_visualization_suppression_threshold(
405    State(_state): State<ApiState>,
406    Json(threshold): Json<i32>,
407) -> ApiResult<Json<serde_json::Value>> {
408    // TODO: Set in visualization config service
409    Ok(Json(serde_json::json!({
410        "message": format!("Suppression threshold set to {}", threshold)
411    })))
412}
413
414// ============================================================================
415// SYSTEM VERSION & INFO ENDPOINTS
416// ============================================================================
417
418/// Get the current FEAGI version string.
419#[utoipa::path(
420    get,
421    path = "/v1/system/version",
422    tag = "system",
423    responses(
424        (status = 200, description = "Version string", body = String)
425    )
426)]
427pub async fn get_version(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
428    Ok(Json(env!("CARGO_PKG_VERSION").to_string()))
429}
430
431/// Get detailed version information for all FEAGI crates and components.
432#[utoipa::path(
433    get,
434    path = "/v1/system/versions",
435    tag = "system",
436    responses(
437        (status = 200, description = "Version information", body = HashMap<String, String>)
438    )
439)]
440pub async fn get_versions(
441    State(state): State<ApiState>,
442) -> ApiResult<Json<HashMap<String, String>>> {
443    // Use system service to get version information
444    // The application (feagi-rust) provides this at startup with all crates it was compiled with
445    match state.system_service.get_version().await {
446        Ok(version_info) => {
447            let mut versions = version_info.crates.clone();
448
449            // Add build metadata
450            versions.insert("rust".to_string(), version_info.rust_version);
451            versions.insert("build_timestamp".to_string(), version_info.build_timestamp);
452
453            Ok(Json(versions))
454        }
455        Err(e) => {
456            // Fallback to minimal version info
457            tracing::warn!(
458                "Failed to get version from system service: {}, using fallback",
459                e
460            );
461            let mut versions = HashMap::new();
462            versions.insert(
463                "error".to_string(),
464                "system service unavailable".to_string(),
465            );
466            Ok(Json(versions))
467        }
468    }
469}
470
471/// Get system configuration including API settings, neuron capacity, and synapse limits.
472#[utoipa::path(
473    get,
474    path = "/v1/system/configuration",
475    tag = "system",
476    responses(
477        (status = 200, description = "System configuration", body = HashMap<String, serde_json::Value>)
478    )
479)]
480pub async fn get_configuration(
481    State(state): State<ApiState>,
482) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
483    // Get actual NPU capacity from analytics service
484    let health = state
485        .analytics_service
486        .get_system_health()
487        .await
488        .map_err(|e| ApiError::internal(format!("Failed to get system health: {}", e)))?;
489
490    let mut config = HashMap::new();
491    config.insert("api_host".to_string(), serde_json::json!("0.0.0.0"));
492    config.insert("api_port".to_string(), serde_json::json!(8000));
493    // Use actual NPU capacity from system health (NOT hardcoded values)
494    config.insert(
495        "max_neurons".to_string(),
496        serde_json::json!(health.neuron_capacity),
497    );
498    config.insert(
499        "max_synapses".to_string(),
500        serde_json::json!(health.synapse_capacity),
501    );
502
503    Ok(Json(config))
504}
505
506/// Get user preferences including advanced mode, UI magnification, and auto-creation settings.
507#[utoipa::path(
508    get,
509    path = "/v1/system/user_preferences",
510    tag = "system",
511    responses(
512        (status = 200, description = "User preferences", body = HashMap<String, serde_json::Value>)
513    )
514)]
515pub async fn get_user_preferences(
516    State(_state): State<ApiState>,
517) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
518    let mut prefs = HashMap::new();
519    prefs.insert("adv_mode".to_string(), serde_json::json!(false));
520    prefs.insert("ui_magnification".to_string(), serde_json::json!(1.0));
521    prefs.insert(
522        "auto_pns_area_creation".to_string(),
523        serde_json::json!(true),
524    );
525
526    Ok(Json(prefs))
527}
528
529/// Update user preferences for UI customization and behavior settings.
530#[utoipa::path(
531    put,
532    path = "/v1/system/user_preferences",
533    tag = "system",
534    responses(
535        (status = 200, description = "Preferences updated", body = HashMap<String, String>)
536    )
537)]
538pub async fn put_user_preferences(
539    State(_state): State<ApiState>,
540    Json(_prefs): Json<HashMap<String, serde_json::Value>>,
541) -> ApiResult<Json<HashMap<String, String>>> {
542    Ok(Json(HashMap::from([(
543        "message".to_string(),
544        "User preferences updated successfully".to_string(),
545    )])))
546}
547
548/// Get list of available cortical area types (Sensory, Motor, Custom, Memory, Core).
549#[utoipa::path(
550    get,
551    path = "/v1/system/cortical_area_types",
552    tag = "system",
553    responses(
554        (status = 200, description = "Cortical area types", body = Vec<String>)
555    )
556)]
557pub async fn get_cortical_area_types_list(
558    State(_state): State<ApiState>,
559) -> ApiResult<Json<Vec<String>>> {
560    Ok(Json(vec![
561        "Sensory".to_string(),
562        "Motor".to_string(),
563        "Custom".to_string(),
564        "Memory".to_string(),
565        "Core".to_string(),
566    ]))
567}
568
569/// Enable the Fire Queue (FQ) sampler for visualization data streaming.
570#[utoipa::path(
571    post,
572    path = "/v1/system/enable_visualization_fq_sampler",
573    tag = "system",
574    responses(
575        (status = 200, description = "FQ sampler enabled", body = HashMap<String, String>)
576    )
577)]
578pub async fn post_enable_visualization_fq_sampler(
579    State(state): State<ApiState>,
580) -> ApiResult<Json<HashMap<String, String>>> {
581    let runtime_service = state.runtime_service.as_ref();
582
583    runtime_service
584        .set_fcl_sampler_config(None, Some(1))
585        .await
586        .map_err(|e| ApiError::internal(format!("Failed to enable FQ sampler: {}", e)))?;
587
588    Ok(Json(HashMap::from([(
589        "message".to_string(),
590        "Visualization FQ sampler enabled".to_string(),
591    )])))
592}
593
594/// Disable the Fire Queue (FQ) sampler to stop visualization data streaming.
595#[utoipa::path(
596    post,
597    path = "/v1/system/disable_visualization_fq_sampler",
598    tag = "system",
599    responses(
600        (status = 200, description = "FQ sampler disabled", body = HashMap<String, String>)
601    )
602)]
603pub async fn post_disable_visualization_fq_sampler(
604    State(state): State<ApiState>,
605) -> ApiResult<Json<HashMap<String, String>>> {
606    let runtime_service = state.runtime_service.as_ref();
607
608    runtime_service
609        .set_fcl_sampler_config(None, Some(0))
610        .await
611        .map_err(|e| ApiError::internal(format!("Failed to disable FQ sampler: {}", e)))?;
612
613    Ok(Json(HashMap::from([(
614        "message".to_string(),
615        "Visualization FQ sampler disabled".to_string(),
616    )])))
617}
618
619/// Get Fire Candidate List (FCL) sampler status including frequency and consumer state.
620#[utoipa::path(
621    get,
622    path = "/v1/system/fcl_status",
623    tag = "system",
624    responses(
625        (status = 200, description = "FCL status", body = HashMap<String, serde_json::Value>)
626    )
627)]
628pub async fn get_fcl_status_system(
629    State(state): State<ApiState>,
630) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
631    let runtime_service = state.runtime_service.as_ref();
632
633    let (frequency, consumer) = runtime_service
634        .get_fcl_sampler_config()
635        .await
636        .map_err(|e| ApiError::internal(format!("Failed to get FCL status: {}", e)))?;
637
638    let mut response = HashMap::new();
639    response.insert("available".to_string(), serde_json::json!(true));
640    response.insert("frequency".to_string(), serde_json::json!(frequency));
641    response.insert("consumer".to_string(), serde_json::json!(consumer));
642    response.insert("enabled".to_string(), serde_json::json!(consumer > 0));
643
644    Ok(Json(response))
645}
646
647/// Reset the Fire Candidate List (FCL) to clear all pending fire candidates.
648#[utoipa::path(
649    post,
650    path = "/v1/system/fcl_reset",
651    tag = "system",
652    responses(
653        (status = 200, description = "FCL reset", body = HashMap<String, String>)
654    )
655)]
656pub async fn post_fcl_reset_system(
657    State(_state): State<ApiState>,
658) -> ApiResult<Json<HashMap<String, String>>> {
659    tracing::info!(target: "feagi-api", "FCL reset requested");
660
661    Ok(Json(HashMap::from([(
662        "message".to_string(),
663        "FCL reset successfully".to_string(),
664    )])))
665}
666
667/// Get status of active system processes including burst engine and API server.
668#[utoipa::path(
669    get,
670    path = "/v1/system/processes",
671    tag = "system",
672    responses(
673        (status = 200, description = "Active processes", body = HashMap<String, serde_json::Value>)
674    )
675)]
676pub async fn get_processes(
677    State(state): State<ApiState>,
678) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
679    let runtime_service = state.runtime_service.as_ref();
680
681    let status = runtime_service
682        .get_status()
683        .await
684        .map_err(|e| ApiError::internal(format!("Failed to get processes: {}", e)))?;
685
686    let mut processes = HashMap::new();
687    processes.insert(
688        "burst_engine".to_string(),
689        serde_json::json!({
690            "active": status.is_running,
691            "paused": status.is_paused
692        }),
693    );
694    processes.insert(
695        "api_server".to_string(),
696        serde_json::json!({"active": true}),
697    );
698
699    Ok(Json(processes))
700}
701
702/// Get collection of unique log messages for debugging and monitoring.
703#[utoipa::path(
704    get,
705    path = "/v1/system/unique_logs",
706    tag = "system",
707    responses(
708        (status = 200, description = "Unique logs", body = HashMap<String, Vec<String>>)
709    )
710)]
711pub async fn get_unique_logs(
712    State(_state): State<ApiState>,
713) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
714    let mut response = HashMap::new();
715    response.insert("logs".to_string(), Vec::new());
716
717    Ok(Json(response))
718}
719
720/// Configure logging settings including log level and output destinations.
721#[utoipa::path(
722    post,
723    path = "/v1/system/logs",
724    tag = "system",
725    responses(
726        (status = 200, description = "Log config updated", body = HashMap<String, String>)
727    )
728)]
729pub async fn post_logs(
730    State(_state): State<ApiState>,
731    Json(_config): Json<HashMap<String, serde_json::Value>>,
732) -> ApiResult<Json<HashMap<String, String>>> {
733    Ok(Json(HashMap::from([(
734        "message".to_string(),
735        "Log configuration updated".to_string(),
736    )])))
737}
738
739/// Get list of all beacon subscribers currently monitoring system events.
740#[utoipa::path(
741    get,
742    path = "/v1/system/beacon/subscribers",
743    tag = "system",
744    responses(
745        (status = 200, description = "Beacon subscribers", body = Vec<String>)
746    )
747)]
748pub async fn get_beacon_subscribers(
749    State(_state): State<ApiState>,
750) -> ApiResult<Json<Vec<String>>> {
751    Ok(Json(Vec::new()))
752}
753
754/// Subscribe to system beacon for event notifications and status updates.
755#[utoipa::path(
756    post,
757    path = "/v1/system/beacon/subscribe",
758    tag = "system",
759    responses(
760        (status = 200, description = "Subscribed", body = HashMap<String, String>)
761    )
762)]
763pub async fn post_beacon_subscribe(
764    State(_state): State<ApiState>,
765    Json(_request): Json<HashMap<String, String>>,
766) -> ApiResult<Json<HashMap<String, String>>> {
767    Ok(Json(HashMap::from([(
768        "message".to_string(),
769        "Subscribed to beacon".to_string(),
770    )])))
771}
772
773/// Unsubscribe from system beacon to stop receiving event notifications.
774#[utoipa::path(
775    delete,
776    path = "/v1/system/beacon/unsubscribe",
777    tag = "system",
778    responses(
779        (status = 200, description = "Unsubscribed", body = HashMap<String, String>)
780    )
781)]
782pub async fn delete_beacon_unsubscribe(
783    State(_state): State<ApiState>,
784    Json(_request): Json<HashMap<String, String>>,
785) -> ApiResult<Json<HashMap<String, String>>> {
786    Ok(Json(HashMap::from([(
787        "message".to_string(),
788        "Unsubscribed from beacon".to_string(),
789    )])))
790}
791
792/// Get global activity visualization configuration including enabled state and frequency.
793#[utoipa::path(
794    get,
795    path = "/v1/system/global_activity_visualization",
796    tag = "system",
797    responses(
798        (status = 200, description = "Global activity viz status", body = HashMap<String, serde_json::Value>)
799    )
800)]
801pub async fn get_global_activity_visualization(
802    State(_state): State<ApiState>,
803) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
804    let mut response = HashMap::new();
805    response.insert("enabled".to_string(), serde_json::json!(false));
806    response.insert("frequency_hz".to_string(), serde_json::json!(30.0));
807
808    Ok(Json(response))
809}
810
811/// Configure global activity visualization settings and frequency.
812#[utoipa::path(
813    put,
814    path = "/v1/system/global_activity_visualization",
815    tag = "system",
816    responses(
817        (status = 200, description = "Configured", body = HashMap<String, String>)
818    )
819)]
820pub async fn put_global_activity_visualization(
821    State(_state): State<ApiState>,
822    Json(_config): Json<HashMap<String, serde_json::Value>>,
823) -> ApiResult<Json<HashMap<String, String>>> {
824    Ok(Json(HashMap::from([(
825        "message".to_string(),
826        "Global activity visualization configured".to_string(),
827    )])))
828}
829
830/// Set the file system path for the circuit library storage location.
831#[utoipa::path(
832    post,
833    path = "/v1/system/circuit_library_path",
834    tag = "system",
835    responses(
836        (status = 200, description = "Path set", body = HashMap<String, String>)
837    )
838)]
839pub async fn post_circuit_library_path(
840    State(_state): State<ApiState>,
841    Json(_request): Json<HashMap<String, String>>,
842) -> ApiResult<Json<HashMap<String, String>>> {
843    Ok(Json(HashMap::from([(
844        "message".to_string(),
845        "Circuit library path updated".to_string(),
846    )])))
847}
848
849/// Test connectivity to InfluxDB database for time-series data storage.
850#[utoipa::path(
851    get,
852    path = "/v1/system/db/influxdb/test",
853    tag = "system",
854    responses(
855        (status = 200, description = "Test result", body = HashMap<String, bool>)
856    )
857)]
858pub async fn get_influxdb_test(
859    State(_state): State<ApiState>,
860) -> ApiResult<Json<HashMap<String, bool>>> {
861    let mut response = HashMap::new();
862    response.insert("connected".to_string(), false);
863    response.insert("available".to_string(), false);
864
865    Ok(Json(response))
866}
867
868/// Register a new system component or module with FEAGI.
869#[utoipa::path(
870    post,
871    path = "/v1/system/register",
872    tag = "system",
873    responses(
874        (status = 200, description = "Registered", body = HashMap<String, String>)
875    )
876)]
877pub async fn post_register_system(
878    State(_state): State<ApiState>,
879    Json(_request): Json<HashMap<String, serde_json::Value>>,
880) -> ApiResult<Json<HashMap<String, String>>> {
881    Ok(Json(HashMap::from([(
882        "message".to_string(),
883        "System component registered".to_string(),
884    )])))
885}