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 reconciled: bool,
94 pub restart_recommended: bool,
95 pub restarting: bool,
96}
97
98pub fn source_views(state: &ServicesBundleState) -> Vec<SourceView> {
99 state
100 .sources
101 .iter()
102 .map(|(name, s)| SourceView {
103 name: name.clone(),
104 digest: s.digest.clone(),
105 version: s.version.clone(),
106 content_hash: s.content_hash.clone(),
107 fetched_at: s.fetched_at,
108 })
109 .collect()
110}
111
112pub fn provenance_view(provenance: &ServicesProvenance) -> ProvenanceView {
113 match provenance {
114 ServicesProvenance::Bundled => ProvenanceView {
115 kind: "bundled".to_owned(),
116 composed_hash: None,
117 error: None,
118 },
119 ServicesProvenance::Fetched { composed_hash, .. } => ProvenanceView {
120 kind: "fetched".to_owned(),
121 composed_hash: Some(composed_hash.clone()),
122 error: None,
123 },
124 ServicesProvenance::LastGood {
125 composed_hash,
126 error,
127 } => ProvenanceView {
128 kind: "last_good".to_owned(),
129 composed_hash: Some(composed_hash.clone()),
130 error: Some(error.clone()),
131 },
132 ServicesProvenance::BundledFallback { error } => ProvenanceView {
133 kind: "bundled".to_owned(),
134 composed_hash: None,
135 error: Some(error.clone()),
136 },
137 }
138}
139
140pub const fn composed_hash_of(active: &ActiveServicesRoot) -> Option<&str> {
141 match &active.provenance {
142 ServicesProvenance::Fetched { composed_hash, .. }
143 | ServicesProvenance::LastGood { composed_hash, .. } => Some(composed_hash.as_str()),
144 ServicesProvenance::Bundled | ServicesProvenance::BundledFallback { .. } => None,
145 }
146}