""" HTML Report Generator Generates comprehensive HTML reports from migration validation comparison results. """ import json import logging from typing import Dict, Any from datetime import datetime from pathlib import Path logger = logging.getLogger(__name__) class HTMLReportGenerator: """Generator for HTML migration validation reports.""" def __init__(self): self.css_styles = self.get_css_styles() self.js_scripts = self.get_js_scripts() def generate_report(self, comparison: Dict[str, Any], output_file: str) -> str: """Generate HTML report from comparison data.""" logger.info(f"Generating HTML report: {output_file}") html_content = self.build_html_content(comparison) with open(output_file, 'w', encoding='utf-8') as f: f.write(html_content) logger.info(f"HTML report generated: {output_file}") return output_file def build_html_content(self, comparison: Dict[str, Any]) -> str: """Build complete HTML content.""" metadata = comparison.get("metadata", {}) summary = comparison.get("summary", {}) differences = comparison.get("differences", {}) risk_assessment = comparison.get("risk_assessment", {}) validation_results = comparison.get("validation_results", {}) html = f""" Migration Validation Report

Migration Validation Report

Report Generated: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}

Comparison ID: {metadata.get('comparison_id', 'N/A')}

Snapshot 1: {metadata.get('snapshot1', 'N/A')}

Snapshot 2: {metadata.get('snapshot2', 'N/A')}

Executive Summary

{self.generate_summary_section(summary)}

Risk Assessment

{self.generate_risk_section(risk_assessment)}

Validation Results

{self.generate_validation_section(validation_results)}

Detailed Changes

{self.generate_changes_section(differences)}

Recommendations

{self.generate_recommendations_section(risk_assessment)}
""" return html def generate_summary_section(self, summary: Dict[str, Any]) -> str: """Generate executive summary HTML.""" total_systems = summary.get('total_systems', 0) systems_with_changes = summary.get('systems_with_changes', 0) total_changes = summary.get('total_changes', 0) html = f"""

Systems Analyzed

{total_systems}

Systems with Changes

{systems_with_changes}

Total Changes

{total_changes}

Changes by Type

""" for data_type, count in summary.get('changes_by_type', {}).items(): html += f""" """ html += """
Data Type Changes
{data_type.replace('_', ' ').title()} {count}

Most Affected Systems

""" for system, count in summary.get('most_affected_systems', []): html += f""" """ html += """
System Changes
{system} {count}
""" return html def generate_risk_section(self, risk_assessment: Dict[str, Any]) -> str: """Generate risk assessment HTML.""" overall_risk = risk_assessment.get('overall_risk', 'unknown') risk_color = self.get_risk_color(overall_risk) html = f"""

Overall Risk Level

{overall_risk.upper()}

Risk Factors

""" for factor in risk_assessment.get('risk_factors', []): factor_color = self.get_risk_color(self.get_risk_level_name(factor.get('level', 1))) html += f"""
{self.get_risk_level_name(factor.get('level', 1)).upper()}
{factor.get('type', 'Unknown').replace('_', ' ').title()}

{factor.get('description', 'No description')}

""" html += """

Critical Changes

""" for change in risk_assessment.get('critical_changes', []): html += f"""

{change.get('system', 'Unknown System')}

Type: {change.get('data_type', 'Unknown')}

{change.get('factor', {}).get('description', 'No details')}

""" html += "
" return html def generate_validation_section(self, validation_results: Dict[str, Any]) -> str: """Generate validation results HTML.""" passed = validation_results.get('passed', False) status_color = "#28a745" if passed else "#dc3545" status_text = "PASSED" if passed else "FAILED" html = f"""
{status_text}

Validation Checks

""" for check in validation_results.get('checks', []): check_status = "✓" if check.get('passed', False) else "✗" check_color = "#28a745" if check.get('passed', False) else "#dc3545" html += f"""
{check_status}

{check.get('name', 'Unknown').replace('_', ' ').title()}

{check.get('description', 'No description')}

