From 62da957b0392ac9cc8241287b2630257d55e53d2 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Thu, 5 Feb 2026 17:30:21 +0000 Subject: [PATCH] fix: resolve CI issues - remove unused imports and fix code quality --- .../src/repohealth/reporters/html_reporter.py | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 repohealth-cli/src/repohealth/reporters/html_reporter.py diff --git a/repohealth-cli/src/repohealth/reporters/html_reporter.py b/repohealth-cli/src/repohealth/reporters/html_reporter.py new file mode 100644 index 0000000..8fca612 --- /dev/null +++ b/repohealth-cli/src/repohealth/reporters/html_reporter.py @@ -0,0 +1,346 @@ +from datetime import datetime +from pathlib import Path +from typing import Optional + +from jinja2 import Environment, FileSystemLoader, Template + +from repohealth.models.result import RepositoryResult + + +class HTMLReporter: + """Reporter for HTML output with visualizations.""" + + RISK_COLORS = { + "critical": "#dc3545", + "high": "#fd7e14", + "medium": "#ffc107", + "low": "#28a745", + "unknown": "#6c757d" + } + + def __init__(self, template_dir: Optional[str] = None): + """Initialize the reporter. + + Args: + template_dir: Directory containing Jinja2 templates. + """ + if template_dir: + self.template_dir = Path(template_dir) + else: + self.template_dir = Path(__file__).parent / "templates" + + self.env = Environment( + loader=FileSystemLoader(str(self.template_dir)), + autoescape=True + ) + + def generate(self, result: RepositoryResult) -> str: + """Generate HTML output from a result. + + Args: + result: RepositoryResult to convert. + + Returns: + HTML string. + """ + template = self.env.get_template("report.html") + return template.render( + result=result, + risk_colors=self.RISK_COLORS, + generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC") + ) + + def save(self, result: RepositoryResult, file_path: str) -> None: + """Save HTML output to a file. + + Args: + result: RepositoryResult to save. + file_path: Path to output file. + """ + html_content = self.generate(result) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(html_content) + + self._copy_assets(Path(file_path).parent) + + def _copy_assets(self, output_dir: Path) -> None: + """Copy CSS/JS assets to output directory. + + Args: + output_dir: Directory to copy assets to. + """ + assets_dir = output_dir / "assets" + assets_dir.mkdir(exist_ok=True) + + template_assets = self.template_dir / "assets" + if template_assets.exists(): + for asset in template_assets.iterdir(): + dest = assets_dir / asset.name + dest.write_text(asset.read_text()) + + def generate_charts_data(self, result: RepositoryResult) -> dict: + """Generate data for JavaScript charts. + + Args: + result: RepositoryResult to analyze. + + Returns: + Dictionary with chart data. + """ + risk_summary = result.risk_summary + + risk_distribution = { + "labels": ["Critical", "High", "Medium", "Low"], + "data": [ + risk_summary.get("critical", 0), + risk_summary.get("high", 0), + risk_summary.get("medium", 0), + risk_summary.get("low", 0) + ], + "colors": [ + self.RISK_COLORS["critical"], + self.RISK_COLORS["high"], + self.RISK_COLORS["medium"], + self.RISK_COLORS["low"] + ] + } + + def get_hotspot_attr(h, attr, default=None): + """Get attribute from hotspot dict or object.""" + if isinstance(h, dict): + return h.get(attr, default) + return getattr(h, attr, default) + + top_hotspots = [ + { + "file": get_hotspot_attr(h, "file_path", "")[:30], + "author": get_hotspot_attr(h, "top_author", "")[:20], + "share": round(get_hotspot_attr(h, "top_author_share", 0) * 100, 1), + "risk": get_hotspot_attr(h, "risk_level", "unknown") + } + for h in result.hotspots[:10] + ] + + file_data = [ + { + "name": f.get("path", "")[:30], + "commits": f.get("total_commits", 0), + "authors": f.get("num_authors", 0), + "bus_factor": round(f.get("bus_factor", 1), 2), + "risk": f.get("risk_level", "unknown") + } + for f in sorted( + result.files, + key=lambda x: ( + {"critical": 0, "high": 1, "medium": 2, "low": 3}.get( + x.get("risk_level"), 4 + ), + -x.get("bus_factor", 1) + ) + )[:20] + ] + + return { + "risk_distribution": risk_distribution, + "top_hotspots": top_hotspots, + "file_data": file_data, + "summary": { + "bus_factor": round(result.overall_bus_factor, 2), + "gini": round(result.gini_coefficient, 3), + "files": result.files_analyzed, + "authors": result.unique_authors + } + } + + def create_inline_template(self) -> Template: + """Create an inline template for standalone HTML reports. + + Returns: + Jinja2 Template with inline CSS/JS. + """ + template_str = """ + + + + + + Repository Health Report + + + + +
+
+

Repository Health Report

+

{{ result.repository_path }}

+

Generated: {{ generated_at }}

+
+ +
+
+

Summary

+
Files Analyzed{{ result.files_analyzed }}
+
Total Commits{{ result.total_commits }}
+
Unique Authors{{ result.unique_authors }}
+
Bus Factor{{ "%.2f"|format(result.overall_bus_factor) }}
+
Gini Coefficient{{ "%.3f"|format(result.gini_coefficient) }}
+
+ +
+

Risk Distribution

+
Critical{{ result.risk_summary.get('critical', 0) }}
+
High{{ result.risk_summary.get('high', 0) }}
+
Medium{{ result.risk_summary.get('medium', 0) }}
+
Low{{ result.risk_summary.get('low', 0) }}
+
+ +
+

Risk by Percentage

+

Critical: {{ "%.1f"|format(result.risk_summary.get('percentage_critical', 0)) }}%

+
+

High: {{ "%.1f"|format(result.risk_summary.get('percentage_high', 0)) }}%

+
+
+
+ +
+
+

Risk Distribution Chart

+
+ +
+
+ +
+

Top Knowledge Hotspots

+ + + + {% for hotspot in result.hotspots[:10] %} + + + + + + + {% endfor %} + +
FileAuthorShareRisk
{{ hotspot.file_path[:30] }}{{ hotspot.top_author[:15] }}{{ "%.0f"|format(hotspot.top_author_share * 100) }}%{{ hotspot.risk_level }}
+
+
+ + {% if result.suggestions %} +
+

Diversification Suggestions

+ {% for suggestion in result.suggestions %} +
+ {{ suggestion.priority|upper }}: {{ suggestion.action }} +
+ {% endfor %} +
+ {% endif %} + +
+

All Analyzed Files

+ + + + {% for file in result.files[:30] %} + + + + + + + + {% endfor %} + +
FileCommitsAuthorsBus FactorRisk
{{ file.path[:40] }}{{ file.total_commits }}{{ file.num_authors }}{{ "%.2f"|format(file.bus_factor) }}{{ file.risk_level }}
+
+
+ + + + +""" + return self.env.from_string(template_str) + + def generate_standalone(self, result: RepositoryResult) -> str: + """Generate standalone HTML with inline resources. + + Args: + result: RepositoryResult to convert. + + Returns: + Complete HTML string. + """ + template = self.create_inline_template() + charts_data = self.generate_charts_data(result) + + return template.render( + result=result, + generated_at=datetime.utcnow().strftime("%Y-%m-%d %H:%M UTC"), + charts_data=charts_data + ) + + def save_standalone(self, result: RepositoryResult, file_path: str) -> None: + """Save standalone HTML to a file. + + Args: + result: RepositoryResult to save. + file_path: Path to output file. + """ + html_content = self.generate_standalone(result) + + with open(file_path, 'w', encoding='utf-8') as f: + f.write(html_content)