Skip to main content

fastskill_core/http/handlers/
status.rs

1//! Status and root endpoint handlers
2
3use 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/// Shared state for HTTP handlers
11#[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    /// Canonicalize a path if it exists, otherwise return as-is
49    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
61/// GET / - Root endpoint with HTML dashboard
62pub 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) // Show first 10 skills
75            .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
237/// GET /api/status - Service status endpoint
238pub 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}