{"
    " + "".join(f"
  • {detail}
  • " for detail in check.get('details', [])) + "
" if check.get('details') else ""}
""" html += "
" return html def generate_changes_section(self, differences: Dict[str, Any]) -> str: """Generate detailed changes HTML.""" html = "" for data_type, systems in differences.items(): html += f"

{data_type.replace('_', ' ').title()} Changes

" for system, system_diffs in systems.items(): html += f"

System: {system}

" # Generate tables for different change types html += self.generate_change_tables(system_diffs) return html def generate_change_tables(self, system_diffs: Dict[str, Any]) -> str: """Generate HTML tables for different types of changes.""" html = "" change_types = { 'added_mounts': ('Added Mounts', ['mountpoint', 'device', 'fstype']), 'removed_mounts': ('Removed Mounts', ['mountpoint', 'device', 'fstype']), 'changed_mounts': ('Changed Mounts', ['mountpoint', 'before', 'after']), 'usage_changes': ('Usage Changes', ['mountpoint', 'before', 'after']), 'added_services': ('Added Services', ['name', 'active_state', 'sub_state']), 'removed_services': ('Removed Services', ['name', 'active_state', 'sub_state']), 'status_changes': ('Service Status Changes', ['name', 'before', 'after']), 'filesystem_changes': ('Filesystem Changes', ['mountpoint', 'before', 'after']), 'directory_size_changes': ('Directory Size Changes', ['path', 'before', 'after']), 'significant_usage_changes': ('Significant Usage Changes', ['mountpoint', 'change_percent', 'before', 'after']) } for change_key, (title, columns) in change_types.items(): if change_key in system_diffs and system_diffs[change_key]: html += f"
{title}
" html += '' html += '' for col in columns: html += f'' html += '' for item in system_diffs[change_key]: html += '' for col in columns: value = item.get(col, '') if isinstance(value, dict): value = json.dumps(value, indent=2) html += f'' html += '' html += '
{col.replace("_", " ").title()}
{value}
' return html def generate_recommendations_section(self, risk_assessment: Dict[str, Any]) -> str: """Generate recommendations HTML.""" recommendations = risk_assessment.get('recommendations', []) html = '
' if not recommendations: html += '

No specific recommendations at this time.

' else: html += '' html += '
' return html def get_risk_color(self, risk_level: str) -> str: """Get color for risk level.""" colors = { 'low': '#28a745', 'medium': '#ffc107', 'high': '#fd7e14', 'critical': '#dc3545' } return colors.get(risk_level.lower(), '#6c757d') def get_risk_level_name(self, level: int) -> str: """Get risk level name from numeric level.""" levels = {1: 'low', 2: 'medium', 3: 'high', 4: 'critical'} return levels.get(level, 'unknown') def get_css_styles(self) -> str: """Get CSS styles for the report.""" return """ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 20px; background-color: #f8f9fa; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); } header { text-align: center; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #e9ecef; } h1 { color: #2c3e50; margin-bottom: 10px; } .report-meta { color: #6c757d; font-size: 0.9em; } .toc { background: #f8f9fa; padding: 20px; border-radius: 5px; margin-bottom: 30px; } .toc ul { list-style: none; padding: 0; } .toc li { margin: 5px 0; } .toc a { color: #007bff; text-decoration: none; } .toc a:hover { text-decoration: underline; } section { margin-bottom: 40px; } h2 { color: #2c3e50; border-bottom: 1px solid #e9ecef; padding-bottom: 10px; } h3 { color: #495057; margin-top: 30px; } .summary-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin: 20px 0; } .summary-card { background: #f8f9fa; padding: 20px; border-radius: 5px; text-align: center; } .metric { font-size: 2em; font-weight: bold; color: #007bff; margin: 10px 0; } table { width: 100%; border-collapse: collapse; margin: 20px 0; } th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e9ecef; } th { background-color: #f8f9fa; font-weight: 600; } .risk-overview { text-align: center; margin: 20px 0; } .risk-badge { display: inline-block; padding: 8px 16px; border-radius: 20px; color: white; font-weight: bold; margin: 10px; } .risk-factors, .critical-changes { margin: 20px 0; } .risk-factor, .critical-change { background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0; border-left: 4px solid #007bff; } .risk-factor { display: flex; align-items: center; } .risk-details { margin-left: 15px; } .validation-status { text-align: center; margin: 20px 0; } .status-indicator { display: inline-block; padding: 15px 30px; border-radius: 5px; color: white; font-weight: bold; font-size: 1.2em; } .validation-checks { margin: 20px 0; } .validation-check { display: flex; align-items: flex-start; background: #f8f9fa; padding: 15px; border-radius: 5px; margin: 10px 0; } .check-status { font-size: 1.5em; margin-right: 15px; margin-top: 5px; } .check-details { flex: 1; } .recommendations ul { background: #e7f3ff; padding: 20px; border-radius: 5px; border-left: 4px solid #007bff; } .recommendations li { margin: 10px 0; } pre { background: #f8f9fa; padding: 10px; border-radius: 3px; overflow-x: auto; max-width: 100%; white-space: pre-wrap; word-wrap: break-word; } @media (max-width: 768px) { .container { padding: 15px; } .summary-grid { grid-template-columns: 1fr; } .risk-factor { flex-direction: column; text-align: center; } .validation-check { flex-direction: column; } } """ def get_js_scripts(self) -> str: """Get JavaScript for interactive features.""" return """ // Add smooth scrolling to TOC links document.addEventListener('DOMContentLoaded', function() { const links = document.querySelectorAll('.toc a'); links.forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const target = document.querySelector(this.getAttribute('href')); if (target) { target.scrollIntoView({ behavior: 'smooth' }); } }); }); // Add collapsible sections for large tables const tables = document.querySelectorAll('table'); tables.forEach(table => { if (table.rows.length > 10) { const toggle = document.createElement('button'); toggle.textContent = 'Toggle Details'; toggle.style.marginBottom = '10px'; toggle.addEventListener('click', function() { const tbody = table.querySelector('tbody'); tbody.style.display = tbody.style.display === 'none' ? '' : 'none'; }); table.parentNode.insertBefore(toggle, table); } }); }); """ def generate(comparison: Dict[str, Any], output_file: str) -> str: """Main function to generate HTML report.""" generator = HTMLReportGenerator() return generator.generate_report(comparison, output_file)