1use reinhardt_core::security::escape_html;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::sync::Arc;
5use std::time::{Duration, Instant};
6use tokio::sync::RwLock;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct DebugPanel {
11 pub title: String,
13 pub content: Vec<DebugEntry>,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19#[serde(tag = "type")]
20pub enum DebugEntry {
21 KeyValue {
23 key: String,
25 value: String,
27 },
28 Table {
30 headers: Vec<String>,
32 rows: Vec<Vec<String>>,
34 },
35 Code {
37 language: String,
39 code: String,
41 },
42 Text {
44 text: String,
46 },
47}
48
49#[derive(Debug, Clone, Serialize)]
51pub struct TimingInfo {
52 pub total_time: Duration,
54 pub sql_time: Duration,
56 pub sql_queries: usize,
58 pub cache_hits: usize,
60 pub cache_misses: usize,
62}
63
64#[derive(Debug, Clone, Serialize)]
66pub struct SqlQuery {
67 pub query: String,
69 pub duration: Duration,
71 pub stack_trace: Vec<String>,
73}
74
75pub struct DebugToolbar {
77 panels: Arc<RwLock<HashMap<String, DebugPanel>>>,
78 timing: Arc<RwLock<TimingInfo>>,
79 sql_queries: Arc<RwLock<Vec<SqlQuery>>>,
80 start_time: Instant,
81 enabled: bool,
82}
83
84impl DebugToolbar {
85 pub fn new() -> Self {
96 Self {
97 panels: Arc::new(RwLock::new(HashMap::new())),
98 timing: Arc::new(RwLock::new(TimingInfo {
99 total_time: Duration::from_secs(0),
100 sql_time: Duration::from_secs(0),
101 sql_queries: 0,
102 cache_hits: 0,
103 cache_misses: 0,
104 })),
105 sql_queries: Arc::new(RwLock::new(Vec::new())),
106 start_time: Instant::now(),
107 enabled: true,
108 }
109 }
110 pub fn set_enabled(&mut self, enabled: bool) {
122 self.enabled = enabled;
123 }
124 pub fn is_enabled(&self) -> bool {
135 self.enabled
136 }
137 pub async fn add_panel(&self, id: String, panel: DebugPanel) {
156 if !self.enabled {
157 return;
158 }
159 self.panels.write().await.insert(id, panel);
160 }
161 pub async fn record_sql_query(&self, query: String, duration: Duration) {
178 if !self.enabled {
179 return;
180 }
181
182 let sql_query = SqlQuery {
183 query,
184 duration,
185 stack_trace: vec![],
186 };
187
188 self.sql_queries.write().await.push(sql_query);
189
190 let mut timing = self.timing.write().await;
191 timing.sql_queries += 1;
192 timing.sql_time += duration;
193 }
194 pub async fn record_cache_hit(&self) {
209 if !self.enabled {
210 return;
211 }
212 self.timing.write().await.cache_hits += 1;
213 }
214 pub async fn record_cache_miss(&self) {
229 if !self.enabled {
230 return;
231 }
232 self.timing.write().await.cache_misses += 1;
233 }
234 pub async fn finalize(&self) {
252 if !self.enabled {
253 return;
254 }
255 self.timing.write().await.total_time = self.start_time.elapsed();
256 }
257 pub async fn get_panels(&self) -> HashMap<String, DebugPanel> {
276 self.panels.read().await.clone()
277 }
278 pub async fn get_timing(&self) -> TimingInfo {
293 self.timing.read().await.clone()
294 }
295 pub async fn get_sql_queries(&self) -> Vec<SqlQuery> {
312 self.sql_queries.read().await.clone()
313 }
314 pub async fn render_html(&self) -> String {
330 if !self.enabled {
331 return String::new();
332 }
333
334 let panels = self.get_panels().await;
335 let timing = self.get_timing().await;
336 let queries = self.get_sql_queries().await;
337
338 format!(
339 r#"
340<div class="debug-toolbar">
341 <div class="timing">
342 <h3>Timing</h3>
343 <p>Total: {:?}</p>
344 <p>SQL: {:?} ({} queries)</p>
345 <p>Cache: {} hits, {} misses</p>
346 </div>
347 <div class="sql-queries">
348 <h3>SQL Queries</h3>
349 <ul>
350 {}
351 </ul>
352 </div>
353 <div class="panels">
354 {}
355 </div>
356</div>
357"#,
358 timing.total_time,
359 timing.sql_time,
360 timing.sql_queries,
361 timing.cache_hits,
362 timing.cache_misses,
363 queries
364 .iter()
365 .map(|q| format!("<li>{} ({:?})</li>", escape_html(&q.query), q.duration))
366 .collect::<Vec<_>>()
367 .join("\n"),
368 panels
369 .iter()
370 .map(|(id, panel)| format!(
371 "<div class='panel' id='{}'><h3>{}</h3></div>",
372 escape_html(id),
373 escape_html(&panel.title)
374 ))
375 .collect::<Vec<_>>()
376 .join("\n")
377 )
378 }
379}
380
381impl Default for DebugToolbar {
382 fn default() -> Self {
383 Self::new()
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[tokio::test]
392 async fn test_debug_toolbar() {
393 let toolbar = DebugToolbar::new();
394
395 toolbar
396 .record_sql_query("SELECT * FROM users".to_string(), Duration::from_millis(10))
397 .await;
398 toolbar.record_cache_hit().await;
399 toolbar.finalize().await;
400
401 let timing = toolbar.get_timing().await;
402 assert_eq!(timing.sql_queries, 1);
403 assert_eq!(timing.cache_hits, 1);
404
405 let queries = toolbar.get_sql_queries().await;
406 assert_eq!(queries.len(), 1);
407 }
408
409 #[tokio::test]
410 async fn test_debug_panel() {
411 let toolbar = DebugToolbar::new();
412
413 let panel = DebugPanel {
414 title: "Test Panel".to_string(),
415 content: vec![DebugEntry::KeyValue {
416 key: "key".to_string(),
417 value: "value".to_string(),
418 }],
419 };
420
421 toolbar.add_panel("test".to_string(), panel).await;
422
423 let panels = toolbar.get_panels().await;
424 assert_eq!(panels.len(), 1);
425 assert!(panels.contains_key("test"));
426 }
427
428 #[test]
429 fn test_escape_html() {
430 assert_eq!(escape_html("hello"), "hello");
431 assert_eq!(
432 escape_html("<script>alert('xss')</script>"),
433 "<script>alert('xss')</script>"
434 );
435 assert_eq!(escape_html("a & b"), "a & b");
436 assert_eq!(escape_html(r#"key="value""#), "key="value"");
437 }
438
439 #[tokio::test]
440 async fn test_render_html_escapes_sql_queries() {
441 let toolbar = DebugToolbar::new();
442 toolbar
443 .record_sql_query(
444 "SELECT * FROM users WHERE name = '<script>alert(1)</script>'".to_string(),
445 Duration::from_millis(1),
446 )
447 .await;
448 toolbar.finalize().await;
449
450 let html = toolbar.render_html().await;
451 assert!(!html.contains("<script>"));
452 assert!(html.contains("<script>"));
453 }
454
455 #[tokio::test]
456 async fn test_render_html_escapes_panel_content() {
457 let toolbar = DebugToolbar::new();
458 let panel = DebugPanel {
459 title: "<img src=x onerror=alert(1)>".to_string(),
460 content: vec![],
461 };
462 toolbar.add_panel("<script>".to_string(), panel).await;
463 toolbar.finalize().await;
464
465 let html = toolbar.render_html().await;
466 assert!(!html.contains("<script>"));
467 assert!(!html.contains("<img src=x"));
468 assert!(html.contains("<script>"));
469 assert!(html.contains("<img src=x onerror=alert(1)>"));
470 }
471
472 #[tokio::test]
473 async fn test_disabled_toolbar() {
474 let mut toolbar = DebugToolbar::new();
475 toolbar.set_enabled(false);
476
477 toolbar
478 .record_sql_query("SELECT 1".to_string(), Duration::from_millis(1))
479 .await;
480
481 let timing = toolbar.get_timing().await;
482 assert_eq!(timing.sql_queries, 0);
483 }
484}