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    // Real genome validity comes from the chain report, written by the
190    // loader into core state. Falls back to `None` until a genome has
191    // been loaded in this process. See `SystemHealth::genome_validity`.
192    let genome_validity = health.genome_validity;
193
194    // Get FEAGI session timestamp (unique identifier for this FEAGI instance)
195    let feagi_session = Some(state.feagi_session_timestamp);
196
197    // Fields requiring future service implementations
198    let fitness = None; // TODO: Get from evolution service
199
200    // Get memory area stats from plasticity service cache (event-driven updates).
201    //
202    // IMPORTANT: BV expects both:
203    // - per-area stats keyed by cortical_id (base64), and
204    // - a global `memory_neuron_count` that matches the sum of per-area `neuron_count`.
205    let (memory_area_stats, memory_neuron_count_from_cache) = state
206        .memory_stats_cache
207        .as_ref()
208        .map(|cache| {
209            let snapshot = feagi_npu_plasticity::memory_stats_cache::get_stats_snapshot(cache);
210            let total = snapshot
211                .values()
212                .map(|s| s.neuron_count as i64)
213                .sum::<i64>();
214            let per_area = snapshot
215                .into_iter()
216                .map(|(name, stats)| {
217                    let mut inner_map = HashMap::new();
218                    inner_map.insert(
219                        "neuron_count".to_string(),
220                        serde_json::json!(stats.neuron_count),
221                    );
222                    inner_map.insert(
223                        "created_total".to_string(),
224                        serde_json::json!(stats.created_total),
225                    );
226                    inner_map.insert(
227                        "deleted_total".to_string(),
228                        serde_json::json!(stats.deleted_total),
229                    );
230                    inner_map.insert(
231                        "last_updated".to_string(),
232                        serde_json::json!(stats.last_updated),
233                    );
234                    (name, inner_map)
235                })
236                .collect::<HashMap<String, HashMap<String, serde_json::Value>>>();
237            (Some(per_area), Some(total))
238        })
239        .unwrap_or((None, None));
240
241    // Prefer the plasticity cache-derived total to avoid discrepancies.
242    let memory_neuron_count = memory_neuron_count_from_cache.or(memory_neuron_count);
243
244    // BV expects `amalgamation_pending` to be a dict with:
245    // - amalgamation_id
246    // - genome_title
247    // - circuit_size
248    let amalgamation_pending = state.amalgamation_state.read().pending.as_ref().map(|p| {
249        let v = amalgamation::pending_summary_to_health_json(&p.summary);
250        v.as_object()
251            .cloned()
252            .unwrap_or_default()
253            .into_iter()
254            .collect::<HashMap<String, serde_json::Value>>()
255    });
256
257    // Get root region ID from ConnectomeManager (only available when services feature is enabled)
258    #[cfg(feature = "services")]
259    let brain_regions_root = feagi_brain_development::ConnectomeManager::instance()
260        .read()
261        .get_root_region_id();
262    #[cfg(not(feature = "services"))]
263    let brain_regions_root = None; // WASM: Use connectome service instead
264
265    // Get fatigue information from state manager
266    // Note: feagi-state-manager is included in the "services" feature
267    #[cfg(feature = "services")]
268    let fatigue = {
269        use feagi_state_manager::StateManager;
270        // Initialize singleton on first access (Lazy will handle this)
271        match StateManager::instance().try_read() {
272            Some(state_manager) => {
273                let core_state = state_manager.get_core_state();
274                Some(FatigueInfo {
275                    fatigue_index: Some(core_state.get_fatigue_index()),
276                    fatigue_active: Some(core_state.is_fatigue_active()),
277                    regular_neuron_util: Some(core_state.get_regular_neuron_util()),
278                    memory_neuron_util: Some(core_state.get_memory_neuron_util()),
279                    synapse_util: Some(core_state.get_synapse_util()),
280                })
281            }
282            None => {
283                // State manager is locked, return None (shouldn't happen in normal operation)
284                tracing::warn!(target: "feagi-api", "StateManager is locked, cannot read fatigue data");
285                None
286            }
287        }
288    };
289    #[cfg(not(feature = "services"))]
290    let fatigue = {
291        tracing::debug!(target: "feagi-api", "Services feature not enabled, fatigue data unavailable");
292        None
293    };
294
295    let (
296        brain_regions_hash,
297        cortical_areas_hash,
298        brain_geometry_hash,
299        morphologies_hash,
300        cortical_mappings_hash,
301        agent_data_hash,
302        genome_loading,
303        genome_state,
304    ) = {
305        use feagi_state_manager::GenomeState;
306        let state_manager = feagi_state_manager::StateManager::instance();
307        let state_manager = state_manager.read();
308        let gs = state_manager.get_genome_state();
309        let genome_state = match gs {
310            GenomeState::Missing => "missing",
311            GenomeState::Loading => "loading",
312            GenomeState::Loaded => "loaded",
313            GenomeState::Saving => "saving",
314            GenomeState::Error => "error",
315        }
316        .to_string();
317        let genome_loading = gs == GenomeState::Loading;
318        (
319            Some(state_manager.get_brain_regions_hash()),
320            Some(state_manager.get_cortical_areas_hash()),
321            Some(state_manager.get_brain_geometry_hash()),
322            Some(state_manager.get_morphologies_hash()),
323            Some(state_manager.get_cortical_mappings_hash()),
324            Some(state_manager.get_agent_data_hash()),
325            genome_loading,
326            genome_state,
327        )
328    };
329
330    Ok(Json(HealthCheckResponse {
331        burst_engine: burst_engine_active,
332        connected_agents,
333        influxdb_availability,
334        neuron_count_max,
335        synapse_count_max,
336        latest_changes_saved_externally,
337        genome_availability,
338        genome_validity,
339        brain_readiness: health.brain_readiness,
340        genome_loading,
341        genome_state,
342        feagi_session,
343        fitness,
344        cortical_area_count: Some(health.cortical_area_count as i32),
345        neuron_count: Some(health.neuron_count as i64),
346        memory_neuron_count,
347        regular_neuron_count,
348        synapse_count,
349        estimated_brain_size_in_MB,
350        genome_num,
351        genome_timestamp,
352        simulation_timestep,
353        memory_area_stats,
354        amalgamation_pending,
355        brain_regions_hash,
356        cortical_areas_hash,
357        brain_geometry_hash,
358        morphologies_hash,
359        cortical_mappings_hash,
360        agent_data_hash,
361        brain_regions_root, // NEW: Root region ID for O(1) lookup
362        fatigue,
363    }))
364}
365
366/// Get the visualization skip rate (how many frames to skip during visualization).
367#[utoipa::path(
368    get,
369    path = "/v1/system/cortical_area_visualization_skip_rate",
370    responses(
371        (status = 200, description = "Skip rate retrieved successfully", body = i32),
372        (status = 500, description = "Internal server error")
373    ),
374    tag = "system"
375)]
376pub async fn get_cortical_area_visualization_skip_rate(
377    State(_state): State<ApiState>,
378) -> ApiResult<Json<i32>> {
379    // TODO: Get from visualization config service
380    // For now return default value
381    Ok(Json(1))
382}
383
384/// Set the visualization skip rate to reduce visualization frequency and improve performance.
385#[utoipa::path(
386    put,
387    path = "/v1/system/cortical_area_visualization_skip_rate",
388    request_body = i32,
389    responses(
390        (status = 200, description = "Skip rate updated successfully"),
391        (status = 500, description = "Internal server error")
392    ),
393    tag = "system"
394)]
395pub async fn set_cortical_area_visualization_skip_rate(
396    State(_state): State<ApiState>,
397    Json(skip_rate): Json<i32>,
398) -> ApiResult<Json<serde_json::Value>> {
399    // TODO: Set in visualization config service
400    Ok(Json(serde_json::json!({
401        "message": format!("Skip rate set to {}", skip_rate)
402    })))
403}
404
405/// Get the threshold below which cortical areas are suppressed from visualization.
406#[utoipa::path(
407    get,
408    path = "/v1/system/cortical_area_visualization_suppression_threshold",
409    responses(
410        (status = 200, description = "Threshold retrieved successfully", body = i32),
411        (status = 500, description = "Internal server error")
412    ),
413    tag = "system"
414)]
415pub async fn get_cortical_area_visualization_suppression_threshold(
416    State(_state): State<ApiState>,
417) -> ApiResult<Json<i32>> {
418    // TODO: Get from visualization config service
419    // For now return default value
420    Ok(Json(0))
421}
422
423/// Set the threshold for suppressing low-activity cortical areas from visualization.
424#[utoipa::path(
425    put,
426    path = "/v1/system/cortical_area_visualization_suppression_threshold",
427    request_body = i32,
428    responses(
429        (status = 200, description = "Threshold updated successfully"),
430        (status = 500, description = "Internal server error")
431    ),
432    tag = "system"
433)]
434pub async fn set_cortical_area_visualization_suppression_threshold(
435    State(_state): State<ApiState>,
436    Json(threshold): Json<i32>,
437) -> ApiResult<Json<serde_json::Value>> {
438    // TODO: Set in visualization config service
439    Ok(Json(serde_json::json!({
440        "message": format!("Suppression threshold set to {}", threshold)
441    })))
442}
443
444// ============================================================================
445// SYSTEM VERSION & INFO ENDPOINTS
446// ============================================================================
447
448/// Get the current FEAGI version string.
449#[utoipa::path(
450    get,
451    path = "/v1/system/version",
452    tag = "system",
453    responses(
454        (status = 200, description = "Version string", body = String)
455    )
456)]
457pub async fn get_version(State(_state): State<ApiState>) -> ApiResult<Json<String>> {
458    Ok(Json(env!("CARGO_PKG_VERSION").to_string()))
459}
460
461/// Get detailed version information for all FEAGI crates and components.
462#[utoipa::path(
463    get,
464    path = "/v1/system/versions",
465    tag = "system",
466    responses(
467        (status = 200, description = "Version information", body = HashMap<String, String>)
468    )
469)]
470pub async fn get_versions(
471    State(state): State<ApiState>,
472) -> ApiResult<Json<HashMap<String, String>>> {
473    // Use system service to get version information
474    // The application (feagi-rust) provides this at startup with all crates it was compiled with
475    match state.system_service.get_version().await {
476        Ok(version_info) => {
477            let mut versions = version_info.crates.clone();
478
479            // Add build metadata
480            versions.insert("rust".to_string(), version_info.rust_version);
481            versions.insert("build_timestamp".to_string(), version_info.build_timestamp);
482
483            Ok(Json(versions))
484        }
485        Err(e) => {
486            // Fallback to minimal version info
487            tracing::warn!(
488                "Failed to get version from system service: {}, using fallback",
489                e
490            );
491            let mut versions = HashMap::new();
492            versions.insert(
493                "error".to_string(),
494                "system service unavailable".to_string(),
495            );
496            Ok(Json(versions))
497        }
498    }
499}
500
501/// Get system configuration including API settings, neuron capacity, and synapse limits.
502#[utoipa::path(
503    get,
504    path = "/v1/system/configuration",
505    tag = "system",
506    responses(
507        (status = 200, description = "System configuration", body = HashMap<String, serde_json::Value>)
508    )
509)]
510pub async fn get_configuration(
511    State(state): State<ApiState>,
512) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
513    // Get actual NPU capacity from analytics service
514    let health = state
515        .analytics_service
516        .get_system_health()
517        .await
518        .map_err(|e| ApiError::internal(format!("Failed to get system health: {}", e)))?;
519
520    let mut config = HashMap::new();
521    config.insert("api_host".to_string(), serde_json::json!("0.0.0.0"));
522    config.insert("api_port".to_string(), serde_json::json!(8000));
523    // Use actual NPU capacity from system health (NOT hardcoded values)
524    config.insert(
525        "max_neurons".to_string(),
526        serde_json::json!(health.neuron_capacity),
527    );
528    config.insert(
529        "max_synapses".to_string(),
530        serde_json::json!(health.synapse_capacity),
531    );
532
533    Ok(Json(config))
534}
535
536/// Get user preferences including advanced mode, UI magnification, and auto-creation settings.
537#[utoipa::path(
538    get,
539    path = "/v1/system/user_preferences",
540    tag = "system",
541    responses(
542        (status = 200, description = "User preferences", body = HashMap<String, serde_json::Value>)
543    )
544)]
545pub async fn get_user_preferences(
546    State(_state): State<ApiState>,
547) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
548    let mut prefs = HashMap::new();
549    prefs.insert("adv_mode".to_string(), serde_json::json!(false));
550    prefs.insert("ui_magnification".to_string(), serde_json::json!(1.0));
551    prefs.insert(
552        "auto_pns_area_creation".to_string(),
553        serde_json::json!(true),
554    );
555
556    Ok(Json(prefs))
557}
558
559/// Update user preferences for UI customization and behavior settings.
560#[utoipa::path(
561    put,
562    path = "/v1/system/user_preferences",
563    tag = "system",
564    responses(
565        (status = 200, description = "Preferences updated", body = HashMap<String, String>)
566    )
567)]
568pub async fn put_user_preferences(
569    State(_state): State<ApiState>,
570    Json(_prefs): Json<HashMap<String, serde_json::Value>>,
571) -> ApiResult<Json<HashMap<String, String>>> {
572    Ok(Json(HashMap::from([(
573        "message".to_string(),
574        "User preferences updated successfully".to_string(),
575    )])))
576}
577
578/// Get list of available cortical area types (Sensory, Motor, Custom, Memory, Core).
579#[utoipa::path(
580    get,
581    path = "/v1/system/cortical_area_types",
582    tag = "system",
583    responses(
584        (status = 200, description = "Cortical area types", body = Vec<String>)
585    )
586)]
587pub async fn get_cortical_area_types_list(
588    State(_state): State<ApiState>,
589) -> ApiResult<Json<Vec<String>>> {
590    Ok(Json(vec![
591        "Sensory".to_string(),
592        "Motor".to_string(),
593        "Custom".to_string(),
594        "Memory".to_string(),
595        "Core".to_string(),
596    ]))
597}
598
599/// Enable the Fire Queue (FQ) sampler for visualization data streaming.
600#[utoipa::path(
601    post,
602    path = "/v1/system/enable_visualization_fq_sampler",
603    tag = "system",
604    responses(
605        (status = 200, description = "FQ sampler enabled", body = HashMap<String, String>)
606    )
607)]
608pub async fn post_enable_visualization_fq_sampler(
609    State(state): State<ApiState>,
610) -> ApiResult<Json<HashMap<String, String>>> {
611    let runtime_service = state.runtime_service.as_ref();
612
613    runtime_service
614        .set_fcl_sampler_config(None, Some(1))
615        .await
616        .map_err(|e| ApiError::internal(format!("Failed to enable FQ sampler: {}", e)))?;
617
618    Ok(Json(HashMap::from([(
619        "message".to_string(),
620        "Visualization FQ sampler enabled".to_string(),
621    )])))
622}
623
624/// Disable the Fire Queue (FQ) sampler to stop visualization data streaming.
625#[utoipa::path(
626    post,
627    path = "/v1/system/disable_visualization_fq_sampler",
628    tag = "system",
629    responses(
630        (status = 200, description = "FQ sampler disabled", body = HashMap<String, String>)
631    )
632)]
633pub async fn post_disable_visualization_fq_sampler(
634    State(state): State<ApiState>,
635) -> ApiResult<Json<HashMap<String, String>>> {
636    let runtime_service = state.runtime_service.as_ref();
637
638    runtime_service
639        .set_fcl_sampler_config(None, Some(0))
640        .await
641        .map_err(|e| ApiError::internal(format!("Failed to disable FQ sampler: {}", e)))?;
642
643    Ok(Json(HashMap::from([(
644        "message".to_string(),
645        "Visualization FQ sampler disabled".to_string(),
646    )])))
647}
648
649/// Get Fire Candidate List (FCL) sampler status including frequency and consumer state.
650#[utoipa::path(
651    get,
652    path = "/v1/system/fcl_status",
653    tag = "system",
654    responses(
655        (status = 200, description = "FCL status", body = HashMap<String, serde_json::Value>)
656    )
657)]
658pub async fn get_fcl_status_system(
659    State(state): State<ApiState>,
660) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
661    let runtime_service = state.runtime_service.as_ref();
662
663    let (frequency, consumer) = runtime_service
664        .get_fcl_sampler_config()
665        .await
666        .map_err(|e| ApiError::internal(format!("Failed to get FCL status: {}", e)))?;
667
668    let mut response = HashMap::new();
669    response.insert("available".to_string(), serde_json::json!(true));
670    response.insert("frequency".to_string(), serde_json::json!(frequency));
671    response.insert("consumer".to_string(), serde_json::json!(consumer));
672    response.insert("enabled".to_string(), serde_json::json!(consumer > 0));
673
674    Ok(Json(response))
675}
676
677/// Reset the Fire Candidate List (FCL) to clear all pending fire candidates.
678#[utoipa::path(
679    post,
680    path = "/v1/system/fcl_reset",
681    tag = "system",
682    responses(
683        (status = 200, description = "FCL reset", body = HashMap<String, String>)
684    )
685)]
686pub async fn post_fcl_reset_system(
687    State(_state): State<ApiState>,
688) -> ApiResult<Json<HashMap<String, String>>> {
689    tracing::info!(target: "feagi-api", "FCL reset requested");
690
691    Ok(Json(HashMap::from([(
692        "message".to_string(),
693        "FCL reset successfully".to_string(),
694    )])))
695}
696
697/// Get status of active system processes including burst engine and API server.
698#[utoipa::path(
699    get,
700    path = "/v1/system/processes",
701    tag = "system",
702    responses(
703        (status = 200, description = "Active processes", body = HashMap<String, serde_json::Value>)
704    )
705)]
706pub async fn get_processes(
707    State(state): State<ApiState>,
708) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
709    let runtime_service = state.runtime_service.as_ref();
710
711    let status = runtime_service
712        .get_status()
713        .await
714        .map_err(|e| ApiError::internal(format!("Failed to get processes: {}", e)))?;
715
716    let mut processes = HashMap::new();
717    processes.insert(
718        "burst_engine".to_string(),
719        serde_json::json!({
720            "active": status.is_running,
721            "paused": status.is_paused
722        }),
723    );
724    processes.insert(
725        "api_server".to_string(),
726        serde_json::json!({"active": true}),
727    );
728
729    Ok(Json(processes))
730}
731
732/// Get collection of unique log messages for debugging and monitoring.
733#[utoipa::path(
734    get,
735    path = "/v1/system/unique_logs",
736    tag = "system",
737    responses(
738        (status = 200, description = "Unique logs", body = HashMap<String, Vec<String>>)
739    )
740)]
741pub async fn get_unique_logs(
742    State(_state): State<ApiState>,
743) -> ApiResult<Json<HashMap<String, Vec<String>>>> {
744    let mut response = HashMap::new();
745    response.insert("logs".to_string(), Vec::new());
746
747    Ok(Json(response))
748}
749
750// ============================================================================
751// LOG TAIL (in-process tracing ring buffer)
752// ============================================================================
753
754/// Single log record returned from the ring buffer.
755#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
756pub struct LogTailRecord {
757    pub timestamp_ms: i64,
758    pub level: String,
759    pub target: String,
760    #[serde(skip_serializing_if = "String::is_empty")]
761    pub file: String,
762    pub line: u32,
763    pub message: String,
764    #[serde(skip_serializing_if = "Option::is_none")]
765    pub fields: Option<serde_json::Value>,
766}
767
768/// Response payload for `GET /v1/system/log_tail`.
769#[derive(Debug, Clone, Serialize, utoipa::ToSchema)]
770pub struct LogTailResponse {
771    /// True when the in-process log ring buffer is enabled. False indicates the
772    /// `FEAGI_LOG_RING_BUFFER_CAPACITY` environment variable is set to 0 or the
773    /// initialiser has not yet run.
774    pub enabled: bool,
775    /// Configured ring buffer capacity (records).
776    pub capacity: usize,
777    /// Records that matched the supplied filters, oldest first.
778    pub records: Vec<LogTailRecord>,
779    /// Number of records returned (matches `records.len()`).
780    pub returned: usize,
781}
782
783/// Get a tail of recent log messages from FEAGI's in-process tracing ring buffer.
784///
785/// Filters (all optional, AND'd together):
786/// * `since_ts_ms` - only records with `timestamp_ms >= since_ts_ms`
787/// * `level`       - minimum severity (`TRACE` < `DEBUG` < `INFO` < `WARN` < `ERROR`)
788/// * `target_prefix` - only records whose tracing target starts with this prefix
789/// * `limit`       - cap on returned record count (most recent records win)
790///
791/// The buffer is bounded; old records are dropped when capacity is exceeded.
792/// Capacity is controlled by `FEAGI_LOG_RING_BUFFER_CAPACITY` (default 2000,
793/// set to 0 to disable).
794#[utoipa::path(
795    get,
796    path = "/v1/system/log_tail",
797    tag = "system",
798    params(
799        ("since_ts_ms" = Option<i64>, Query, description = "Only return records emitted at or after this Unix timestamp (ms)"),
800        ("level" = Option<String>, Query, description = "Minimum severity (TRACE/DEBUG/INFO/WARN/ERROR)"),
801        ("target_prefix" = Option<String>, Query, description = "Restrict to tracing targets starting with this prefix"),
802        ("limit" = Option<usize>, Query, description = "Maximum number of records to return")
803    ),
804    responses(
805        (status = 200, description = "Recent log records", body = LogTailResponse)
806    )
807)]
808pub async fn get_log_tail(
809    State(_state): State<ApiState>,
810    axum::extract::Query(query): axum::extract::Query<HashMap<String, String>>,
811) -> ApiResult<Json<LogTailResponse>> {
812    let since_ts_ms = query.get("since_ts_ms").and_then(|v| v.parse::<i64>().ok());
813    let level = query.get("level").map(|v| v.as_str());
814    let target_prefix = query.get("target_prefix").map(|v| v.as_str());
815    let limit = query.get("limit").and_then(|v| v.parse::<usize>().ok());
816
817    let Some(ring) = feagi_observability::global_ring() else {
818        return Ok(Json(LogTailResponse {
819            enabled: false,
820            capacity: 0,
821            records: Vec::new(),
822            returned: 0,
823        }));
824    };
825
826    let snap = ring.snapshot(since_ts_ms, level, target_prefix, limit);
827    let records: Vec<LogTailRecord> = snap
828        .into_iter()
829        .map(|r| LogTailRecord {
830            timestamp_ms: r.timestamp_ms,
831            level: r.level,
832            target: r.target,
833            file: r.file,
834            line: r.line,
835            message: r.message,
836            fields: r.fields,
837        })
838        .collect();
839    let returned = records.len();
840
841    Ok(Json(LogTailResponse {
842        enabled: true,
843        capacity: ring.capacity(),
844        records,
845        returned,
846    }))
847}
848
849/// Configure logging settings including log level and output destinations.
850#[utoipa::path(
851    post,
852    path = "/v1/system/logs",
853    tag = "system",
854    responses(
855        (status = 200, description = "Log config updated", body = HashMap<String, String>)
856    )
857)]
858pub async fn post_logs(
859    State(_state): State<ApiState>,
860    Json(_config): Json<HashMap<String, serde_json::Value>>,
861) -> ApiResult<Json<HashMap<String, String>>> {
862    Ok(Json(HashMap::from([(
863        "message".to_string(),
864        "Log configuration updated".to_string(),
865    )])))
866}
867
868/// Get list of all beacon subscribers currently monitoring system events.
869#[utoipa::path(
870    get,
871    path = "/v1/system/beacon/subscribers",
872    tag = "system",
873    responses(
874        (status = 200, description = "Beacon subscribers", body = Vec<String>)
875    )
876)]
877pub async fn get_beacon_subscribers(
878    State(_state): State<ApiState>,
879) -> ApiResult<Json<Vec<String>>> {
880    Ok(Json(Vec::new()))
881}
882
883/// Subscribe to system beacon for event notifications and status updates.
884#[utoipa::path(
885    post,
886    path = "/v1/system/beacon/subscribe",
887    tag = "system",
888    responses(
889        (status = 200, description = "Subscribed", body = HashMap<String, String>)
890    )
891)]
892pub async fn post_beacon_subscribe(
893    State(_state): State<ApiState>,
894    Json(_request): Json<HashMap<String, String>>,
895) -> ApiResult<Json<HashMap<String, String>>> {
896    Ok(Json(HashMap::from([(
897        "message".to_string(),
898        "Subscribed to beacon".to_string(),
899    )])))
900}
901
902/// Unsubscribe from system beacon to stop receiving event notifications.
903#[utoipa::path(
904    delete,
905    path = "/v1/system/beacon/unsubscribe",
906    tag = "system",
907    responses(
908        (status = 200, description = "Unsubscribed", body = HashMap<String, String>)
909    )
910)]
911pub async fn delete_beacon_unsubscribe(
912    State(_state): State<ApiState>,
913    Json(_request): Json<HashMap<String, String>>,
914) -> ApiResult<Json<HashMap<String, String>>> {
915    Ok(Json(HashMap::from([(
916        "message".to_string(),
917        "Unsubscribed from beacon".to_string(),
918    )])))
919}
920
921/// Get global activity visualization configuration including enabled state and frequency.
922#[utoipa::path(
923    get,
924    path = "/v1/system/global_activity_visualization",
925    tag = "system",
926    responses(
927        (status = 200, description = "Global activity viz status", body = HashMap<String, serde_json::Value>)
928    )
929)]
930pub async fn get_global_activity_visualization(
931    State(_state): State<ApiState>,
932) -> ApiResult<Json<HashMap<String, serde_json::Value>>> {
933    let mut response = HashMap::new();
934    response.insert("enabled".to_string(), serde_json::json!(false));
935    response.insert("frequency_hz".to_string(), serde_json::json!(30.0));
936
937    Ok(Json(response))
938}
939
940/// Configure global activity visualization settings and frequency.
941#[utoipa::path(
942    put,
943    path = "/v1/system/global_activity_visualization",
944    tag = "system",
945    responses(
946        (status = 200, description = "Configured", body = HashMap<String, String>)
947    )
948)]
949pub async fn put_global_activity_visualization(
950    State(_state): State<ApiState>,
951    Json(_config): Json<HashMap<String, serde_json::Value>>,
952) -> ApiResult<Json<HashMap<String, String>>> {
953    Ok(Json(HashMap::from([(
954        "message".to_string(),
955        "Global activity visualization configured".to_string(),
956    )])))
957}
958
959/// Set the file system path for the circuit library storage location.
960#[utoipa::path(
961    post,
962    path = "/v1/system/circuit_library_path",
963    tag = "system",
964    responses(
965        (status = 200, description = "Path set", body = HashMap<String, String>)
966    )
967)]
968pub async fn post_circuit_library_path(
969    State(_state): State<ApiState>,
970    Json(_request): Json<HashMap<String, String>>,
971) -> ApiResult<Json<HashMap<String, String>>> {
972    Ok(Json(HashMap::from([(
973        "message".to_string(),
974        "Circuit library path updated".to_string(),
975    )])))
976}
977
978/// Test connectivity to InfluxDB database for time-series data storage.
979#[utoipa::path(
980    get,
981    path = "/v1/system/db/influxdb/test",
982    tag = "system",
983    responses(
984        (status = 200, description = "Test result", body = HashMap<String, bool>)
985    )
986)]
987pub async fn get_influxdb_test(
988    State(_state): State<ApiState>,
989) -> ApiResult<Json<HashMap<String, bool>>> {
990    let mut response = HashMap::new();
991    response.insert("connected".to_string(), false);
992    response.insert("available".to_string(), false);
993
994    Ok(Json(response))
995}
996
997/// Register a new system component or module with FEAGI.
998#[utoipa::path(
999    post,
1000    path = "/v1/system/register",
1001    tag = "system",
1002    responses(
1003        (status = 200, description = "Registered", body = HashMap<String, String>)
1004    )
1005)]
1006pub async fn post_register_system(
1007    State(_state): State<ApiState>,
1008    Json(_request): Json<HashMap<String, serde_json::Value>>,
1009) -> ApiResult<Json<HashMap<String, String>>> {
1010    Ok(Json(HashMap::from([(
1011        "message".to_string(),
1012        "System component registered".to_string(),
1013    )])))
1014}