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