Skip to main content

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