fastskill_core/http/handlers/
status.rs1use crate::core::service::FastSkillService;
4use crate::http::errors::HttpResult;
5use crate::http::models::{ApiResponse, StatusResponse};
6use axum::{extract::State, response::Html};
7use std::sync::Arc;
8use std::time::SystemTime;
9
10#[derive(Clone)]
12pub struct AppState {
13 pub service: Arc<FastSkillService>,
14 pub start_time: SystemTime,
15 pub project_file_path: std::path::PathBuf,
16 pub project_root: std::path::PathBuf,
17 pub skills_directory: std::path::PathBuf,
18}
19
20impl AppState {
21 pub fn new(service: Arc<FastSkillService>) -> Result<Self, Box<dyn std::error::Error>> {
22 Ok(Self {
23 service,
24 start_time: SystemTime::now(),
25 project_file_path: std::path::PathBuf::from("skill-project.toml"),
26 project_root: std::path::PathBuf::from("."),
27 skills_directory: std::path::PathBuf::from(".claude/skills"),
28 })
29 }
30
31 pub fn with_project_file_path(mut self, path: std::path::PathBuf) -> Self {
32 self.project_file_path = path;
33 self
34 }
35
36 pub fn with_project_config(
37 mut self,
38 project_root: std::path::PathBuf,
39 project_file_path: std::path::PathBuf,
40 skills_directory: std::path::PathBuf,
41 ) -> Self {
42 self.project_root = Self::canonicalize_path(project_root);
43 self.project_file_path = Self::canonicalize_path(project_file_path);
44 self.skills_directory = Self::canonicalize_path(skills_directory);
45 self
46 }
47
48 fn canonicalize_path(path: std::path::PathBuf) -> std::path::PathBuf {
50 path.canonicalize().unwrap_or(path)
51 }
52
53 pub fn uptime_seconds(&self) -> u64 {
54 SystemTime::now()
55 .duration_since(self.start_time)
56 .unwrap_or_default()
57 .as_secs()
58 }
59}
60
61pub async fn root(State(state): State<AppState>) -> Html<String> {
63 let skills: Vec<_> =
64 (state.service.skill_manager().list_skills(None).await).unwrap_or_default();
65
66 let skills_count = skills.len();
67 let uptime = state.uptime_seconds();
68
69 let skills_html = if skills.is_empty() {
70 "<li>No skills found</li>".to_string()
71 } else {
72 skills
73 .iter()
74 .take(10) .map(|skill| {
76 let name = &skill.name;
77 let desc = &skill.description;
78 format!("<li><strong>{}</strong> - {}</li>", name, desc)
79 })
80 .collect::<Vec<_>>()
81 .join("\n")
82 };
83
84 let html = format!(
85 r#"<!DOCTYPE html>
86<html>
87<head>
88 <title>FastSkill Service</title>
89 <style>
90 body {{
91 font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
92 margin: 0;
93 padding: 20px;
94 background: #f5f5f5;
95 }}
96 .container {{
97 max-width: 1200px;
98 margin: 0 auto;
99 background: white;
100 border-radius: 8px;
101 box-shadow: 0 2px 10px rgba(0,0,0,0.1);
102 overflow: hidden;
103 }}
104 .header {{
105 background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
106 color: white;
107 padding: 30px;
108 text-align: center;
109 }}
110 .content {{
111 padding: 30px;
112 }}
113 .stats {{
114 display: grid;
115 grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
116 gap: 20px;
117 margin-bottom: 30px;
118 }}
119 .stat-card {{
120 background: #f8f9fa;
121 padding: 20px;
122 border-radius: 6px;
123 text-align: center;
124 border-left: 4px solid #667eea;
125 }}
126 .stat-number {{
127 font-size: 2em;
128 font-weight: bold;
129 color: #333;
130 }}
131 .stat-label {{
132 color: #666;
133 margin-top: 5px;
134 }}
135 .skills {{
136 margin-top: 30px;
137 }}
138 .skill {{
139 background: #f9f9f9;
140 padding: 15px;
141 margin: 8px 0;
142 border-radius: 6px;
143 border-left: 4px solid #28a745;
144 }}
145 .api-links {{
146 margin-top: 40px;
147 padding: 20px;
148 background: #f8f9fa;
149 border-radius: 6px;
150 }}
151 .api-links h3 {{
152 margin-top: 0;
153 color: #333;
154 }}
155 .api-links ul {{
156 list-style: none;
157 padding: 0;
158 }}
159 .api-links li {{
160 margin: 8px 0;
161 }}
162 .api-links a {{
163 color: #667eea;
164 text-decoration: none;
165 font-weight: 500;
166 }}
167 .api-links a:hover {{
168 text-decoration: underline;
169 }}
170 .footer {{
171 text-align: center;
172 padding: 20px;
173 background: #f8f9fa;
174 color: #666;
175 font-size: 0.9em;
176 }}
177 </style>
178</head>
179<body>
180 <div class="container">
181 <div class="header">
182 <h1>FastSkill Service</h1>
183 <p>AI Agent Skills Management Platform</p>
184 </div>
185
186 <div class="content">
187 <div class="stats">
188 <div class="stat-card">
189 <div class="stat-number">{}</div>
190 <div class="stat-label">Skills Indexed</div>
191 </div>
192 <div class="stat-card">
193 <div class="stat-number">{}</div>
194 <div class="stat-label">Uptime (seconds)</div>
195 </div>
196 <div class="stat-card">
197 <div class="stat-number">v{}</div>
198 <div class="stat-label">Version</div>
199 </div>
200 </div>
201
202 <div class="skills">
203 <h2>Recent Skills</h2>
204 <ul>
205 {}
206 </ul>
207 {}
208 </div>
209
210 <div class="api-links">
211 <h3>API Endpoints</h3>
212 <ul>
213 <li><a href="/api/skills">GET /api/skills</a> - List all skills</li>
214 <li><a href="/api/status">GET /api/status</a> - Service status</li>
215 <li><a href="/api/search">POST /api/search</a> - Search skills</li>
216 <li><a href="/api/reindex">POST /api/reindex</a> - Reindex skills</li>
217 </ul>
218 </div>
219 </div>
220 </div>
221</body>
222</html>"#,
223 skills_count,
224 uptime,
225 env!("CARGO_PKG_VERSION"),
226 skills_html,
227 if skills_count > 10 {
228 format!("<p>... and {} more skills</p>", skills_count - 10)
229 } else {
230 "".to_string()
231 }
232 );
233
234 Html(html)
235}
236
237pub async fn status(
239 State(state): State<AppState>,
240) -> HttpResult<axum::Json<ApiResponse<StatusResponse>>> {
241 let skills = state.service.skill_manager().list_skills(None).await?;
242 let skills_count = skills.len();
243
244 let config = state.service.config();
245
246 let response = StatusResponse {
247 status: "running".to_string(),
248 version: env!("CARGO_PKG_VERSION").to_string(),
249 skills_count,
250 storage_path: config.skill_storage_path.to_string_lossy().to_string(),
251 hot_reload_enabled: config.hot_reload.enabled,
252 uptime_seconds: state.uptime_seconds(),
253 };
254
255 Ok(axum::Json(ApiResponse::success(response)))
256}