async_inspect/reporter/
html.rs

1//! HTML visualization output
2//!
3//! Generates interactive HTML reports with timeline visualization,
4//! state machine graphs, and task inspection panels.
5
6use crate::inspector::Inspector;
7use crate::task::{TaskInfo, TaskState};
8use std::fmt::Write as FmtWrite;
9
10/// HTML report generator
11pub struct HtmlReporter {
12    inspector: Inspector,
13}
14
15impl HtmlReporter {
16    /// Create a new HTML reporter
17    #[must_use]
18    pub fn new(inspector: Inspector) -> Self {
19        Self { inspector }
20    }
21
22    /// Create a reporter using the global inspector
23    #[must_use]
24    pub fn global() -> Self {
25        Self::new(Inspector::global().clone())
26    }
27
28    /// Generate a complete HTML report
29    #[must_use]
30    pub fn generate_html(&self) -> String {
31        let mut html = String::new();
32
33        // HTML structure
34        writeln!(html, "<!DOCTYPE html>").unwrap();
35        writeln!(html, "<html lang=\"en\">").unwrap();
36        writeln!(html, "<head>").unwrap();
37        writeln!(html, "    <meta charset=\"UTF-8\">").unwrap();
38        writeln!(
39            html,
40            "    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">"
41        )
42        .unwrap();
43        writeln!(html, "    <title>async-inspect Report</title>").unwrap();
44
45        // Embedded CSS
46        html.push_str(&self.generate_css());
47
48        writeln!(html, "</head>").unwrap();
49        writeln!(html, "<body>").unwrap();
50
51        // Header
52        html.push_str(&self.generate_header());
53
54        // Main content
55        writeln!(html, "    <div class=\"container\">").unwrap();
56
57        // Statistics panel
58        html.push_str(&self.generate_stats_panel());
59
60        // Timeline visualization
61        html.push_str(&self.generate_timeline_viz());
62
63        // State machine graph
64        html.push_str(&self.generate_state_machine_graph());
65
66        // Task list with details
67        html.push_str(&self.generate_task_list());
68
69        writeln!(html, "    </div>").unwrap();
70
71        // Embedded JavaScript
72        html.push_str(&self.generate_javascript());
73
74        writeln!(html, "</body>").unwrap();
75        writeln!(html, "</html>").unwrap();
76
77        html
78    }
79
80    /// Generate CSS styles
81    fn generate_css(&self) -> String {
82        r"
83    <style>
84        * {
85            margin: 0;
86            padding: 0;
87            box-sizing: border-box;
88        }
89
90        body {
91            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
92            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
93            min-height: 100vh;
94            padding: 20px;
95        }
96
97        .container {
98            max-width: 1400px;
99            margin: 0 auto;
100            background: white;
101            border-radius: 12px;
102            box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
103            overflow: hidden;
104        }
105
106        header {
107            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
108            color: white;
109            padding: 30px;
110            text-align: center;
111        }
112
113        header h1 {
114            font-size: 2.5em;
115            margin-bottom: 10px;
116        }
117
118        header p {
119            font-size: 1.2em;
120            opacity: 0.9;
121        }
122
123        .stats-panel {
124            display: grid;
125            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
126            gap: 20px;
127            padding: 30px;
128            background: #f8f9fa;
129            border-bottom: 1px solid #e0e0e0;
130        }
131
132        .stat-card {
133            background: white;
134            padding: 20px;
135            border-radius: 8px;
136            box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
137            transition: transform 0.2s;
138        }
139
140        .stat-card:hover {
141            transform: translateY(-5px);
142            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
143        }
144
145        .stat-card .label {
146            color: #666;
147            font-size: 0.9em;
148            text-transform: uppercase;
149            letter-spacing: 1px;
150            margin-bottom: 5px;
151        }
152
153        .stat-card .value {
154            font-size: 2em;
155            font-weight: bold;
156            color: #667eea;
157        }
158
159        .timeline-viz {
160            padding: 30px;
161            border-bottom: 1px solid #e0e0e0;
162        }
163
164        .timeline-viz h2 {
165            margin-bottom: 20px;
166            color: #333;
167        }
168
169        .timeline-container {
170            position: relative;
171            height: 400px;
172            background: #f8f9fa;
173            border-radius: 8px;
174            overflow-x: auto;
175            overflow-y: auto;
176            border: 1px solid #e0e0e0;
177        }
178
179        .timeline-svg {
180            width: 100%;
181            min-width: 800px;
182            height: 100%;
183        }
184
185        .task-row {
186            cursor: pointer;
187            transition: opacity 0.2s;
188        }
189
190        .task-row:hover {
191            opacity: 0.8;
192        }
193
194        .task-bar {
195            stroke-width: 2;
196            stroke: white;
197        }
198
199        .task-bar.completed {
200            fill: #4caf50;
201        }
202
203        .task-bar.running {
204            fill: #2196f3;
205        }
206
207        .task-bar.blocked {
208            fill: #ff9800;
209        }
210
211        .task-bar.failed {
212            fill: #f44336;
213        }
214
215        .task-bar.pending {
216            fill: #9e9e9e;
217        }
218
219        .task-list {
220            padding: 30px;
221        }
222
223        .task-list h2 {
224            margin-bottom: 20px;
225            color: #333;
226        }
227
228        .task-item {
229            background: #f8f9fa;
230            border-radius: 8px;
231            padding: 20px;
232            margin-bottom: 15px;
233            cursor: pointer;
234            transition: all 0.2s;
235            border-left: 4px solid #667eea;
236        }
237
238        .task-item:hover {
239            background: #e9ecef;
240            transform: translateX(5px);
241        }
242
243        .task-item.expanded {
244            background: white;
245            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
246        }
247
248        .task-header {
249            display: flex;
250            justify-content: space-between;
251            align-items: center;
252        }
253
254        .task-name {
255            font-weight: bold;
256            font-size: 1.1em;
257            color: #333;
258        }
259
260        .task-state {
261            padding: 5px 15px;
262            border-radius: 20px;
263            font-size: 0.85em;
264            font-weight: bold;
265            text-transform: uppercase;
266        }
267
268        .state-completed {
269            background: #4caf50;
270            color: white;
271        }
272
273        .state-running {
274            background: #2196f3;
275            color: white;
276        }
277
278        .state-blocked {
279            background: #ff9800;
280            color: white;
281        }
282
283        .state-failed {
284            background: #f44336;
285            color: white;
286        }
287
288        .state-pending {
289            background: #9e9e9e;
290            color: white;
291        }
292
293        .task-details {
294            margin-top: 15px;
295            padding-top: 15px;
296            border-top: 1px solid #e0e0e0;
297            display: none;
298        }
299
300        .task-item.expanded .task-details {
301            display: block;
302        }
303
304        .task-meta {
305            display: grid;
306            grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
307            gap: 15px;
308            margin-bottom: 15px;
309        }
310
311        .meta-item {
312            font-size: 0.9em;
313        }
314
315        .meta-label {
316            color: #666;
317            font-weight: bold;
318            margin-bottom: 3px;
319        }
320
321        .meta-value {
322            color: #333;
323        }
324
325        .events-section {
326            margin-top: 15px;
327        }
328
329        .events-section h4 {
330            margin-bottom: 10px;
331            color: #667eea;
332        }
333
334        .event-item {
335            background: white;
336            padding: 10px;
337            margin-bottom: 8px;
338            border-radius: 4px;
339            border-left: 3px solid #667eea;
340            font-size: 0.9em;
341        }
342
343        .event-time {
344            color: #666;
345            font-family: 'Courier New', monospace;
346        }
347
348        .legend {
349            display: flex;
350            gap: 20px;
351            margin-top: 15px;
352            padding: 15px;
353            background: white;
354            border-radius: 8px;
355        }
356
357        .legend-item {
358            display: flex;
359            align-items: center;
360            gap: 8px;
361            font-size: 0.9em;
362        }
363
364        .legend-color {
365            width: 20px;
366            height: 20px;
367            border-radius: 4px;
368        }
369
370        @keyframes pulse {
371            0%, 100% { opacity: 1; }
372            50% { opacity: 0.5; }
373        }
374
375        .running-indicator {
376            animation: pulse 1.5s infinite;
377        }
378
379        /* State Machine Graph */
380        .state-machine-graph {
381            padding: 30px;
382            border-bottom: 1px solid #e0e0e0;
383        }
384
385        .state-machine-graph h2 {
386            margin-bottom: 20px;
387            color: #333;
388        }
389
390        .graph-container {
391            background: #f8f9fa;
392            border-radius: 8px;
393            padding: 20px;
394            min-height: 400px;
395            border: 1px solid #e0e0e0;
396            position: relative;
397        }
398
399        .state-node {
400            cursor: pointer;
401            transition: all 0.2s;
402        }
403
404        .state-node:hover {
405            transform: scale(1.05);
406        }
407
408        .state-node circle {
409            stroke-width: 2;
410            transition: all 0.2s;
411        }
412
413        .state-node:hover rect,
414        .state-node:hover circle {
415            stroke-width: 3;
416        }
417
418        .state-node.pending rect,
419        .state-node.pending circle {
420            fill: #9e9e9e;
421            stroke: #757575;
422        }
423
424        .state-node.running rect,
425        .state-node.running circle {
426            fill: #2196f3;
427            stroke: #1976d2;
428        }
429
430        .state-node.blocked rect,
431        .state-node.blocked circle {
432            fill: #ff9800;
433            stroke: #f57c00;
434        }
435
436        .state-node.completed rect,
437        .state-node.completed circle {
438            fill: #4caf50;
439            stroke: #388e3c;
440        }
441
442        .state-node.failed rect,
443        .state-node.failed circle {
444            fill: #f44336;
445            stroke: #d32f2f;
446        }
447
448        .state-node text {
449            fill: white;
450            font-size: 12px;
451            font-weight: bold;
452            text-anchor: middle;
453            pointer-events: none;
454        }
455
456        .state-transition {
457            fill: none;
458            stroke: #999;
459            stroke-width: 2;
460            marker-end: url(#arrowhead);
461        }
462
463        .state-transition.animated {
464            stroke-dasharray: 5, 5;
465            animation: dash 1s linear infinite;
466        }
467
468        @keyframes dash {
469            to {
470                stroke-dashoffset: -10;
471            }
472        }
473
474        .transition-label {
475            font-size: 10px;
476            fill: #666;
477            text-anchor: middle;
478        }
479
480        .graph-legend {
481            margin-top: 15px;
482            padding: 15px;
483            background: white;
484            border-radius: 8px;
485            display: flex;
486            gap: 20px;
487            flex-wrap: wrap;
488        }
489    </style>
490"
491        .to_string()
492    }
493
494    /// Generate header
495    fn generate_header(&self) -> String {
496        let stats = self.inspector.stats();
497        format!(
498            r"    <header>
499        <h1>🔍 async-inspect</h1>
500        <p>X-ray vision for async Rust - {} tasks analyzed</p>
501    </header>
502",
503            stats.total_tasks
504        )
505    }
506
507    /// Generate statistics panel
508    fn generate_stats_panel(&self) -> String {
509        let stats = self.inspector.stats();
510        let mut html = String::new();
511
512        writeln!(html, "        <div class=\"stats-panel\">").unwrap();
513
514        self.add_stat_card(&mut html, "Total Tasks", &stats.total_tasks.to_string());
515        self.add_stat_card(&mut html, "Running", &stats.running_tasks.to_string());
516        self.add_stat_card(&mut html, "Blocked", &stats.blocked_tasks.to_string());
517        self.add_stat_card(&mut html, "Completed", &stats.completed_tasks.to_string());
518        self.add_stat_card(&mut html, "Failed", &stats.failed_tasks.to_string());
519        self.add_stat_card(&mut html, "Total Events", &stats.total_events.to_string());
520        self.add_stat_card(
521            &mut html,
522            "Duration",
523            &format!("{:.2}s", stats.timeline_duration.as_secs_f64()),
524        );
525
526        writeln!(html, "        </div>").unwrap();
527
528        html
529    }
530
531    /// Add a stat card
532    fn add_stat_card(&self, html: &mut String, label: &str, value: &str) {
533        writeln!(html, "            <div class=\"stat-card\">").unwrap();
534        writeln!(html, "                <div class=\"label\">{label}</div>").unwrap();
535        writeln!(html, "                <div class=\"value\">{value}</div>").unwrap();
536        writeln!(html, "            </div>").unwrap();
537    }
538
539    /// Generate interactive timeline visualization
540    fn generate_timeline_viz(&self) -> String {
541        let tasks = self.inspector.get_all_tasks();
542
543        if tasks.is_empty() {
544            return String::from(
545                "        <div class=\"timeline-viz\"><p>No tasks to visualize</p></div>",
546            );
547        }
548
549        let mut html = String::new();
550        writeln!(html, "        <div class=\"timeline-viz\">").unwrap();
551        writeln!(html, "            <h2>Concurrency Timeline</h2>").unwrap();
552        writeln!(html, "            <div class=\"timeline-container\">").unwrap();
553
554        // Generate SVG timeline
555        html.push_str(&self.generate_svg_timeline(&tasks));
556
557        writeln!(html, "            </div>").unwrap();
558
559        // Legend
560        writeln!(html, "            <div class=\"legend\">").unwrap();
561        writeln!(html, "                <div class=\"legend-item\">").unwrap();
562        writeln!(
563            html,
564            "                    <div class=\"legend-color\" style=\"background: #4caf50;\"></div>"
565        )
566        .unwrap();
567        writeln!(html, "                    <span>Completed</span>").unwrap();
568        writeln!(html, "                </div>").unwrap();
569        writeln!(html, "                <div class=\"legend-item\">").unwrap();
570        writeln!(
571            html,
572            "                    <div class=\"legend-color\" style=\"background: #2196f3;\"></div>"
573        )
574        .unwrap();
575        writeln!(html, "                    <span>Running</span>").unwrap();
576        writeln!(html, "                </div>").unwrap();
577        writeln!(html, "                <div class=\"legend-item\">").unwrap();
578        writeln!(
579            html,
580            "                    <div class=\"legend-color\" style=\"background: #ff9800;\"></div>"
581        )
582        .unwrap();
583        writeln!(html, "                    <span>Blocked</span>").unwrap();
584        writeln!(html, "                </div>").unwrap();
585        writeln!(html, "                <div class=\"legend-item\">").unwrap();
586        writeln!(
587            html,
588            "                    <div class=\"legend-color\" style=\"background: #f44336;\"></div>"
589        )
590        .unwrap();
591        writeln!(html, "                    <span>Failed</span>").unwrap();
592        writeln!(html, "                </div>").unwrap();
593        writeln!(html, "                <div class=\"legend-item\">").unwrap();
594        writeln!(
595            html,
596            "                    <div class=\"legend-color\" style=\"background: #9e9e9e;\"></div>"
597        )
598        .unwrap();
599        writeln!(html, "                    <span>Pending</span>").unwrap();
600        writeln!(html, "                </div>").unwrap();
601        writeln!(html, "            </div>").unwrap();
602
603        writeln!(html, "        </div>").unwrap();
604
605        html
606    }
607
608    /// Generate SVG timeline
609    fn generate_svg_timeline(&self, tasks: &[TaskInfo]) -> String {
610        let mut svg = String::new();
611
612        // Calculate time bounds
613        let start_time = tasks
614            .iter()
615            .map(|t| t.created_at)
616            .min()
617            .unwrap_or_else(std::time::Instant::now);
618
619        let end_time = tasks
620            .iter()
621            .map(|t| t.created_at + t.age())
622            .max()
623            .unwrap_or_else(std::time::Instant::now);
624
625        let total_duration = end_time.duration_since(start_time);
626        let total_ms = total_duration.as_millis() as f64;
627
628        // SVG dimensions
629        let width = 1200.0;
630        let row_height = 40.0;
631        let margin_left = 200.0;
632        let timeline_width = width - margin_left - 50.0;
633        let height = (tasks.len() as f64 * row_height) + 60.0;
634
635        writeln!(svg, "<svg class=\"timeline-svg\" viewBox=\"0 0 {width} {height}\" xmlns=\"http://www.w3.org/2000/svg\">").unwrap();
636
637        // Time axis
638        self.add_time_axis(&mut svg, margin_left, timeline_width, total_ms);
639
640        // Task rows
641        for (i, task) in tasks.iter().enumerate() {
642            let y = 50.0 + (i as f64 * row_height);
643            self.add_task_row(
644                &mut svg,
645                task,
646                y,
647                margin_left,
648                timeline_width,
649                start_time,
650                total_ms,
651            );
652        }
653
654        writeln!(svg, "</svg>").unwrap();
655
656        svg
657    }
658
659    /// Add time axis to SVG
660    fn add_time_axis(&self, svg: &mut String, margin_left: f64, width: f64, total_ms: f64) {
661        // Time markers
662        let num_markers = 10;
663        for i in 0..=num_markers {
664            let x = margin_left + (f64::from(i) / f64::from(num_markers)) * width;
665            let time_ms = (f64::from(i) / f64::from(num_markers)) * total_ms;
666
667            writeln!(svg, "  <line x1=\"{x}\" y1=\"30\" x2=\"{x}\" y2=\"35\" stroke=\"#999\" stroke-width=\"1\" />").unwrap();
668            writeln!(svg, "  <text x=\"{}\" y=\"25\" text-anchor=\"middle\" font-size=\"10\" fill=\"#666\">{}ms</text>", x, time_ms as u64).unwrap();
669        }
670
671        // Axis line
672        writeln!(
673            svg,
674            "  <line x1=\"{}\" y1=\"35\" x2=\"{}\" y2=\"35\" stroke=\"#333\" stroke-width=\"2\" />",
675            margin_left,
676            margin_left + width
677        )
678        .unwrap();
679    }
680
681    /// Add task row to SVG
682    fn add_task_row(
683        &self,
684        svg: &mut String,
685        task: &TaskInfo,
686        y: f64,
687        margin_left: f64,
688        timeline_width: f64,
689        start_time: std::time::Instant,
690        total_ms: f64,
691    ) {
692        // Task name
693        writeln!(svg, "  <text x=\"10\" y=\"{}\" font-size=\"12\" font-weight=\"bold\" fill=\"#333\">{}</text>", y + 5.0, task.name).unwrap();
694
695        // Task bar
696        let task_start = task.created_at.duration_since(start_time).as_millis() as f64;
697        let task_duration = task.age().as_millis() as f64;
698
699        let x = margin_left + (task_start / total_ms) * timeline_width;
700        let bar_width = ((task_duration / total_ms) * timeline_width).max(2.0);
701
702        let state_class = match task.state {
703            TaskState::Completed => "completed",
704            TaskState::Running => "running",
705            TaskState::Blocked { .. } => "blocked",
706            TaskState::Failed => "failed",
707            TaskState::Pending => "pending",
708        };
709
710        writeln!(svg, "  <g class=\"task-row\" data-task-id=\"{}\">", task.id).unwrap();
711        writeln!(svg, "    <rect class=\"task-bar {}\" x=\"{}\" y=\"{}\" width=\"{}\" height=\"25\" rx=\"3\" />",
712            state_class, x, y - 12.0, bar_width).unwrap();
713        writeln!(
714            svg,
715            "    <title>{}: {:.2}ms</title>",
716            task.name, task_duration
717        )
718        .unwrap();
719        writeln!(svg, "  </g>").unwrap();
720    }
721
722    /// Generate state machine graph visualization
723    fn generate_state_machine_graph(&self) -> String {
724        let mut html = String::new();
725        writeln!(html, "        <div class=\"state-machine-graph\">").unwrap();
726        writeln!(html, "            <h2>Task Relationship Graph</h2>").unwrap();
727        writeln!(html, "            <p style=\"color: #666; margin-bottom: 15px;\">Hierarchical view of task dependencies and interactions</p>").unwrap();
728        writeln!(
729            html,
730            "            <div class=\"graph-container\" id=\"state-graph\">"
731        )
732        .unwrap();
733
734        // Generate SVG for state machine
735        html.push_str(&self.generate_state_machine_svg());
736
737        writeln!(html, "            </div>").unwrap();
738
739        // Legend
740        writeln!(html, "            <div class=\"graph-legend\">").unwrap();
741        writeln!(html, "                <div class=\"legend-item\">").unwrap();
742        writeln!(
743            html,
744            "                    <div class=\"legend-color\" style=\"background: #9e9e9e;\"></div>"
745        )
746        .unwrap();
747        writeln!(html, "                    <span>Pending</span>").unwrap();
748        writeln!(html, "                </div>").unwrap();
749        writeln!(html, "                <div class=\"legend-item\">").unwrap();
750        writeln!(
751            html,
752            "                    <div class=\"legend-color\" style=\"background: #2196f3;\"></div>"
753        )
754        .unwrap();
755        writeln!(html, "                    <span>Running</span>").unwrap();
756        writeln!(html, "                </div>").unwrap();
757        writeln!(html, "                <div class=\"legend-item\">").unwrap();
758        writeln!(
759            html,
760            "                    <div class=\"legend-color\" style=\"background: #ff9800;\"></div>"
761        )
762        .unwrap();
763        writeln!(html, "                    <span>Blocked</span>").unwrap();
764        writeln!(html, "                </div>").unwrap();
765        writeln!(html, "                <div class=\"legend-item\">").unwrap();
766        writeln!(
767            html,
768            "                    <div class=\"legend-color\" style=\"background: #4caf50;\"></div>"
769        )
770        .unwrap();
771        writeln!(html, "                    <span>Completed</span>").unwrap();
772        writeln!(html, "                </div>").unwrap();
773        writeln!(html, "                <div class=\"legend-item\">").unwrap();
774        writeln!(
775            html,
776            "                    <div class=\"legend-color\" style=\"background: #f44336;\"></div>"
777        )
778        .unwrap();
779        writeln!(html, "                    <span>Failed</span>").unwrap();
780        writeln!(html, "                </div>").unwrap();
781        writeln!(html, "            </div>").unwrap();
782
783        writeln!(html, "        </div>").unwrap();
784
785        html
786    }
787
788    /// Generate SVG for state machine visualization (task relationship graph)
789    fn generate_state_machine_svg(&self) -> String {
790        use std::collections::{HashMap, HashSet};
791
792        let mut svg = String::new();
793        let tasks = self.inspector.get_all_tasks();
794
795        if tasks.is_empty() {
796            writeln!(svg, "<svg width=\"800\" height=\"400\"><text x=\"400\" y=\"200\" text-anchor=\"middle\" fill=\"#666\">No tasks to visualize</text></svg>").unwrap();
797            return svg;
798        }
799
800        let width = 1000.0;
801        let height = 600.0;
802
803        writeln!(
804            svg,
805            "<svg width=\"{width}\" height=\"{height}\" xmlns=\"http://www.w3.org/2000/svg\">"
806        )
807        .unwrap();
808
809        // Define arrowhead markers for different relationship types
810        writeln!(svg, "  <defs>").unwrap();
811        writeln!(svg, "    <marker id=\"arrowhead\" markerWidth=\"10\" markerHeight=\"10\" refX=\"9\" refY=\"3\" orient=\"auto\">").unwrap();
812        writeln!(
813            svg,
814            "      <polygon points=\"0 0, 10 3, 0 6\" fill=\"#999\" />"
815        )
816        .unwrap();
817        writeln!(svg, "    </marker>").unwrap();
818        writeln!(svg, "    <marker id=\"arrowhead-parent\" markerWidth=\"10\" markerHeight=\"10\" refX=\"9\" refY=\"3\" orient=\"auto\">").unwrap();
819        writeln!(
820            svg,
821            "      <polygon points=\"0 0, 10 3, 0 6\" fill=\"#667eea\" />"
822        )
823        .unwrap();
824        writeln!(svg, "    </marker>").unwrap();
825        writeln!(svg, "    <marker id=\"arrowhead-blocked\" markerWidth=\"10\" markerHeight=\"10\" refX=\"9\" refY=\"3\" orient=\"auto\">").unwrap();
826        writeln!(
827            svg,
828            "      <polygon points=\"0 0, 10 3, 0 6\" fill=\"#ff9800\" />"
829        )
830        .unwrap();
831        writeln!(svg, "    </marker>").unwrap();
832        writeln!(svg, "  </defs>").unwrap();
833
834        // Build task hierarchy and relationships
835        let mut parent_child: Vec<(crate::task::TaskId, crate::task::TaskId)> = Vec::new();
836        let mut root_tasks: Vec<&TaskInfo> = Vec::new();
837
838        for task in &tasks {
839            if let Some(parent_id) = task.parent {
840                parent_child.push((parent_id, task.id));
841            } else {
842                root_tasks.push(task);
843            }
844        }
845
846        // Layout tasks in layers (hierarchical layout)
847        let mut task_positions: HashMap<crate::task::TaskId, (f64, f64)> = HashMap::new();
848        let mut layers: Vec<Vec<crate::task::TaskId>> = Vec::new();
849
850        // Layer 0: root tasks
851        if root_tasks.is_empty() {
852            // If no root tasks, treat all as layer 0
853            layers.push(tasks.iter().map(|t| t.id).collect());
854        } else {
855            layers.push(root_tasks.iter().map(|t| t.id).collect());
856        }
857
858        // Build subsequent layers from parent-child relationships
859        let mut processed: HashSet<crate::task::TaskId> = layers[0].iter().copied().collect();
860        loop {
861            let last_layer = layers.last().unwrap();
862            let mut next_layer = Vec::new();
863
864            for &parent_id in last_layer {
865                for &(pid, cid) in &parent_child {
866                    if pid == parent_id && !processed.contains(&cid) {
867                        next_layer.push(cid);
868                        processed.insert(cid);
869                    }
870                }
871            }
872
873            if next_layer.is_empty() {
874                break;
875            }
876            layers.push(next_layer);
877        }
878
879        // Position tasks
880        let layer_height = 120.0;
881        let base_y = 80.0;
882
883        for (layer_idx, layer) in layers.iter().enumerate() {
884            let y = base_y + (layer_idx as f64 * layer_height);
885            let layer_width = width - 100.0;
886            let spacing = if layer.len() > 1 {
887                layer_width / (layer.len() - 1) as f64
888            } else {
889                0.0
890            };
891
892            for (i, &task_id) in layer.iter().enumerate() {
893                let x = if layer.len() == 1 {
894                    width / 2.0
895                } else {
896                    50.0 + (i as f64 * spacing)
897                };
898                task_positions.insert(task_id, (x, y));
899            }
900        }
901
902        // Draw parent-child relationships
903        for &(parent_id, child_id) in &parent_child {
904            if let (Some(&(x1, y1)), Some(&(x2, y2))) = (
905                task_positions.get(&parent_id),
906                task_positions.get(&child_id),
907            ) {
908                writeln!(svg, "  <line class=\"state-transition\" x1=\"{}\" y1=\"{}\" x2=\"{}\" y2=\"{}\" stroke=\"#667eea\" stroke-width=\"2\" marker-end=\"url(#arrowhead-parent)\" stroke-dasharray=\"5,5\" />",
909                    x1, y1 + 35.0, x2, y2 - 35.0).unwrap();
910
911                // Add label
912                let mid_x = (x1 + x2) / 2.0;
913                let mid_y = (y1 + y2) / 2.0;
914                writeln!(svg, "  <text x=\"{}\" y=\"{}\" class=\"transition-label\" fill=\"#667eea\">spawns</text>",
915                    mid_x + 10.0, mid_y).unwrap();
916            }
917        }
918
919        // Draw blocking relationships (tasks waiting on each other)
920        // This would come from await points and blocked states
921        for task in &tasks {
922            if let TaskState::Blocked { ref await_point } = task.state {
923                // Find if await_point references another task
924                for other_task in &tasks {
925                    if other_task.id != task.id && await_point.contains(&other_task.name) {
926                        if let (Some(&(x1, y1)), Some(&(x2, y2))) = (
927                            task_positions.get(&task.id),
928                            task_positions.get(&other_task.id),
929                        ) {
930                            writeln!(svg, "  <path class=\"state-transition\" d=\"M {} {} Q {} {} {} {}\" stroke=\"#ff9800\" stroke-width=\"2\" marker-end=\"url(#arrowhead-blocked)\" />",
931                                x1 + 30.0, y1, x1 + 50.0, (y1 + y2) / 2.0, x2 - 30.0, y2).unwrap();
932
933                            writeln!(svg, "  <text x=\"{}\" y=\"{}\" class=\"transition-label\" fill=\"#ff9800\">waits for</text>",
934                                x1 + 60.0, (y1 + y2) / 2.0).unwrap();
935                        }
936                    }
937                }
938            }
939        }
940
941        // Draw task nodes
942        for task in &tasks {
943            if let Some(&(x, y)) = task_positions.get(&task.id) {
944                let state_class = match task.state {
945                    TaskState::Pending => "pending",
946                    TaskState::Running => "running",
947                    TaskState::Blocked { .. } => "blocked",
948                    TaskState::Completed => "completed",
949                    TaskState::Failed => "failed",
950                };
951
952                // Draw rounded rectangle for task
953                writeln!(
954                    svg,
955                    "  <g class=\"state-node {}\" data-task-id=\"{}\">",
956                    state_class, task.id
957                )
958                .unwrap();
959                writeln!(svg, "    <rect x=\"{}\" y=\"{}\" width=\"120\" height=\"70\" rx=\"10\" ry=\"10\" />", x - 60.0, y - 35.0).unwrap();
960
961                // Task name (truncate if needed)
962                let display_name = if task.name.len() > 12 {
963                    format!("{}...", &task.name[..9])
964                } else {
965                    task.name.clone()
966                };
967                writeln!(
968                    svg,
969                    "    <text x=\"{}\" y=\"{}\" font-size=\"13\">{}</text>",
970                    x,
971                    y - 5.0,
972                    display_name
973                )
974                .unwrap();
975
976                // Task ID
977                writeln!(svg, "    <text x=\"{}\" y=\"{}\" font-size=\"10\" fill=\"white\" opacity=\"0.8\">#{}</text>",
978                    x, y + 12.0, task.id.as_u64()).unwrap();
979
980                // State indicator
981                let state_text = match task.state {
982                    TaskState::Pending => "⏸ Pending",
983                    TaskState::Running => "▶ Running",
984                    TaskState::Blocked { .. } => "⏳ Blocked",
985                    TaskState::Completed => "✓ Done",
986                    TaskState::Failed => "✗ Failed",
987                };
988                writeln!(svg, "    <text x=\"{}\" y=\"{}\" font-size=\"9\" fill=\"white\" opacity=\"0.9\">{}</text>",
989                    x, y + 25.0, state_text).unwrap();
990
991                // Tooltip
992                writeln!(
993                    svg,
994                    "    <title>{}\nState: {:?}\nPoll count: {}\nRuntime: {:.2}ms</title>",
995                    task.name,
996                    task.state,
997                    task.poll_count,
998                    task.total_run_time.as_millis()
999                )
1000                .unwrap();
1001                writeln!(svg, "  </g>").unwrap();
1002            }
1003        }
1004
1005        // Add legend
1006        let legend_y = height - 80.0;
1007        writeln!(svg, "  <text x=\"20\" y=\"{legend_y}\" font-size=\"14\" font-weight=\"bold\" fill=\"#333\">Relationships:</text>").unwrap();
1008        writeln!(svg, "  <line x1=\"20\" y1=\"{}\" x2=\"80\" y2=\"{}\" stroke=\"#667eea\" stroke-width=\"2\" stroke-dasharray=\"5,5\" marker-end=\"url(#arrowhead-parent)\" />",
1009            legend_y + 15.0, legend_y + 15.0).unwrap();
1010        writeln!(
1011            svg,
1012            "  <text x=\"90\" y=\"{}\" font-size=\"12\" fill=\"#666\">Parent spawns child</text>",
1013            legend_y + 20.0
1014        )
1015        .unwrap();
1016
1017        writeln!(svg, "  <line x1=\"20\" y1=\"{}\" x2=\"80\" y2=\"{}\" stroke=\"#ff9800\" stroke-width=\"2\" marker-end=\"url(#arrowhead-blocked)\" />",
1018            legend_y + 35.0, legend_y + 35.0).unwrap();
1019        writeln!(
1020            svg,
1021            "  <text x=\"90\" y=\"{}\" font-size=\"12\" fill=\"#666\">Task waits for</text>",
1022            legend_y + 40.0
1023        )
1024        .unwrap();
1025
1026        writeln!(svg, "</svg>").unwrap();
1027
1028        svg
1029    }
1030
1031    /// Generate task list with details
1032    fn generate_task_list(&self) -> String {
1033        let tasks = self.inspector.get_all_tasks();
1034        let mut html = String::new();
1035
1036        writeln!(html, "        <div class=\"task-list\">").unwrap();
1037        writeln!(html, "            <h2>Task Details</h2>").unwrap();
1038
1039        for task in &tasks {
1040            html.push_str(&self.generate_task_item(task));
1041        }
1042
1043        writeln!(html, "        </div>").unwrap();
1044
1045        html
1046    }
1047
1048    /// Generate a single task item
1049    fn generate_task_item(&self, task: &TaskInfo) -> String {
1050        let mut html = String::new();
1051
1052        let (state_class, state_text) = match task.state {
1053            TaskState::Completed => ("completed", "Completed"),
1054            TaskState::Running => ("running", "Running"),
1055            TaskState::Blocked { .. } => ("blocked", "Blocked"),
1056            TaskState::Failed => ("failed", "Failed"),
1057            TaskState::Pending => ("pending", "Pending"),
1058        };
1059
1060        writeln!(
1061            html,
1062            "            <div class=\"task-item\" data-task-id=\"{}\">",
1063            task.id
1064        )
1065        .unwrap();
1066        writeln!(html, "                <div class=\"task-header\">").unwrap();
1067        writeln!(
1068            html,
1069            "                    <div class=\"task-name\">{}</div>",
1070            task.name
1071        )
1072        .unwrap();
1073        writeln!(
1074            html,
1075            "                    <div class=\"task-state state-{state_class}\">{state_text}</div>"
1076        )
1077        .unwrap();
1078        writeln!(html, "                </div>").unwrap();
1079
1080        // Expandable details
1081        writeln!(html, "                <div class=\"task-details\">").unwrap();
1082        writeln!(html, "                    <div class=\"task-meta\">").unwrap();
1083        writeln!(html, "                        <div class=\"meta-item\">").unwrap();
1084        writeln!(
1085            html,
1086            "                            <div class=\"meta-label\">Task ID</div>"
1087        )
1088        .unwrap();
1089        writeln!(
1090            html,
1091            "                            <div class=\"meta-value\">{}</div>",
1092            task.id
1093        )
1094        .unwrap();
1095        writeln!(html, "                        </div>").unwrap();
1096        writeln!(html, "                        <div class=\"meta-item\">").unwrap();
1097        writeln!(
1098            html,
1099            "                            <div class=\"meta-label\">Age</div>"
1100        )
1101        .unwrap();
1102        writeln!(
1103            html,
1104            "                            <div class=\"meta-value\">{:.2}ms</div>",
1105            task.age().as_millis()
1106        )
1107        .unwrap();
1108        writeln!(html, "                        </div>").unwrap();
1109        writeln!(html, "                        <div class=\"meta-item\">").unwrap();
1110        writeln!(
1111            html,
1112            "                            <div class=\"meta-label\">Poll Count</div>"
1113        )
1114        .unwrap();
1115        writeln!(
1116            html,
1117            "                            <div class=\"meta-value\">{}</div>",
1118            task.poll_count
1119        )
1120        .unwrap();
1121        writeln!(html, "                        </div>").unwrap();
1122        writeln!(html, "                        <div class=\"meta-item\">").unwrap();
1123        writeln!(
1124            html,
1125            "                            <div class=\"meta-label\">Total Runtime</div>"
1126        )
1127        .unwrap();
1128        writeln!(
1129            html,
1130            "                            <div class=\"meta-value\">{:.2}ms</div>",
1131            task.total_run_time.as_millis()
1132        )
1133        .unwrap();
1134        writeln!(html, "                        </div>").unwrap();
1135        writeln!(html, "                    </div>").unwrap();
1136
1137        // Events
1138        let events = self.inspector.get_task_events(task.id);
1139        if !events.is_empty() {
1140            writeln!(html, "                    <div class=\"events-section\">").unwrap();
1141            writeln!(
1142                html,
1143                "                        <h4>Events ({} total)</h4>",
1144                events.len()
1145            )
1146            .unwrap();
1147            for event in events.iter().take(10) {
1148                writeln!(html, "                        <div class=\"event-item\">").unwrap();
1149                writeln!(
1150                    html,
1151                    "                            <span class=\"event-time\">[{:.3}ms]</span> {}",
1152                    event.age().as_millis(),
1153                    event.kind
1154                )
1155                .unwrap();
1156                writeln!(html, "                        </div>").unwrap();
1157            }
1158            if events.len() > 10 {
1159                writeln!(html, "                        <div style=\"margin-top: 10px; color: #666; font-size: 0.85em;\">... and {} more events</div>", events.len() - 10).unwrap();
1160            }
1161            writeln!(html, "                    </div>").unwrap();
1162        }
1163
1164        writeln!(html, "                </div>").unwrap();
1165        writeln!(html, "            </div>").unwrap();
1166
1167        html
1168    }
1169
1170    /// Generate JavaScript for interactivity
1171    fn generate_javascript(&self) -> String {
1172        String::from(
1173            r##"
1174    <script>
1175        // Task item click to expand/collapse
1176        document.querySelectorAll('.task-item').forEach(item => {
1177            item.addEventListener('click', (e) => {
1178                // Don't toggle if clicking on a link or button
1179                if (e.target.tagName === 'A' || e.target.tagName === 'BUTTON') {
1180                    return;
1181                }
1182                item.classList.toggle('expanded');
1183            });
1184        });
1185
1186        // SVG task row click to highlight corresponding task item
1187        document.querySelectorAll('.task-row').forEach(row => {
1188            row.addEventListener('click', (e) => {
1189                const taskId = row.getAttribute('data-task-id');
1190                const taskItem = document.querySelector(`.task-item[data-task-id="${taskId}"]`);
1191
1192                if (taskItem) {
1193                    // Scroll into view
1194                    taskItem.scrollIntoView({ behavior: 'smooth', block: 'center' });
1195
1196                    // Expand
1197                    taskItem.classList.add('expanded');
1198
1199                    // Flash highlight
1200                    taskItem.style.background = '#fff3cd';
1201                    setTimeout(() => {
1202                        taskItem.style.background = '';
1203                    }, 1000);
1204                }
1205            });
1206        });
1207
1208        // Add smooth scrolling
1209        document.querySelectorAll('a[href^="#"]').forEach(anchor => {
1210            anchor.addEventListener('click', function (e) {
1211                e.preventDefault();
1212                const target = document.querySelector(this.getAttribute('href'));
1213                if (target) {
1214                    target.scrollIntoView({ behavior: 'smooth' });
1215                }
1216            });
1217        });
1218    </script>
1219"##,
1220        )
1221    }
1222
1223    /// Save HTML report to file
1224    pub fn save_to_file(&self, path: &str) -> std::io::Result<()> {
1225        let html = self.generate_html();
1226        std::fs::write(path, html)?;
1227        Ok(())
1228    }
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233    use super::*;
1234
1235    #[test]
1236    fn test_html_generation() {
1237        let inspector = Inspector::new();
1238        inspector.register_task("test_task".to_string());
1239
1240        let reporter = HtmlReporter::new(inspector);
1241        let html = reporter.generate_html();
1242
1243        assert!(html.contains("<!DOCTYPE html>"));
1244        assert!(html.contains("async-inspect"));
1245        assert!(html.contains("test_task"));
1246    }
1247
1248    #[test]
1249    fn test_save_to_file() {
1250        let inspector = Inspector::new();
1251        let reporter = HtmlReporter::new(inspector);
1252
1253        // Use cross-platform temp directory
1254        let temp_dir = std::env::temp_dir();
1255        let temp_file = temp_dir.join("async_inspect_test.html");
1256        reporter.save_to_file(temp_file.to_str().unwrap()).unwrap();
1257
1258        let content = std::fs::read_to_string(&temp_file).unwrap();
1259        assert!(content.contains("<!DOCTYPE html>"));
1260
1261        // Cleanup
1262        std::fs::remove_file(temp_file).ok();
1263    }
1264}