systemprompt_api/routes/admin/services/
mod.rs1mod refresh;
19mod status;
20
21use std::sync::Arc;
22
23use axum::routing::{get, post};
24use axum::{Extension, Router};
25use chrono::{DateTime, Utc};
26use serde::{Deserialize, Serialize};
27use systemprompt_loader::services_root::{ActiveServicesRoot, ServicesProvenance};
28use systemprompt_models::services::bundle::ServicesBundleState;
29use systemprompt_runtime::AppContext;
30
31pub use refresh::{RefreshQuery, refresh};
32pub use status::build_status;
33
34pub(super) fn router() -> Router<AppContext> {
35 Router::new()
36 .route("/status", get(status::status))
37 .route("/refresh", post(refresh))
38 .layer(Extension(RefreshLock::default()))
39}
40
41#[derive(Debug, Clone, Default)]
47pub struct RefreshLock(Arc<tokio::sync::Mutex<()>>);
48
49impl RefreshLock {
50 pub fn try_acquire(&self) -> Option<tokio::sync::OwnedMutexGuard<()>> {
51 Arc::clone(&self.0).try_lock_owned().ok()
52 }
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56pub struct SourceView {
57 pub name: String,
58 pub digest: String,
59 pub version: String,
60 pub content_hash: String,
61 pub fetched_at: DateTime<Utc>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub struct ProvenanceView {
66 pub kind: String,
67
68 #[serde(skip_serializing_if = "Option::is_none")]
69 pub composed_hash: Option<String>,
70
71 #[serde(skip_serializing_if = "Option::is_none")]
72 pub error: Option<String>,
73}
74
75#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
76pub struct ServicesStatusResponse {
77 pub active_root: String,
78 pub provenance: ProvenanceView,
79 pub sources: Vec<SourceView>,
80
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub last_reconciled_hash: Option<String>,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
86pub struct ServicesRefreshResponse {
87 pub changed: bool,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
90 pub composed_hash: Option<String>,
91
92 pub sources: Vec<SourceView>,
93 pub restarting: bool,
94}
95
96pub fn source_views(state: &ServicesBundleState) -> Vec<SourceView> {
97 state
98 .sources
99 .iter()
100 .map(|(name, s)| SourceView {
101 name: name.clone(),
102 digest: s.digest.clone(),
103 version: s.version.clone(),
104 content_hash: s.content_hash.clone(),
105 fetched_at: s.fetched_at,
106 })
107 .collect()
108}
109
110pub fn provenance_view(provenance: &ServicesProvenance) -> ProvenanceView {
111 match provenance {
112 ServicesProvenance::Bundled => ProvenanceView {
113 kind: "bundled".to_owned(),
114 composed_hash: None,
115 error: None,
116 },
117 ServicesProvenance::Fetched { composed_hash, .. } => ProvenanceView {
118 kind: "fetched".to_owned(),
119 composed_hash: Some(composed_hash.clone()),
120 error: None,
121 },
122 ServicesProvenance::LastGood {
123 composed_hash,
124 error,
125 } => ProvenanceView {
126 kind: "last_good".to_owned(),
127 composed_hash: Some(composed_hash.clone()),
128 error: Some(error.clone()),
129 },
130 ServicesProvenance::BundledFallback { error } => ProvenanceView {
131 kind: "bundled".to_owned(),
132 composed_hash: None,
133 error: Some(error.clone()),
134 },
135 }
136}
137
138pub const fn composed_hash_of(active: &ActiveServicesRoot) -> Option<&str> {
139 match &active.provenance {
140 ServicesProvenance::Fetched { composed_hash, .. }
141 | ServicesProvenance::LastGood { composed_hash, .. } => Some(composed_hash.as_str()),
142 ServicesProvenance::Bundled | ServicesProvenance::BundledFallback { .. } => None,
143 }
144}