1use crate::server::AppState;
2use axum::{
3 extract::State,
4 response::{Html, IntoResponse, Json},
5 routing::get,
6 Router,
7};
8use serde_json::json;
9use tokio::net::TcpListener;
10
11pub async fn run_admin_server(state: AppState) -> anyhow::Result<()> {
13 let port = state.config.server.admin_port;
14 let metrics_export_enabled =
15 state.config.server.metrics.enabled && state.config.server.metrics.export_endpoint;
16
17 let mut app = Router::new()
18 .route("/health", get(health_check))
19 .route("/", get(dashboard));
20
21 if metrics_export_enabled {
23 app = app.route("/metrics", get(metrics_handler));
24 }
25
26 let host = state.config.server.host.clone();
27 let app = app
28 .layer(axum::middleware::from_fn_with_state(
29 state.clone(),
30 crate::server::middleware::auth::admin_auth_middleware,
31 ))
32 .with_state(state);
33
34 let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
35 tracing::info!("Admin dashboard available at http://{}:{}", host, port);
36 if metrics_export_enabled {
37 tracing::info!(
38 "Prometheus metrics available at http://{}:{}/metrics",
39 host,
40 port
41 );
42 }
43 axum::serve(listener, app).await?;
44 Ok(())
45}
46
47async fn health_check(State(state): State<AppState>) -> impl IntoResponse {
48 let mut health_data = json!({
49 "status": "ok",
50 "version": env!("CARGO_PKG_VERSION")
51 });
52
53 if let Some(browser) = &state.browser_manager {
54 if let Ok(pool_health) = browser.get_pool_health().await {
55 health_data["browser_pool"] = pool_health;
56 } else {
57 health_data["browser_pool"] = json!({"status": "unhealthy"});
58 }
59 }
60
61 Json(health_data)
62}
63
64async fn dashboard(State(state): State<AppState>) -> impl IntoResponse {
65 let capabilities = state.registry.known_types();
66 let auth_status = if state.config.server.auth.enabled {
67 "Enabled"
68 } else {
69 "Disabled (Dev Mode)"
70 };
71 let rate_limit_status = if state.config.server.rate_limit.enabled {
72 "Enabled"
73 } else {
74 "Disabled"
75 };
76 let cb_status = if state.config.server.circuit_breaker.enabled {
77 "Enabled"
78 } else {
79 "Disabled"
80 };
81
82 let api_port = state.config.server.port;
83 let host = &state.config.server.host;
84
85 let html = format!(
86 r#"
87<!DOCTYPE html>
88<html lang="en">
89<head>
90 <meta charset="UTF-8">
91 <meta name="viewport" content="width=device-width, initial-scale=1.0">
92 <title>Kroki-rs | Admin Dashboard</title>
93 <style>
94 :root {{
95 --primary: #6366f1;
96 --primary-dark: #4f46e5;
97 --bg: #0f172a;
98 --card-bg: #1e293b;
99 --text: #f8fafc;
100 --text-muted: #94a3b8;
101 --success: #22c55e;
102 --warning: #eab308;
103 }}
104 body {{
105 font-family: 'Inter', -apple-system, sans-serif;
106 background-color: var(--bg);
107 color: var(--text);
108 margin: 0;
109 display: flex;
110 flex-direction: column;
111 align-items: center;
112 min-height: 100vh;
113 padding: 2rem;
114 }}
115 .container {{
116 max-width: 900px;
117 width: 100%;
118 }}
119 header {{
120 text-align: center;
121 margin-bottom: 3rem;
122 }}
123 h1 {{
124 font-size: 2.5rem;
125 margin: 0;
126 background: linear-gradient(to right, #818cf8, #c084fc);
127 -webkit-background-clip: text;
128 -webkit-text-fill-color: transparent;
129 }}
130 .version {{
131 font-size: 0.875rem;
132 color: var(--text-muted);
133 margin-top: 0.5rem;
134 }}
135 .grid {{
136 display: grid;
137 grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
138 gap: 1.5rem;
139 margin-bottom: 3rem;
140 }}
141 .card {{
142 background: var(--card-bg);
143 border-radius: 1rem;
144 padding: 1.5rem;
145 border: 1px solid #334155;
146 }}
147 .card h2 {{
148 margin-top: 0;
149 font-size: 1.25rem;
150 border-bottom: 1px solid #334155;
151 padding-bottom: 0.75rem;
152 margin-bottom: 1rem;
153 }}
154 .status-row {{
155 display: flex;
156 justify-content: space-between;
157 align-items: center;
158 margin-bottom: 0.75rem;
159 }}
160 .status-label {{ color: var(--text-muted); }}
161 .status-value {{ font-weight: 600; }}
162 .enabled {{ color: var(--success); }}
163 .disabled {{ color: var(--text-muted); }}
164
165 ul {{
166 columns: 2;
167 list-style-type: none;
168 padding: 0;
169 margin: 0;
170 }}
171 li {{
172 padding: 0.25rem 0;
173 font-size: 0.875rem;
174 }}
175
176 .nav-link {{
177 display: inline-flex;
178 align-items: center;
179 gap: 0.5rem;
180 color: var(--primary);
181 text-decoration: none;
182 font-weight: 500;
183 margin-bottom: 2rem;
184 }}
185 .nav-link:hover {{ text-decoration: underline; }}
186
187 .footer {{
188 margin-top: auto;
189 padding-top: 3rem;
190 color: var(--text-muted);
191 font-size: 0.875rem;
192 text-align: center;
193 }}
194 </style>
195</head>
196<body>
197 <div class="container">
198 <header>
199 <a href="http://{}:{}" class="nav-link">🏠 Back to Discovery</a>
200 <h1>Admin Dashboard</h1>
201 <div class="version">Kroki-rs v{}</div>
202 </header>
203
204 <div class="grid">
205 <div class="card">
206 <h2>System Status</h2>
207 <div class="status-row">
208 <span class="status-label">Service</span>
209 <span class="status-value enabled">Online</span>
210 </div>
211 <div class="status-row">
212 <span class="status-label">Authentication</span>
213 <span class="status-value {}">{}</span>
214 </div>
215 <div class="status-row">
216 <span class="status-label">Rate Limiting</span>
217 <span class="status-value {}">{}</span>
218 </div>
219 <div class="status-row">
220 <span class="status-label">Circuit Breaker</span>
221 <span class="status-value {}">{}</span>
222 </div>
223 </div>
224
225 <div class="card">
226 <h2>Diagram Providers</h2>
227 <div style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem;">
228 {} available tools registered
229 </div>
230 <ul>
231 {}
232 </ul>
233 </div>
234 </div>
235
236 <div class="footer">
237 Built with Rust & Axum • <a href="https://github.com/softmentor/kroki-rs" style="color: inherit;">GitHub</a>
238 </div>
239 </div>
240</body>
241</html>
242"#,
243 host,
244 api_port,
245 env!("CARGO_PKG_VERSION"),
246 if state.config.server.auth.enabled {
247 "enabled"
248 } else {
249 "disabled"
250 },
251 auth_status,
252 if state.config.server.rate_limit.enabled {
253 "enabled"
254 } else {
255 "disabled"
256 },
257 rate_limit_status,
258 if state.config.server.circuit_breaker.enabled {
259 "enabled"
260 } else {
261 "disabled"
262 },
263 cb_status,
264 capabilities.len(),
265 capabilities
266 .iter()
267 .map(|c| format!("<li>✅ {}</li>", c))
268 .collect::<Vec<_>>()
269 .join("\n")
270 );
271
272 Html(html)
273}
274
275async fn metrics_handler(State(state): State<AppState>) -> impl IntoResponse {
276 if let Some(handle) = &state.metrics_handle {
277 handle.render()
278 } else {
279 "Metrics collection is disabled".to_string()
280 }
281}